From 776d20f1250995f27dac25918d31a3e73a7b232d Mon Sep 17 00:00:00 2001
From: ncastejon <castejon.nicolas@gmail.com>
Date: Fri, 26 Jul 2019 11:19:56 +0200
Subject: [PATCH] Use of lunr to filter by text the features

---
 package-lock.json                             |   5 +
 package.json                                  |   2 +
 .../services/elasticsearch.service.ts         |  72 ++---
 src/app/map/components/map.component.html     |   3 +
 src/app/map/components/map.component.ts       |   6 +
 src/app/map/services/map.service.ts           | 284 +++++++++++-------
 6 files changed, 234 insertions(+), 138 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index bda88f53..cdd312cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6449,6 +6449,11 @@
         "yallist": "^2.1.2"
       }
     },
+    "lunr": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.6.tgz",
+      "integrity": "sha512-swStvEyDqQ85MGpABCMBclZcLI/pBIlu8FFDtmX197+oEgKloJ67QnB+Tidh0340HmLMs39c4GrkPY3cmkXp6Q=="
+    },
     "magic-string": {
       "version": "0.22.5",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
diff --git a/package.json b/package.json
index 9a312834..bd9b5819 100644
--- a/package.json
+++ b/package.json
@@ -42,9 +42,11 @@
     "core-js": "^2.5.7",
     "file-saver": "^2.0.2",
     "font-awesome": "^4.7.0",
+    "geojson-vt": "^3.2.1",
     "hamburgers": "^1.1.3",
     "jwt-decode": "^2.2.0",
     "lodash.clonedeep": "^4.5.0",
+    "lunr": "^2.3.6",
     "mapbox-gl": "^0.47.0",
     "ng-inline-svg": "^8.2.1",
     "ng-lazyload-image": "^5.1.2",
diff --git a/src/app/geosource/services/elasticsearch.service.ts b/src/app/geosource/services/elasticsearch.service.ts
index 54f1b5e4..3dbc4a93 100644
--- a/src/app/geosource/services/elasticsearch.service.ts
+++ b/src/app/geosource/services/elasticsearch.service.ts
@@ -954,42 +954,42 @@ export class ElasticsearchService {
 
 
   getDataStream(uuid: string) {
-    let counter = 0;
-    let features = [];
-    const dataObs = new Observable((obs) => {
-      oboe(`http://localhost:9200/scrollStream/${uuid}`)
-        .node('![*]', (data) => {
-
-          // This callback will be called everytime a new object is
-          // found in the foods array.
-          features.push(data);
-          counter += 1;
-          if (counter === 100) {
-            counter = 0;
-            obs.next(features);
-            features = [];
-          }
-          // console.log('Go eat some', data);
-        })
-        .done((things) => {
-          console.log(
-            'there are', things.foods.length, 'things to eat',
-            'and', things.nonFoods.length, 'to avoid');
-        });
-    });
-    return dataObs;
-
-    // return this._http.get(`http://localhost:9200/scrollStream/${uuid}`).pipe(
-    //   map((e) => {
-    //     // console.log(e);
-    //     return e;
-    //   }),
-    //   catchError(
-    //     (err) => {
-    //       throw this._errorService.handleError(err, { message: notificationMessages.geosource.getDatasetById });
-    //     },
-    //   ),
-    // );
+    // let counter = 0;
+    // let features = [];
+    // const dataObs = new Observable((obs) => {
+    //   oboe(`http://localhost:9200/scrollStream/${uuid}`)
+    //     .node('![*]', (data) => {
+
+    //       // This callback will be called everytime a new object is
+    //       // found in the foods array.
+    //       features.push(data);
+    //       counter += 1;
+    //       if (counter === 100) {
+    //         counter = 0;
+    //         obs.next(features);
+    //         features = [];
+    //       }
+    //       // console.log('Go eat some', data);
+    //     })
+    //     .done((things) => {
+    //       console.log(
+    //         'there are', things.foods.length, 'things to eat',
+    //         'and', things.nonFoods.length, 'to avoid');
+    //     });
+    // });
+    // return dataObs;
+
+    return this._http.get(`http://localhost:9200/scroll/${uuid}`).pipe(
+      map((e) => {
+        // console.log(e);
+        return e;
+      }),
+      catchError(
+        (err) => {
+          throw this._errorService.handleError(err, { message: notificationMessages.geosource.getDatasetById });
+        },
+      ),
+    );
   }
 
   getDataFromCoordinates(filter, metadataId) {
diff --git a/src/app/map/components/map.component.html b/src/app/map/components/map.component.html
index 8eb83b02..0af027e6 100644
--- a/src/app/map/components/map.component.html
+++ b/src/app/map/components/map.component.html
@@ -1,4 +1,7 @@
 <div>
+  <input type="text" name="filter" id="filter" [(ngModel)]="filterTerm" [value]="filterTerm">
+  <button (click)="filter()">Filtrer</button>
+  
   <div id="map" class="mapbox-map"
     [ngClass]="{'fullscreen': fullscreen===true, 'display-details': selectedData !== null && displayDataDetails === true, 'hide-details': displayDataDetails === false && selectedData !== null}">
     <div id="menu" *ngIf="displayControls">
diff --git a/src/app/map/components/map.component.ts b/src/app/map/components/map.component.ts
index 89a4ff5d..853b22f5 100644
--- a/src/app/map/components/map.component.ts
+++ b/src/app/map/components/map.component.ts
@@ -49,6 +49,8 @@ export class MapComponent implements OnInit, OnDestroy {
 
   fullscreen = false;
 
+  filterTerm: string;
+
   constructor(
     private _datasetDetailService: DatasetDetailService,
     private _mapService: MapService,
@@ -233,4 +235,8 @@ export class MapComponent implements OnInit, OnDestroy {
     this._mapService.closePanel();
   }
 
+  filter() {
+    this._mapService.filterFeatures(this.filterTerm);
+  }
+
 }
diff --git a/src/app/map/services/map.service.ts b/src/app/map/services/map.service.ts
index dbe21c1a..49d5e38d 100644
--- a/src/app/map/services/map.service.ts
+++ b/src/app/map/services/map.service.ts
@@ -13,6 +13,7 @@ import { DatasetDetailService } from '../../geosource/services';
 import { settings } from '../settings';
 import * as cloneDeep from 'lodash.clonedeep';
 import { linkFormats } from '../../geosource/models/metadata.model';
+import * as lunr from 'lunr';
 
 @Injectable()
 export class MapService {
@@ -45,6 +46,9 @@ export class MapService {
   selectedFeature; // Contains the gid of the selected feature
 
   geojson: GeoJSON.FeatureCollection;
+  geojsonToDisplay: GeoJSON.FeatureCollection;
+
+
   totalData: number;
 
   // Properties used to send information to the component
@@ -52,6 +56,9 @@ export class MapService {
   private _mapToUpdate = new Subject<any>();
   private _mapUpdated = new Subject<any>();
 
+  // index
+  indexLunr: any;
+
   _errorService: any;
 
   constructor(
@@ -144,8 +151,10 @@ export class MapService {
       this.metadata,
       settings.maxDisplayFeatures).subscribe((geojson) => {
         const source = this._map.getSource('wfs-polygon') as mapboxgl.GeoJSONSource;
+        const source2 = this._map.getSource('wfs-clustered-points') as mapboxgl.GeoJSONSource;
         // console.log(this.geojson);
-        source.setData(this.geojson);
+        source.setData(this.geojsonToDisplay);
+        source2.setData(this.geojsonToDisplay);
       });
     this._map
       .on('zoomend', () => {
@@ -169,9 +178,9 @@ export class MapService {
     const bounds = this._map.getBounds();
     this.getWFSFeatures(this.metadata, settings.maxDisplayFeatures, bounds).subscribe((geojson) => {
       this.geojson = geojson;
-      // const source1 = this._map.getSource('wfs-clustered-points') as mapboxgl.GeoJSONSource;
+      const source1 = this._map.getSource('wfs-clustered-points') as mapboxgl.GeoJSONSource;
       const source2 = this._map.getSource('wfs-polygon') as mapboxgl.GeoJSONSource;
-      // source1.setData(geojson);
+      source1.setData(geojson);
       source2.setData(geojson);
       // Notify to the component that the map has been ipdated with new features
       this._mapUpdated.next(this.totalData);
@@ -183,13 +192,13 @@ export class MapService {
   // - Create the WFS layers from this source
   // - if the features are 'Point' type, create clustering layers
   addWFSLayer() {
-    // this._map.addSource('wfs-clustered-points', {
-    //   type: 'geojson',
-    //   data: this.geojson,
-    //   cluster: true,
-    //   clusterMaxZoom: 13, // Max zoom to cluster points on
-    //   clusterRadius: 45, // Radius of each cluster when clustering points (defaults to 50)
-    // });
+    this._map.addSource('wfs-clustered-points', {
+      type: 'geojson',
+      data: this.geojson,
+      cluster: true,
+      clusterMaxZoom: 13, // Max zoom to cluster points on
+      clusterRadius: 45, // Radius of each cluster when clustering points (defaults to 50)
+    });
 
     this._map.addSource('wfs-polygon', {
       type: 'geojson',
@@ -198,55 +207,55 @@ export class MapService {
 
     // Add the layers for 'Point' features (clustered and unclustered layers)
     // Create steps to display different circle size and colors depending the count
-    // this._map.addLayer({
-    //   id: 'point-features',
-    //   type: 'circle',
-    //   source: 'wfs-clustered-points',
-    //   paint: {
-    //     // Use step expressions (https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
-    //     // with three steps to implement three types of circles:
-    //     //   * 20px circles when point count is less than 20
-    //     //   * 30px circles when point count is between 20 and 50
-    //     //   * 40px circles when point count is greater than or equal to 50
-    //     'circle-color': this.featureColor,
-    //     'circle-radius': [
-    //       'step',
-    //       ['get', 'point_count'],
-    //       20, // 20px
-    //       20, // les than 20 features
-    //       30, // 30px
-    //       50, // until - more 50 features
-    //       40, // 40px
-    //     ],
-    //     'circle-stroke-width': [
-    //       'step',
-    //       ['get', 'point_count'],
-    //       4, // 4px
-    //       20, // les than 20 features
-    //       6, // 6px
-    //       50, // until - more 50 features
-    //       11, // 11px
-    //     ],
-    //     'circle-stroke-color': this.featureColorHalo,
-    //   },
-    //   filter: ['has', 'point_count'],
-    // });
+    this._map.addLayer({
+      id: 'clusters',
+      type: 'circle',
+      source: 'wfs-clustered-points',
+      paint: {
+        // Use step expressions (https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
+        // with three steps to implement three types of circles:
+        //   * 20px circles when point count is less than 20
+        //   * 30px circles when point count is between 20 and 50
+        //   * 40px circles when point count is greater than or equal to 50
+        'circle-color': this.featureColor,
+        'circle-radius': [
+          'step',
+          ['get', 'point_count'],
+          20, // 20px
+          20, // les than 20 features
+          30, // 30px
+          50, // until - more 50 features
+          40, // 40px
+        ],
+        'circle-stroke-width': [
+          'step',
+          ['get', 'point_count'],
+          4, // 4px
+          20, // les than 20 features
+          6, // 6px
+          50, // until - more 50 features
+          11, // 11px
+        ],
+        'circle-stroke-color': this.featureColorHalo,
+      },
+      filter: ['has', 'point_count'],
+    });
 
     // Add the cluster count layer (the number inside the circle)
-    // this._map.addLayer({
-    //   id: 'cluster-count',
-    //   type: 'symbol',
-    //   source: 'wfs-clustered-points',
-    //   filter: ['has', 'point_count'],
-    //   layout: {
-    //     'text-field': '{point_count_abbreviated}',
-    //     'text-size': 14,
-    //     'text-font': ['Noto Sans Bold'],
-    //   },
-    //   paint: {
-    //     'text-color': 'white',
-    //   },
-    // });
+    this._map.addLayer({
+      id: 'cluster-count',
+      type: 'symbol',
+      source: 'wfs-clustered-points',
+      filter: ['has', 'point_count'],
+      layout: {
+        'text-field': '{point_count_abbreviated}',
+        'text-size': 14,
+        'text-font': ['Noto Sans Bold'],
+      },
+      paint: {
+        'text-color': 'white',
+      },
+    });
 
     // For 'Polygon' feature highlighted
     this._map.addLayer({
@@ -319,11 +328,30 @@ export class MapService {
 
 
     // Add layer + style for the unclustered points highlighted
+    this._map.addLayer(
+      {
+        id: 'unclustered-point-highlighted',
+        type: 'circle',
+        source: 'wfs-clustered-points',
+        filter: ['!has', 'point_count'],
+        layout: {
+          visibility: 'none',
+        },
+        paint: {
+          'circle-stroke-width': 1,
+          'circle-stroke-opacity': 0.5,
+          'circle-stroke-color': '#000',
+          'circle-color': this.featureHighlightedColor,
+        },
+      },
+    );
+
     this._map.addLayer(
       {
         id: 'unclustered-point',
         type: 'circle',
-        source: 'wfs-polygon',
+        filter: ['!', ['has', 'point_count']],
+        source: 'wfs-clustered-points',
         paint: {
           'circle-stroke-width': 1,
           'circle-stroke-opacity': 0.5,
@@ -332,39 +360,59 @@ export class MapService {
 
         },
       },
-    );
+      'unclustered-point-highlighted',
 
-    // Add layer + style for the unclustered points
+    );
 
-    // this._map.addLayer(
-    //   {
-    //     id: 'unclustered-point-hover',
-    //     type: 'symbol',
-    //     source: 'wfs-clustered-points',
-    //     filter: ['==', '_featureId', ''],
-    //     layout: {
-    //       'icon-image': 'marker-hover',
-    //       'icon-size': 0.5,
-    //       'icon-anchor': 'bottom',
-    //       'icon-allow-overlap': true,
-    //       visibility: 'none',
-    //     },
-    //   },
-    // );
 
+    // Add layer + style for the unclustered points
 
+    this._map.addLayer(
+      {
+        id: 'unclustered-point-hover',
+        type: 'circle',
+        source: 'wfs-clustered-points',
+        filter: ['==', 'gid', ''],
+        layout: {
+          visibility: 'none',
+        },
+        paint: {
+          'circle-stroke-width': 1,
+          'circle-stroke-opacity': 0.5,
+          'circle-stroke-color': '#000',
+          'circle-color': this.featureColorHalo,
+        },
+      },
+    );
 
     if (!this.eventPopupAdded) {
+      // inspect a cluster on click
+      this._map.on('click', 'clusters', (e) => {
+        const features = this._map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
+        const clusterId = features[0].properties.cluster_id;
+        const source = this._map.getSource('wfs-clustered-points') as mapboxgl.GeoJSONSource;
+        source.getClusterExpansionZoom(clusterId, (err, zoom) => {
+          if (err) {
+            return;
+          }
+
+          this._map.easeTo({
+            zoom,
+            center: features[0].geometry['coordinates'],
+          });
+        });
+      });
+
       // Change the cursor to a pointer when the mouse is over the unclustered-point layer.
       this._map.on('mouseenter', 'unclustered-point', (e) => {
         this._map.getCanvas().style.cursor = 'pointer';
-        const hoveredFeature = e.features[0].properties._featureId;
-        // this._map.setFilter('unclustered-point-hover', ['==', '_featureId', hoveredFeature]);
-        // this._map.setLayoutProperty('unclustered-point-hover', 'visibility', 'visible');
+        const hoveredFeature = e.features[0].properties.gid;
+        this._map.setFilter('unclustered-point-hover', ['==', 'gid', hoveredFeature]);
+        this._map.setLayoutProperty('unclustered-point-hover', 'visibility', 'visible');
       }).on('mouseleave', 'unclustered-point', () => {
         this._map.getCanvas().style.cursor = '';
-        // this._map.setFilter('unclustered-point-hover', ['==', '_featureId', '']);
-        // this._map.setLayoutProperty('unclustered-point-hover', 'visibility', 'none');
+        this._map.setFilter('unclustered-point-hover', ['==', 'gid', '']);
+        this._map.setLayoutProperty('unclustered-point-hover', 'visibility', 'none');
 
       });
 
@@ -380,6 +428,13 @@ export class MapService {
         this._map.getCanvas().style.cursor = '';
       });
 
+      this._map.on('mouseenter', 'clusters', () => {
+        this._map.getCanvas().style.cursor = 'pointer';
+      });
+      this._map.on('mouseleave', 'clusters', () => {
+        this._map.getCanvas().style.cursor = '';
+      });
+
       // When a click event occurs on a feature in the states layer
       this._map.on('click', () => {
         // Reset state of panel
@@ -397,16 +452,16 @@ export class MapService {
 
   addClickEventOnLayer(layer, highlightedLayer) {
     this._map.on('click', layer, (e) => {
-      this.selectedFeature = e.features[0].properties._featureId;
-      this._map.setFilter(highlightedLayer, ['==', ['get', '_featureId'], this.selectedFeature]);
+      this.selectedFeature = e.features[0].properties.gid;
+      this._map.setFilter(highlightedLayer, ['==', ['get', 'gid'], this.selectedFeature]);
       this._map.setLayoutProperty(highlightedLayer, 'visibility', 'visible');
 
       this.handleMapPosition(e.point.x, e.lngLat, () => {
-        const feature = this.geojson.features.find(f => f.properties._featureId === this.selectedFeature);
+        const feature = this.geojson.features.find(f => f.properties.gid === this.selectedFeature);
         const featureCloned = cloneDeep(feature);
 
         // Remove the generated id from the properties to be displayed
-        delete featureCloned.properties._featureId;
+        delete featureCloned.properties.gid;
         this._panelState.next({ state: true, properties: featureCloned.properties });
       });
     });
@@ -526,8 +581,13 @@ export class MapService {
     //   count,
     //   coordinates).pipe(
     return this._elasticSearchService.getDataStream(this.metadata.geonet.uuid).pipe(
-      map((data: GeoJSON.Feature[]) => {
-        this.geojson.features = this.geojson.features.concat(data);
+      // map((data: GeoJSON.Feature[]) => {
+      map((data) => {
+        this.geojson.features = this.geojson.features.concat(data['features']);
+        console.log(data['index']);
+        this.indexLunr = lunr.Index.load(data['index']);
+
+
         // data.forEach((feature) => {
         //   this.geojson.features.push(feature);
         // });
@@ -541,26 +601,46 @@ export class MapService {
         //   feature.properties = newDataPropertiesOrder;
         // });
 
-        // const newFeatures = [];
-        // // If the features are 'MultiPoint' type, explode it into multiple 'Point'
-        // this.geojson.features.forEach((feature, index) => {
-        //   feature.properties['_featureId'] = index;
-        //   if (feature.geometry.type === 'MultiPoint') {
-        //     feature.geometry.coordinates.forEach((point) => {
-        //       const newFeature = Object.assign(feature);
-        //       newFeature.geometry.coordinates = point;
-        //       newFeature.geometry.type = 'Point';
-        //       newFeatures.push(newFeature);
-        //     });
-        //   } else {
-        //     newFeatures.push(feature);
-        //   }
-        // });
-        // this.geojson.features = newFeatures;
+        const newFeatures = [];
+        // If the features are 'MultiPoint' type, explode it into multiple 'Point'
+        this.geojson.features.forEach((feature, index) => {
+          feature.properties['gid'] = index;
+          if (feature.geometry.type === 'MultiPoint') {
+            feature.geometry.coordinates.forEach((point) => {
+              const newFeature = Object.assign(feature);
+              newFeature.geometry.coordinates = point;
+              newFeature.geometry.type = 'Point';
+              newFeatures.push(newFeature);
+            });
+          } else {
+            newFeatures.push(feature);
+          }
+        });
+        this.geojson.features = newFeatures;
+
+        // Create lunr index
+        this.geojsonToDisplay = { ...this.geojson };
         return this.geojson as GeoJSON.FeatureCollection;
       }));
   }
 
+  filterFeatures(filterTerm: string) {
+    const docsGid = this.indexLunr.search(filterTerm).map((d) => { return Number(d.ref); });
+    const featuresFiltered = [];
+    this.geojson.features.forEach((feature) => {
+      
+      if (docsGid.includes(feature.properties['gid'])) {
+        featuresFiltered.push(feature);
+      }
+    });
+
+    const source = this._map.getSource('wfs-polygon') as mapboxgl.GeoJSONSource;
+    const source2 = this._map.getSource('wfs-clustered-points') as mapboxgl.GeoJSONSource;
+    this.geojsonToDisplay.features = featuresFiltered;
+    source.setData(this.geojsonToDisplay);
+    source2.setData(this.geojsonToDisplay);
+  }
+
   // Used for WMS layer.
   // Get one feature from the coordinates
   getFeatureInfo(lng, lat) {
-- 
GitLab