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