diff --git a/packages/visualizations/src/components/MapPoi/Map.ts b/packages/visualizations/src/components/MapPoi/Map.ts index c0edd53bc..81984e977 100644 --- a/packages/visualizations/src/components/MapPoi/Map.ts +++ b/packages/visualizations/src/components/MapPoi/Map.ts @@ -8,6 +8,8 @@ import maplibregl, { MapMouseEvent, MapOptions, StyleSpecification, + CircleLayerSpecification, + SymbolLayerSpecification, } from 'maplibre-gl'; import { @@ -36,7 +38,26 @@ const CURSOR = { DRAG: 'move', }; -const POPUP_FEATURE_STATE_KEY = 'popup-feature'; +const ACTIVE_FEATURE_RATIO_SIZE = 1.3; + +/** Sorts features in a layer by setting a sort key for a specific feature. */ +const sortLayerFeatures = ( + map: maplibregl.Map, + layer: MapGeoJSONFeature['layer'], + feature: MapGeoJSONFeature +) => { + map.setLayoutProperty(layer.id, `${layer.type}-sort-key`, [ + 'case', + ['==', ['id'], feature.id], + 1, + 0, + ]); +}; + +/** Restores the original sorting order of features in a layer */ +const unsortLayerFeatures = (map: maplibregl.Map, layer: MapGeoJSONFeature['layer']) => { + map.setLayoutProperty(layer.id, `${layer.type}-sort-key`, 0); +}; type MapFunction = (map: maplibregl.Map) => unknown; @@ -88,11 +109,66 @@ export default class MapPOI { this.queuedFunctions = []; } - private updateFeatureState(feature: ActiveFeatureType, stateKey: string, stateValue: unknown) { + /** Make active feature bigger and sort it on top of other features in the layer */ + private highlightFeature(feature: ActiveFeatureType) { + if (!feature) return; + const { layer } = feature; + this.queue((map) => { + sortLayerFeatures(map, layer, feature); + switch (layer.type) { + case 'symbol': + // eslint-disable-next-line no-case-declarations + const iconSize = ((layer as SymbolLayerSpecification).layout?.['icon-size'] || + 1) as number; + map.setLayoutProperty(layer.id, 'icon-size', [ + 'case', + ['==', ['id'], feature.id], + iconSize * ACTIVE_FEATURE_RATIO_SIZE, + iconSize, + ]); + break; + case 'circle': + // eslint-disable-next-line no-case-declarations + const circleRadius = (layer as CircleLayerSpecification).paint?.[ + 'circle-radius' + ] as number; + map.setPaintProperty(layer.id, 'circle-radius', [ + 'case', + ['==', ['id'], feature.id], + circleRadius * ACTIVE_FEATURE_RATIO_SIZE, + circleRadius, + ]); + break; + default: + break; + } + }); + } + + /** Reset active feature highlight state */ + private unhighlightFeature(feature: ActiveFeatureType) { if (!feature) return; - const { id, source, sourceLayer } = feature; + const { layer } = feature; this.queue((map) => { - map.setFeatureState({ id, source, sourceLayer }, { [stateKey]: stateValue }); + unsortLayerFeatures(map, layer); + switch (layer.type) { + case 'symbol': + map.setLayoutProperty( + layer.id, + 'icon-size', + (layer as SymbolLayerSpecification).layout?.['icon-size'] || 1 + ); + break; + case 'circle': + map.setPaintProperty( + layer.id, + 'circle-radius', + (layer as CircleLayerSpecification).paint?.['circle-radius'] + ); + break; + default: + break; + } }); } @@ -172,7 +248,7 @@ export default class MapPOI { } private navigateToFeature(direction: number) { - this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); + this.unhighlightFeature(this.activeFeature); const activeFeatureIndex = this.availableFeaturesOnClick.indexOf(this.activeFeature); this.activeFeature = this.availableFeaturesOnClick[activeFeatureIndex + direction]; this.updatePopupContent(); @@ -180,7 +256,7 @@ export default class MapPOI { if (this.activeFeature?.geometry.type === 'Point') { this.popup.setLngLat(this.activeFeature?.geometry.coordinates as LngLatLike); } - this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, true); + this.highlightFeature(this.activeFeature); } private renderFeaturesNavigationControls() { @@ -307,7 +383,7 @@ export default class MapPOI { }); // Removing feature state for the obsolete active feature. - this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); + this.unhighlightFeature(this.activeFeature); const hasFeatures = !!features.length; // Current rule: use the first feature to build the popup. // TO DO: Create a menu to display a list of feature to choose from. @@ -340,7 +416,7 @@ export default class MapPOI { this.updatePopupContent(); this.updatePopupDisplay(); - this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, true); + this.highlightFeature(this.activeFeature); } initialize( @@ -520,7 +596,7 @@ export default class MapPOI { constructor() { this.popup.on('close', () => { - this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); + this.unhighlightFeature(this.activeFeature); this.onPopupDisplayUpdate(this.activePopupDisplay, null); this.activePopupDisplay = null; this.activeFeature = null; diff --git a/packages/visualizations/src/components/MapPoi/utils.ts b/packages/visualizations/src/components/MapPoi/utils.ts index 191331877..78b116964 100644 --- a/packages/visualizations/src/components/MapPoi/utils.ts +++ b/packages/visualizations/src/components/MapPoi/utils.ts @@ -89,12 +89,7 @@ const getMapCircleLayer = (layer: CircleLayer): CircleLayerSpecification => { ...getBaseMapLayerConfiguration(layer), type, paint: { - 'circle-radius': [ - 'case', - ['boolean', ['feature-state', 'popup-feature'], false], - circleRadius * 1.3, - circleRadius, - ], + 'circle-radius': circleRadius, ...(circleBorderColor && { 'circle-stroke-width': circleStrokeWidth }), 'circle-color': circleColor, ...(circleBorderColor && { 'circle-stroke-color': circleBorderColor }), @@ -124,6 +119,7 @@ const getMapSymbolLayer = (layer: SymbolLayer): SymbolLayerSpecification => { ...getBaseMapLayerConfiguration(layer), type, layout: { + 'icon-size': 1, 'icon-allow-overlap': true, 'icon-image': iconImage, },