diff --git a/src/app/elasticsearch/services/elasticsearch.service.ts b/src/app/elasticsearch/services/elasticsearch.service.ts index ac3d122b7ad431f6e5651ba21f5b30822a75ef68..92918b47943cbcaf894fb502157eac56014a39cd 100644 --- a/src/app/elasticsearch/services/elasticsearch.service.ts +++ b/src/app/elasticsearch/services/elasticsearch.service.ts @@ -6,7 +6,6 @@ import { geosource, notificationMessages } from '../../../i18n/traductions'; import { ErrorService } from '../../core/services'; import { APP_CONFIG } from '../../core/services/app-config.service'; import { scopesResearch } from '../../shared/variables'; -// tslint:disable-next-line: max-line-length import { Aggregation, ElasticsearchOptions, Filter, IElasticsearchResponse, IPostsESOptions } from '../models'; @Injectable() @@ -128,7 +127,7 @@ export class ElasticsearchService { } if (options.searchString !== '') { - const searchString = this.escapeSpecialCharacters(options.searchString, options.fromAutocompletion); + const searchString = this.escapeSpecialCharacters(options.searchString, options.fromAutocompletion, 'AND'); body.query.bool['must'] = [ { query_string: { @@ -622,7 +621,7 @@ export class ElasticsearchService { /** * Escape special characters except logical operators. * */ - escapeSpecialCharacters(searchString, fromAutocompletion) { + escapeSpecialCharacters(searchString: string, fromAutocompletion: boolean, joinOperator = '+') { let escapedSearchString = ''; /** If the request: @@ -639,7 +638,7 @@ export class ElasticsearchService { // We join each words with the '+' logical operator to accurate the results const words = escapedSearchString.split(/\s+/); - escapedSearchString = words.join(' + '); + escapedSearchString = words.join(` ${joinOperator} `); /** If not all this, we don't escape the "()" (and will be interpreted as logical characters by ES) * but we still do escape some other special characters diff --git a/src/app/map/components/map.component.html b/src/app/map/components/map.component.html index 9d5941967ad1a51df615fb128b2df9479642c89d..a8d91a08cb13d173b33fe8db9d6eaad089a46f98 100644 --- a/src/app/map/components/map.component.html +++ b/src/app/map/components/map.component.html @@ -24,6 +24,7 @@ </svg> </ng-template> </button> + <div class="buttons has-addons column-content is-hidden-mobile"> <ng-container *ngFor="let l of settings.baseLayers"> <button class="button selectBase" [disabled]="l.id === selectedBaseLayer.id" @@ -34,6 +35,12 @@ </ng-container> </div> </div> + <div class="button-3d" *ngIf="displayButton3d"> + <button class="button" [ngClass]="{'is-active': display3d}" + (click)="switch3DLayer()" *ngIf="selectedBaseLayer.id===settings.baseLayers[0].id"> + 3D + </button> + </div> <div class="column is-narrow is-hidden-mobile"> <app-search-address [optionsAutocomplete]="searchLocationResult" (searchAddress)="searchAdress($event)" (addressSelected)="flyTo($event)" (clearAddress)="removeMarker()"> @@ -43,20 +50,17 @@ </div> <div class="geolocation-container"> - <button class="geolocation button is-medium" type="button" - i18n-aria-label="@@dataset.detail.map.center" aria-label="Center to my position" - (click)="centerToMyPosition()"> + <button class="geolocation button is-medium" type="button" i18n-aria-label="@@dataset.detail.map.center" + aria-label="Center to my position" (click)="centerToMyPosition()"> </button> </div> + <div class="copy-map"> <input type="text" class="input" id="mapUrlCopy" i18n-aria-label="@@dataset.detail.map.share" - aria-label="Share the map" - [value]="mapUrl()" #mapUrlElement> + aria-label="Share the map" [value]="mapUrl()" #mapUrlElement> <button class="button is-medium has-tooltip-left tooltip" i18n-aria-label="@@dataset.detail.map.share" - aria-label="Share the map" - type="button" (click)="copyMaplink(mapUrlElement)" - [attr.data-tooltip]="shareMessage"> + aria-label="Share the map" type="button" (click)="copyMaplink(mapUrlElement)" [attr.data-tooltip]="shareMessage"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"> <path fill="#818080" fill-rule="evenodd" d="M1.852 7.777a2.619 2.619 0 0 1 0-3.703l2.222-2.222a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.703l-.37.37.74.741.37-.37a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.704l-2.222 2.222a2.619 2.619 0 0 1-3.704 0L9.26 15.185a2.619 2.619 0 0 1 0-3.704l.37-.37-.74-.741-.37.37a2.619 2.619 0 0 1-3.704 0L1.852 7.777zm7.037-.37l-.74-.74a1.048 1.048 0 0 0-1.482 1.48l.74.742-.37.37a.524.524 0 0 1-.74 0L3.332 6.296a.524.524 0 0 1 0-.74l2.223-2.223a.524.524 0 0 1 .74 0L9.26 6.296a.524.524 0 0 1 0 .74l-.37.371zm2.222 5.185l-.37.37a.524.524 0 0 0 0 .742l2.963 2.962a.524.524 0 0 0 .74 0l2.223-2.222a.524.524 0 0 0 0-.74l-2.963-2.963a.524.524 0 0 0-.741 0l-.37.37.74.74a1.047 1.047 0 1 1-1.481 1.482l-.74-.74z" diff --git a/src/app/map/components/map.component.scss b/src/app/map/components/map.component.scss index 25b28dd5408739a5767150e1bd7ba2b3d7f59a5f..a676ba2eece41da44aa6c4ceb05276893970dc6f 100644 --- a/src/app/map/components/map.component.scss +++ b/src/app/map/components/map.component.scss @@ -61,6 +61,23 @@ border: none; } +.button-3d { + position: absolute; + top: 50px; + right: 0; + + @media screen and (min-width: $tablet) { + left: 0; + } + + & > .button.is-active { + background-color: $blue-color; + color: white; + } +} + + + .copy-map, .geolocation-container { position: absolute; diff --git a/src/app/map/components/map.component.ts b/src/app/map/components/map.component.ts index 3ae47dcbc17242844936caf820ee332fb691fb78..a4ebce8614219fca9ee443dcd2e77b703a69fb6f 100644 --- a/src/app/map/components/map.component.ts +++ b/src/app/map/components/map.component.ts @@ -31,6 +31,7 @@ export class MapComponent implements OnInit, OnDestroy { // Key of the lay to be displayed from baseLayers in environment files selectedBaseLayer = this.settings.baseLayers[this.settings.defaultBaseLayer]; displayPitchSlider = false; + displayButton3d = false; display3d = false; // Attributes to manage the display of the pudate map button @@ -39,8 +40,6 @@ export class MapComponent implements OnInit, OnDestroy { previousDatasetId: string; availableLayers: string[]; - baseLayer3d = 1; - geolocation = false; searchLocationResult = []; @@ -206,12 +205,30 @@ export class MapComponent implements OnInit, OnDestroy { this.map.on('style.load', () => { this._mapService.addLayers(); }); + + this.map.on('zoomend', () => { + if (this.map.getZoom() < 14) { + this.displayButton3d = false; + + if (this.map.getLayer('3d-layer')) { + // this.display3d = false; + this.map.removeLayer('3d-layer'); + } + + } else if (this.map.getZoom() > 14) { + this.displayButton3d = true; + if (this.display3d) { + this._mapService.switch3DLayer(); + } + } + }); } } switchLayer(baseLayer) { this.selectedBaseLayer = baseLayer; this._mapService.switchLayer(baseLayer); + this.display3d = false; } // [WARNING] This toggle only works with two base layers @@ -222,6 +239,7 @@ export class MapComponent implements OnInit, OnDestroy { this.selectedBaseLayer = this.settings.baseLayers[0]; } this._mapService.switchLayer(this.selectedBaseLayer); + this.display3d = false; } // Looks for the language to be used, if not indicated in the url takes the navigator default language @@ -239,7 +257,7 @@ export class MapComponent implements OnInit, OnDestroy { switch3DLayer() { if (this.map.isStyleLoaded()) { - if (this.map.getSource('openmaptiles')) { + if (this.map.getSource('3d-source')) { this.display3d = !this.display3d; this._mapService.switch3DLayer(); } diff --git a/src/app/map/services/map.service.ts b/src/app/map/services/map.service.ts index 1e8f760ae1bc7ff656c14541b5dadb828562b515..03f970b68c2bac04a9fcfe6e140a6e532fa5ce51 100644 --- a/src/app/map/services/map.service.ts +++ b/src/app/map/services/map.service.ts @@ -8,12 +8,10 @@ import { Notification } from '../../core/models'; import { NotificationService } from '../../core/services'; import { APP_CONFIG } from '../../core/services/app-config.service'; import { DataType, MapOptions } from '../models/map-options'; -import { settings } from '../settings'; @Injectable() export class MapService { - settings = settings; private _map: mapboxgl.Map; private url: string; selectedBaseLayer: any; @@ -22,11 +20,10 @@ export class MapService { eventPopupAdded = false; mapIsConstructed: boolean = false; - // Map + // Map features colors featureColor: string = '#1d92ff'; featureHoverColor: string = '#E19190'; featureHighlightedColor: string = '#da322f'; // Tomato color - visitedColor: string = '#4668ab'; hoveredFeatureId: string; highlightedFeatureId: string; @@ -46,8 +43,6 @@ export class MapService { this.featureHighlightedColor, ['boolean', ['feature-state', 'hover'], false], this.featureHoverColor, - ['boolean', ['feature-state', 'visited'], false], - this.visitedColor, 'transparent', ]; @@ -55,6 +50,7 @@ export class MapService { private _notificationService: NotificationService, ) { } + // Init the map with basic options for controls, transform request etc... createMap( mapOptions: MapOptions, url: string, baseLayer: any, options?: mapboxgl.MapboxOptions): mapboxgl.Map { @@ -108,10 +104,22 @@ export class MapService { return this._map; } + // This adds 2 layers: + // - a WMS layer to display the visual part of the features (WMS service send a png or jpeg) + // - a data layer, created from a geojson or an MVT service. It is used for the features interaction (hover, click) addLayers() { this.addWMSLayer(); - // Add a geojson layer only if data from the metropole + // Add the 3d source. Constructed with MVT tiles from the 'fpc_fond_plan_communaut.fpctoit' dataset + const domain = this.mapOptions.vectorService.url.split('wfs')[1]; + const url = `${this.mapOptions.mvtUrl}${domain}?LAYERS= +fpc_fond_plan_communaut.fpctoit&map.imagetype=mvt&tilemode=gmap&tile={x}+{y}+{z}&mode=tile`; + this._map.addSource('3d-source', { + type: 'vector', + tiles: [url], + }); + + // Add the data layer only if it comes from the Lyon Metropole if (this.mapOptions.rasterService.url.includes(APP_CONFIG.backendUrls.wms)) { // There is two ways to add tha data layers: from a geojson or from a MVT url if (!this.mapOptions.isMVT) { @@ -153,7 +161,7 @@ export class MapService { // Set highlited style for the current feature this.highlightedFeatureId = this.selectedFeature; - this.changeFeatureState(this.highlightedFeatureId, { visited: true, highlight: true }); + this.changeFeatureState(this.highlightedFeatureId, { highlight: true }); if (e.point.x > (this._map.getCanvas().width - 400)) { // If the screen is not mobile the dataset data details panel push the map that is then smaller @@ -206,7 +214,6 @@ export class MapService { // Add the raster (WMS) layer addWMSLayer() { - // ------------------- WMS Source & Layer ------------------- this._map.addSource('wms-source', { type: 'raster', tiles: [ @@ -225,10 +232,11 @@ export class MapService { } + // Add the data layer (from geojson or MVT) addDataLayer() { - let layerOptions = {}; + // Set the paint options depending the geometry type // For 'Polygon' and 'MultiPolygon' features if (this.mapOptions.dataType.isAreal) { layerOptions = { @@ -236,15 +244,12 @@ export class MapService { paint: { 'fill-color': this.COLOR_EXPRESSION, 'fill-opacity': 0.7, - 'fill-outline-color': ['case', - ['boolean', ['feature-state', 'visited'], false], - 'white', - 'transparent', - ], + 'fill-outline-color': 'transparent', }, }; } + // For 'Line' and 'MultiLine' features if (this.mapOptions.dataType.isLinear) { layerOptions = { type: 'line', @@ -259,9 +264,8 @@ export class MapService { }; } + // For "Point" features if (this.mapOptions.dataType.isPunctual) { - // Add layer + style for the points - // Get paint options depending the dataset size layerOptions = { type: 'circle', paint: { @@ -274,15 +278,11 @@ export class MapService { 'circle-stroke-width': ['case', ['boolean', ['feature-state', 'highlight'], false], 5, - ['boolean', ['feature-state', 'visited'], false], - 1, 0, ], 'circle-stroke-color': ['case', ['boolean', ['feature-state', 'highlight'], false], this.featureHighlightedColor, - ['boolean', ['feature-state', 'visited'], false], - 'white', 'transparent', ], }, @@ -294,11 +294,13 @@ export class MapService { id: 'data-layer', source: 'vector-source', ...layerOptions, - ...(this.mapOptions.isMVT ? { 'source-layer': this.mapOptions.vectorService.name } : {}), // if not MVT, this property is not needed + ...(this.mapOptions.isMVT ? { 'source-layer': this.mapOptions.vectorService.name } : {}), // if from MVT, this property is needed }); + // If not already done, add all the events listeners if (!this.eventPopupAdded) { - // Manage the cursor and feature state for point-features layer when mouse events + + // Manage the cursor and feature state for data layer when mouse events this._map.on('mousemove', 'data-layer', (e) => { this.manageFeatureOnMouseMove(e.features); }); @@ -331,10 +333,8 @@ export class MapService { ); } }); - this.eventPopupAdded = true; } - } manageFeatureOnMouseMove(features: any) { @@ -364,40 +364,25 @@ export class MapService { } switch3DLayer() { - if (!this._map.getLayer('building-3d')) { - // Insert the layer beneath any symbol layer. - const layers = this._map.getStyle().layers; - let labelLayerId; - - for (let i = 0; i < layers.length; i += 1) { - if (layers[i].type === 'symbol' && layers[i].layout['text-field']) { - labelLayerId = layers[i].id; - break; - } - } + if (!this._map.getLayer('3d-layer')) { this._map.addLayer( { - id: 'building-3d', + id: '3d-layer', type: 'fill-extrusion', - source: 'openmaptiles', - 'source-layer': 'building', + source: '3d-source', + 'source-layer': 'fpc_fond_plan_communaut.fpctoit', paint: { - 'fill-extrusion-color': 'hsla(40, 37%, 50%, 1)', + 'fill-extrusion-color': '#E0E4EF', 'fill-extrusion-height': { - property: 'render_height', + property: 'htotale', type: 'identity', }, - 'fill-extrusion-base': { - property: 'render_min_height', - type: 'identity', - }, - 'fill-extrusion-opacity': 0.8, + 'fill-extrusion-opacity': 0.7, }, - }, - labelLayerId); + }); } else { - this._map.removeLayer('building-3d'); + this._map.removeLayer('3d-layer'); } } @@ -430,7 +415,7 @@ export class MapService { // Set highlited style for the current feature this.highlightedFeatureId = this.selectedFeature; - this.changeFeatureState(this.highlightedFeatureId, { visited: true, highlight: true }); + this.changeFeatureState(this.highlightedFeatureId, { highlight: true }); const pointCenter = selectedFeature.geometry.type === 'Point' ? selectedFeature.geometry.coordinates : centroid(selectedFeature).geometry.coordinates; @@ -448,31 +433,56 @@ export class MapService { /* * When the search value has been changed, we add a text expression - * that filters the features containing this text value in one of its properties. + * that filters the features containing this text value in at least one of its properties. * If there is a match we: * - decrease the opacity for the raster layer (WMS) which displays all the features * - show the found features from our data layer (WMT or GeoJSON) */ filterBySearchValue(searchValue: string, properties: string[]) { - const filters = []; if (searchValue) { - // Add the "in" expression (look for substring in string). To make it case insensitive, set - // both the text value and the property value to uppercase. - properties.forEach((property) => { - filters.push(['in', ['upcase', searchValue], ['upcase', ['to-string', ['get', property]]]]); + const escapedSearchString = searchValue.replace(/[\=~><\"\?^\${}\(\)\|\&\:\!\/[\]\\]/g, '\\$&'); + const words = escapedSearchString.split(/\s+/); + + // Some basic explanations for the operators expression we use: + // - "all": returns true if all the conditions are true + // - "any": returns true if one of the the conditions are true + // - "in": can be used in many context, here it looks for a substring in a string. To make it case insensitive, set + // both the search value and the property value to uppercase with the operator 'upcase'. + // To learn more about it: https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions + // Here is the format of the expression, for example with 2 words: + // ['all', + // ['any', + // ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property1]]]], + // ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property2]]]], + // ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property3]]]] + // ], + // ['any', + // ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property1]]]], + // ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property2]]]], + // ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property3]]]] + // ], + // ] + const anyFilter = []; + words.forEach((word) => { + const propertiesFilters = []; + properties.forEach((property) => { + propertiesFilters.push(['in', ['upcase', word], ['upcase', ['to-string', ['get', property]]]]); + }); + anyFilter.push(['any', ...propertiesFilters]); }); - // For each type layer if exists (point, line, polyon), add this filter and set a color to replace transparent - const copyColor = [...this.COLOR_EXPRESSION] - copyColor.splice(copyColor.length - 1, 0, ['any', ...filters], 'green'); + // Once this search filter expression is done, we add it to the existing paint options (for hover, selected) + // of the data-layer + const copyPaintOptions = [...this.COLOR_EXPRESSION]; + copyPaintOptions.splice(copyPaintOptions.length - 1, 0, ['all', ...anyFilter], 'green'); - this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, copyColor); + this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, copyPaintOptions); this._map.setPaintProperty('wms-layer', 'raster-opacity', 0.5); } else { - // If value is empty, remove the filter and set the opacity for the raster layer back to 1. + // If value is empty, remove the search expression and set the opacity for the raster layer back to 1. this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, this.COLOR_EXPRESSION); this._map.setPaintProperty('wms-layer', 'raster-opacity', 1); diff --git a/src/app/map/settings.ts b/src/app/map/settings.ts index ca5bacf5c07384c10046673c8c9f9b749360cb4f..b08e25eca920f32fad8eeb37b1fb6962251554cd 100644 --- a/src/app/map/settings.ts +++ b/src/app/map/settings.ts @@ -1,5 +1,4 @@ export const settings = { - maxDisplayFeatures: 500, // Map defaultBaseLayer: 0,