diff --git a/src/app/map/services/map.service.ts b/src/app/map/services/map.service.ts index 1236e15251075416efffbc06a4b6155d883411b7..dacea3736f58a0e90ab0a296d2f5ecaf540e490e 100644 --- a/src/app/map/services/map.service.ts +++ b/src/app/map/services/map.service.ts @@ -21,7 +21,10 @@ export class MapService { private _map: mapboxgl.Map; private url: string; selectedBaseLayer; + metadata: Metadata; + uriWFS: IMetadataLink; + eventPopupAdded = false; minimap: Minimap; mapIsConstructed: boolean = false; @@ -29,9 +32,12 @@ export class MapService { // Map featureColor: string = '#1d92ff'; - featureColorHalo: string = 'rgba(29, 146, 255, 0.3)'; + featureHoverColor: string = '#235b8f'; featureHighlightedColor: string = '#F72F2F'; // Tomato color + hoveredFeatureId: string; + highlightedFeatureId: string; + // This is used to remeber what is the previous dataset that the map displayed. // The reason is to know if we need to display the same map settings (for example the user went to another // tab), or if we display with default settings (if the user changed the dataset and go to the map). @@ -62,6 +68,8 @@ export class MapService { createMap(url: string, baseLayer: any, addControls: boolean, options?: mapboxgl.MapboxOptions): mapboxgl.Map { this.metadata = this._datasetDetailService.datasetMetadata; + this.uriWFS = this.metadata.link.find((e) => { return e.service === linkFormats.wfs; }); + // Reset to false in ordre to set event listener this.eventPopupAdded = false; // Re-initialize panel state @@ -78,6 +86,7 @@ export class MapService { // Subscribe to the error observable and send a notification this._errorSubscription = errorObservable.subscribe((v) => { + console.log(v); this._notificationService.notify( new Notification({ message: notificationMessages.geosource.mapError, @@ -125,8 +134,7 @@ export class MapService { // - if WFS exists but data > 100 000, or if WFS no exist: we display the WMS layer addLayers() { // Check if the metadata has WFS data format. - const uriWFS = this.metadata.link.find((e) => { return e.service === linkFormats.wfs; }); - if (uriWFS && this._datasetDetailService.datasetDataNumber < 100000) { + if (this.uriWFS && this._datasetDetailService.datasetDataNumber < 100000) { this._currentLayerType = linkFormats.wfs; this.getWFSFeatures( this.metadata, @@ -170,213 +178,258 @@ export class MapService { // - Create the WFS layers from this source // - if the features are 'Point' type, create clustering layers addWFSLayer() { + console.log(this.metadata); + const url = 'https://download.recette.data.grandlyon.com/mvt/grandlyon?LAYERS' + + `=${this.uriWFS.name}&map.imagetype=mvt&tilemode=gmap&tile={x}+{y}+{z}&mode=tile`; + console.log(url); + // this._map.addSource('wfs-clustered-points', { + // type: 'geojson', + // tiles: url, + // 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) + type: 'vector', + tiles: [url], }); + // this._map.addSource('wfs-polygon', { + // type: 'geojson', + // data: this.geojson, + // }); + this._map.addSource('wfs-polygon', { - type: 'geojson', - data: this.geojson, + type: 'vector', + tiles: [url], }); // 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'], - }); + if (false) { + 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.featureHoverColor, + }, + 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', - }, - }); + if (false) { + 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({ - id: 'polygon-features-highlight', - type: 'fill', - layout: { - visibility: 'none', - }, - source: 'wfs-polygon', - paint: { - 'fill-color': this.featureHighlightedColor, - 'fill-opacity': 0.4, - }, - filter: ['==', '$type', 'Polygon'], - }); - + if (false) { // TODO Remove + this._map.addLayer({ + id: 'polygon-features-highlight', + type: 'fill', + layout: { + visibility: 'none', + }, + source: 'wfs-polygon', + paint: { + 'fill-color': this.featureHighlightedColor, + 'fill-opacity': 0.4, + }, + filter: ['==', '$type', 'Polygon'], + }); + } // For 'Polygon' and 'MultiPolygon' features, one layer is enough (no cluster) this._map.addLayer( { id: 'polygon-features', type: 'fill', source: 'wfs-polygon', + 'source-layer': this.uriWFS.name, paint: { 'fill-color': this.featureColor, 'fill-opacity': 0.4, }, filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], - }, - 'polygon-features-highlight', - ); + }); // For 'LineString' feature selected highlight - this._map.addLayer({ - id: 'line-features-highlight', - type: 'line', - source: 'wfs-polygon', - layout: { - 'line-cap': 'round', - 'line-join': 'round', - visibility: 'none', - }, - paint: { - 'line-color': this.featureHighlightedColor, - 'line-width': 3, - 'line-opacity': 0.8, - }, - }); - - // For 'LineString' and 'MultiLineString features, one layer is enough (no cluster) - this._map.addLayer( - { - id: 'line-features', + if (0) { // TODO Remove + this._map.addLayer({ + id: 'line-features-highlight', type: 'line', source: 'wfs-polygon', layout: { 'line-cap': 'round', 'line-join': 'round', + visibility: 'none', }, paint: { - 'line-color': this.featureColor, + 'line-color': this.featureHighlightedColor, 'line-width': 3, 'line-opacity': 0.8, }, - filter: ['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], - }, - 'line-features-highlight', - ); - - this._map.loadImage('./assets/img/marker_blue_active.png', (error, image) => { - if (error) throw error; - this._map.addImage('marker-active', image); - - this._map.loadImage('./assets/img/marker_blue_hover.png', (error, image) => { - if (error) throw error; - this._map.addImage('marker-hover', image); - // Add layer + style for the unclustered points highlighted - this._map.addLayer( - { - id: 'unclustered-point-highlighted', - type: 'symbol', - source: 'wfs-clustered-points', - filter: ['!has', 'point_count'], - layout: { - 'icon-image': 'marker-active', - 'icon-size': 0.5, - 'icon-anchor': 'bottom', - 'icon-allow-overlap': true, - visibility: 'none', - }, + }); + } + + // For 'LineString' and 'MultiLineString features, one layer is enough (no cluster) + if (false) { // TODO Remove + this._map.addLayer( + { + id: 'line-features', + type: 'line', + source: 'wfs-polygon', + 'source-layer': this.uriWFS.name, + layout: { + 'line-cap': 'round', + 'line-join': 'round', }, - ); + paint: { + 'line-color': this.featureColor, + 'line-width': 3, + 'line-opacity': 0.8, + }, + filter: ['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], + }, + 'line-features-highlight', + ); + } - this._map.loadImage('./assets/img/marker_blue_normal.png', (error, image) => { - if (error) throw error; - this._map.addImage('marker', image); - // Add layer + style for the unclustered points - this._map.addLayer( - { - id: 'unclustered-point', - type: 'symbol', + // Add layer + style for the unclustered points highlighted + // this._map.addLayer( + // { + // id: 'unclustered-point-highlighted', + // type: 'circle', + // source: 'wfs-clustered-points', + // 'source-layer': this.uriWFS.name, + // filter: ['==', 'id', ''], + // layout: { + // visibility: 'none', + // }, + // paint: { + // 'circle-radius': { + // base: 1.75, + // stops: [[12, 2], [22, 180]], + // }, + // 'circle-color': this.featureHighlightedColor, + // }, + // }); + + // Add layer + style for the unclustered points + + this._map.addLayer( + { + id: 'unclustered-point', + type: 'circle', + source: 'wfs-clustered-points', + 'source-layer': this.uriWFS.name, + filter: ['match', ['geometry-type'], ['Point'], true, false], + paint: { + 'circle-radius': { + base: 1.75, + stops: [[12, 2], [22, 180]], + }, + 'circle-stroke-width': { + base: 0.3, + stops: [[12, 0.6], [22, 2]], + }, + 'circle-stroke-color': 'white', + 'circle-stroke-opacity': 0.7, + 'circle-color': ['case', + ['boolean', ['feature-state', 'hover'], false], + this.featureHoverColor, + ['boolean', ['feature-state', 'highlight'], false], + this.featureHighlightedColor, + this.featureColor, + ], + }, + }); + + if (!this.eventPopupAdded) { + // Change the cursor to a pointer when the mouse is over the unclustered-point layer. + this._map.on('mousemove', 'unclustered-point', (e) => { + if (e.features.length > 0) { + this._map.getCanvas().style.cursor = 'pointer'; + if (this.hoveredFeatureId) { + this._map.setFeatureState({ source: 'wfs-clustered-points', - filter: ['!has', 'point_count'], - layout: { - 'icon-image': 'marker', - 'icon-size': 0.5, - 'icon-anchor': 'bottom', - 'icon-allow-overlap': true, - }, + sourceLayer: this.uriWFS.name, + id: this.hoveredFeatureId, }, - 'unclustered-point-highlighted', - ); + // tslint:disable-next-line:align + { hover: false }); + } - this._map.addLayer( - { - id: 'unclustered-point-hover', - type: 'symbol', + this.hoveredFeatureId = e.features[0].id; + + // Display hover state only if not already highlighted + const state = this._map.getFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: this.uriWFS.name, + id: this.hoveredFeatureId, + }); + if (!state.highlight) { + this._map.setFeatureState({ source: 'wfs-clustered-points', - filter: ['==', '_featureId', ''], - layout: { - 'icon-image': 'marker-hover', - 'icon-size': 0.5, - 'icon-anchor': 'bottom', - 'icon-allow-overlap': true, - visibility: 'none', - }, + sourceLayer: this.uriWFS.name, + id: this.hoveredFeatureId, }, - ); - }); + // tslint:disable-next-line:align + { hover: true }); + } + } }); - }); - if (!this.eventPopupAdded) { - // 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'); - }).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.on('mouseleave', 'unclustered-point', (e) => { + if (this.hoveredFeatureId) { + this._map.getCanvas().style.cursor = ''; + this._map.setFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: this.uriWFS.name, + id: this.hoveredFeatureId, + // tslint:disable-next-line:align + }, { hover: false }); + } + this.hoveredFeatureId = null; }); this._map.on('mouseenter', 'polygon-features', () => { @@ -397,27 +450,54 @@ export class MapService { this.closePanel(); }); - this.addClickEventOnLayer('unclustered-point', 'unclustered-point-highlighted'); - this.addClickEventOnLayer('polygon-features', 'polygon-features-highlight'); - this.addClickEventOnLayer('line-features', 'line-features-highlight'); + this.addClickEventOnLayer('unclustered-point', this.uriWFS.name); + // this.addClickEventOnLayer('polygon-features', 'polygon-features-highlight'); + // if (this._map.getLayer('line-features-highlight')) { + // this.addClickEventOnLayer('line-features', 'line-features-highlight'); + // } this.eventPopupAdded = true; } } - addClickEventOnLayer(layer, highlightedLayer) { + addClickEventOnLayer(layer, layerName) { this._map.on('click', layer, (e) => { - this.selectedFeature = e.features[0].properties._featureId; - this._map.setFilter(highlightedLayer, ['==', ['get', '_featureId'], this.selectedFeature]); - this._map.setLayoutProperty(highlightedLayer, 'visibility', 'visible'); + this.selectedFeature = e.features[0].id; + + // Reset the hover and highglithed state for the current and previous feature + this._map.setFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: layerName, + id: this.hoveredFeatureId, + }, + // tslint:disable-next-line:align + { hover: false }); + this._map.setFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: layerName, + id: this.highlightedFeatureId, + }, + // tslint:disable-next-line:align + { highlight: false }); + + // Set highlited style for the current feature + this.highlightedFeatureId = this.selectedFeature; + this._map.setFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: layerName, + id: this.highlightedFeatureId, + }, + // tslint:disable-next-line:align + { highlight: true }); + const feature = e.features[0]; this.handleMapPosition(e.point.x, e.lngLat, () => { - const feature = this.geojson.features.find(f => f.properties._featureId === this.selectedFeature); + const featureCloned = cloneDeep(feature); // Remove the generated id from the properties to be displayed - delete featureCloned.properties._featureId; + delete featureCloned.id; this._panelState.next({ state: true, properties: featureCloned.properties }); }); }); @@ -631,16 +711,15 @@ export class MapService { } closePanel() { + // Remove the highlighted style for the feature + this._map.setFeatureState({ + source: 'wfs-clustered-points', + sourceLayer: this.uriWFS.name, + id: this.highlightedFeatureId, + }, + // tslint:disable-next-line:align + { highlight: false }); this.selectedFeature = null; - if (this._map.getLayer('polygon-features-highlight')) { - this._map.setLayoutProperty('polygon-features-highlight', 'visibility', 'none'); - } - if (this._map.getLayer('line-features-highlight')) { - this._map.setLayoutProperty('line-features-highlight', 'visibility', 'none'); - } - if (this._map.getLayer('unclustered-point-highlighted')) { - this._map.setLayoutProperty('unclustered-point-highlighted', 'visibility', 'none'); - } this._panelState.next({ state: false }); } diff --git a/src/i18n/traductions.fr.ts b/src/i18n/traductions.fr.ts index 0e2a004003e21c62b14148422a4ecc3706fe904e..a9491b3adfb659666a4d5db46e831cd2c0192aad 100644 --- a/src/i18n/traductions.fr.ts +++ b/src/i18n/traductions.fr.ts @@ -22,7 +22,7 @@ export const notificationMessages = { getDatasetById: 'Impossible de charger le dataset', getAutocomplete: 'Impossible de récupérer le resultat de l\'autocompletion', getSuggestion: 'Impossible de récupérer le resultat de la suggestion', - mapError: 'Une erreur c\'est produite avec la carte', + mapError: 'Une erreur s\'est produite avec la carte', getDatasetChildren: 'Impossible de récupérer les enfants du jeu de données', getDatasetParentInfo: 'Impossible de récupérer les informations du jeu de données parent', getDatasetData: 'Impossible de récupérer la donnée du jeu de données',