diff --git a/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx b/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx index b6dab4ed..4317074a 100644 --- a/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx +++ b/packages/visualizations-react/stories/Poi/PoiMap.stories.tsx @@ -57,8 +57,20 @@ const layers = [layer1, layer2]; const citiesColorMatch = { key: 'key', - colors: { Paris: 'blue', Nantes: 'yellow', Bordeaux: 'purple', Marseille: 'lightblue' }, - borderColors: { Paris: 'white', Nantes: 'black', Bordeaux: 'white', Marseille: 'black' }, + colors: { + Paris: 'blue', + 'Paris--duplicate': 'lightblue', + Nantes: 'yellow', + Bordeaux: 'purple', + Marseille: 'lightblue', + }, + borderColors: { + Paris: 'white', + 'Paris--duplicate': 'white', + Nantes: 'black', + Bordeaux: 'white', + Marseille: 'black', + }, }; const battleImageMatch = { diff --git a/packages/visualizations-react/stories/Poi/sources.ts b/packages/visualizations-react/stories/Poi/sources.ts index 265c0191..6bf5818e 100644 --- a/packages/visualizations-react/stories/Poi/sources.ts +++ b/packages/visualizations-react/stories/Poi/sources.ts @@ -14,13 +14,25 @@ const sources : PoiMapData["sources"] = { type: 'Point', coordinates: [2.357573,48.837904], }, + properties: { + key: 'Paris--duplicate', + description: 'Same location as Paris' + }, + }, + { + id: 2, + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [2.357573,48.837904], + }, properties: { key: 'Paris', description: 'Officia deserunt commodo enim ea ad veniam enim consectetur aliquip adipisicing duis. Exercitation aute velit pariatur est et ea qui veniam ad duis quis ad aliquip. Ipsum exercitation dolor tempor deserunt sunt amet laborum tempor excepteur est sunt ea quis.' }, }, { - id: 2, + id: 3, type: 'Feature', geometry: { type: 'Point', @@ -32,7 +44,7 @@ const sources : PoiMapData["sources"] = { }, }, { - id: 3, + id: 4, type: 'Feature', geometry: { type: 'Point', @@ -44,7 +56,7 @@ const sources : PoiMapData["sources"] = { }, }, { - id: 4, + id: 5, type: 'Feature', geometry: { type: 'Point', diff --git a/packages/visualizations/src/components/MapPoi/Map.ts b/packages/visualizations/src/components/MapPoi/Map.ts index 598d3cec..c0edd53b 100644 --- a/packages/visualizations/src/components/MapPoi/Map.ts +++ b/packages/visualizations/src/components/MapPoi/Map.ts @@ -11,10 +11,14 @@ import maplibregl, { } from 'maplibre-gl'; import { - POPUP_CLASSNAME, + POPUP_CONTENT, + POPUP_LOADING_CONTENT, POPUP_DISPLAY_CLASSNAME_MODIFIER, POPUP_NAVIGATION_CONTROLS_CLASSNAME, - POPUP_NAVIGATION_ARROW_CLASSNAME, + POPUP_NAVIGATION_ARROW_BUTTON_CLASSNAME, + POPUP_NAVIGATION_ARROW_BUTTON_ICON_CLASSNAME, + POPUP_NAVIGATION_CLOSE_BUTTON_CLASSNAME, + POPUP_NAVIGATION_CLOSE_BUTTON_ICON_CLASSNAME, POPUP_OPTIONS, POPUP_WIDTH, } from './constants'; @@ -38,16 +42,6 @@ type MapFunction = (map: maplibregl.Map) => unknown; type ActiveFeatureType = MapGeoJSONFeature | null; -function updateFeatureState( - map: maplibregl.Map, - feature: ActiveFeatureType, - stateKey: string, - stateValue: unknown -) { - if (!feature) return; - const { id, source, sourceLayer } = feature; - map.setFeatureState({ id, source, sourceLayer }, { [stateKey]: stateValue }); -} export default class MapPOI { /** The Map object representing the maplibregl.Map instance. */ private map: maplibregl.Map | null = null; @@ -94,6 +88,14 @@ export default class MapPOI { this.queuedFunctions = []; } + private updateFeatureState(feature: ActiveFeatureType, stateKey: string, stateValue: unknown) { + if (!feature) return; + const { id, source, sourceLayer } = feature; + this.queue((map) => { + map.setFeatureState({ id, source, sourceLayer }, { [stateKey]: stateValue }); + }); + } + /** Initialize a resize observer to always fit the map to its container */ private initializeMapResizer(map: maplibregl.Map, container: HTMLElement) { // Set a resizeObserver to resize map on container size changes @@ -170,33 +172,50 @@ export default class MapPOI { } private navigateToFeature(direction: number) { + this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); const activeFeatureIndex = this.availableFeaturesOnClick.indexOf(this.activeFeature); this.activeFeature = this.availableFeaturesOnClick[activeFeatureIndex + direction]; this.updatePopupContent(); + this.updatePopupDisplay(); + if (this.activeFeature?.geometry.type === 'Point') { + this.popup.setLngLat(this.activeFeature?.geometry.coordinates as LngLatLike); + } + this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, true); } private renderFeaturesNavigationControls() { const popupNavigationDiv = document.createElement('div'); const availableFeaturesTotal = this.availableFeaturesOnClick.length; - const activeFeatureHumanIndex = - this.availableFeaturesOnClick.indexOf(this.activeFeature) + 1; + let arrows = ''; + if (availableFeaturesTotal > 1) { + const activeFeatureHumanIndex = + this.availableFeaturesOnClick.indexOf(this.activeFeature) + 1; + arrows = ` +
${activeFeatureHumanIndex} / ${availableFeaturesTotal}
+ `; + } + popupNavigationDiv.innerHTML = ` -
- -
${activeFeatureHumanIndex} / ${availableFeaturesTotal}
- -
- `; +
+ ${arrows} + +
+ `; const prevButton = popupNavigationDiv.querySelector('#prevButton'); prevButton?.addEventListener('click', () => this.navigateToFeature(-1)); const nextButton = popupNavigationDiv.querySelector('#nextButton'); nextButton?.addEventListener('click', () => this.navigateToFeature(1)); + + const closeButton = popupNavigationDiv.querySelector( + `.${POPUP_NAVIGATION_CLOSE_BUTTON_CLASSNAME}` + ); + closeButton?.addEventListener('click', () => this.popup.remove()); return popupNavigationDiv; } @@ -212,20 +231,17 @@ export default class MapPOI { if (!popupLayerConfiguration) return; const { getLoadingContent, getContent } = popupLayerConfiguration; - this.popup.addClassName(`${POPUP_CLASSNAME}--loading`); - this.popup.setHTML(getLoadingContent()); + this.popup.setHTML(`
${getLoadingContent()}
`); getContent(id, properties).then((content) => { const popupContainerDiv = document.createElement('div'); - if (this.availableFeaturesOnClick.length > 1) { - popupContainerDiv.appendChild(this.renderFeaturesNavigationControls()); - } + const controlsDiv = this.renderFeaturesNavigationControls(); + const popupContentDiv = document.createElement('div'); - popupContentDiv.innerHTML = ``; - popupContainerDiv.appendChild(popupContentDiv); + popupContentDiv.innerHTML = `
${content}
`; + popupContainerDiv.append(controlsDiv, popupContentDiv); this.popup.setDOMContent(popupContainerDiv); - this.popup.removeClassName(`${POPUP_CLASSNAME}--loading`); }); } @@ -291,7 +307,7 @@ export default class MapPOI { }); // Removing feature state for the obsolete active feature. - updateFeatureState(map, this.activeFeature, POPUP_FEATURE_STATE_KEY, false); + this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); 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. @@ -324,7 +340,7 @@ export default class MapPOI { this.updatePopupContent(); this.updatePopupDisplay(); - updateFeatureState(map, this.activeFeature, POPUP_FEATURE_STATE_KEY, true); + this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, true); } initialize( @@ -430,7 +446,7 @@ export default class MapPOI { * to reflect the new configuration. * @param config Popups configuration */ - setpopupConfigurationByLayers(config: PopupConfigurationByLayers) { + setPopupConfigurationByLayers(config: PopupConfigurationByLayers) { this.popupConfigurationByLayers = config; this.updatePopupContent(); this.updatePopupDisplay(); @@ -504,9 +520,7 @@ export default class MapPOI { constructor() { this.popup.on('close', () => { - this.queue((map) => { - updateFeatureState(map, this.activeFeature, POPUP_FEATURE_STATE_KEY, false); - }); + this.updateFeatureState(this.activeFeature, POPUP_FEATURE_STATE_KEY, false); this.onPopupDisplayUpdate(this.activePopupDisplay, null); this.activePopupDisplay = null; this.activeFeature = null; diff --git a/packages/visualizations/src/components/MapPoi/MapRender.svelte b/packages/visualizations/src/components/MapPoi/MapRender.svelte index ba85fec8..b6e0e9a8 100644 --- a/packages/visualizations/src/components/MapPoi/MapRender.svelte +++ b/packages/visualizations/src/components/MapPoi/MapRender.svelte @@ -59,7 +59,7 @@ $: map.setMinZoom(minZoom); $: map.setMaxZoom(maxZoom); $: map.setSourcesAndLayers(sources, layers); - $: map.setpopupConfigurationByLayers(popupConfigurationByLayers); + $: map.setPopupConfigurationByLayers(popupConfigurationByLayers); $: map.jumpTo(getCenterZoomOptions({ zoom, center })); $: map.loadImages(images); $: cssVarStyles = `--aspect-ratio:${aspectRatio};`; @@ -177,7 +177,7 @@ display: flex; flex-direction: column; flex-wrap: nowrap; - padding: 13px; + padding: 0px; border-radius: 6px; max-height: 100%; overflow-y: auto; @@ -185,6 +185,10 @@ box-sizing: border-box; box-shadow: 0 6px 13px 0 rgba(0, 0, 0, 0.26); } + .map-card :global(.poi-map__popup .poi-map__popup-content), + .map-card :global(.poi-map__popup .poi-map__popup-content--loading) { + margin: 13px; + } /* Add a more opacity and blur effect on map when cooperative gesture is shown */ .map-card :global(.maplibregl-cooperative-gesture-screen) { background: rgba(0, 0, 0, 0.6); @@ -192,45 +196,96 @@ } /* --- POPUP CLOSE BUTTON --- */ - .map-card :global(.maplibregl-popup-close-button) { - font-size: 16px; - padding: 0; - width: 24px; - height: 24px; - margin-bottom: 13px; - position: relative; - order: -1; - flex-shrink: 0; - left: calc(100% - 26px); - } - .map-card :global(.maplibregl-popup-close-button:hover) { - background-color: transparent; - } - /* Hide close button when content is loading or when its display is as a tooltip */ - .map-card :global(.poi-map__popup--loading .maplibregl-popup-close-button), - .map-card :global(.poi-map__popup--as-tooltip .maplibregl-popup-close-button) { + /* Hide close button when its display is as a tooltip */ + .map-card :global(.poi-map__popup--as-tooltip .poi-map__popup-navigation-close-button) { display: none; } /* --- POPUP NAVIGATION CONTROLS --- */ .map-card :global(.poi-map__popup-navigation-controls) { + position: relative; display: flex; + gap: 6px; justify-content: center; align-items: center; - margin-bottom: 12px; - font-weight: 400; + margin: 6px 6px 0px 6px; } - .map-card :global(.poi-map__popup-navigation-arrow) { - margin: 6px; - cursor: pointer; - border: none; + .map-card :global(.poi-map__popup-navigation-arrow-button) { + display: flex; + align-items: center; + justify-content: center; + padding: 0px; + width: 36px; + height: 36px; background: none; + border: none; + cursor: pointer; } - .map-card :global(.poi-map__popup-navigation-arrow:disabled) { + .map-card :global(.poi-map__popup-navigation-arrow-button:disabled) { cursor: not-allowed; + } + .map-card :global(.poi-map__popup-navigation-arrow-button-icon) { + width: 6px; + height: 6px; + border-top: 2px solid; + border-left: 2px solid; + } + .map-card + :global(#prevButton.poi-map__popup-navigation-arrow-button + .poi-map__popup-navigation-arrow-button-icon) { + transform: rotate(-45deg); + } + .map-card + :global(#nextButton.poi-map__popup-navigation-arrow-button + .poi-map__popup-navigation-arrow-button-icon) { + transform: rotate(135deg); + } + + .map-card + :global(.poi-map__popup-navigation-arrow-button:disabled + .poi-map__popup-navigation-arrow-button-icon) { opacity: 0.5; } + .map-card :global(.poi-map__popup-navigation-close-button) { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + padding: 0px; + border: none; + background: none; + cursor: pointer; + position: absolute; + top: 0; + bottom: 0; + right: 0; + } + .map-card :global(.poi-map__popup-navigation-close-button-icon) { + position: relative; + display: block; + width: 14px; + height: 14px; + } + + .map-card :global(.poi-map__popup-navigation-close-button-icon:before), + .map-card :global(.poi-map__popup-navigation-close-button-icon:after) { + position: absolute; + left: 6px; + content: ''; + height: 100%; + width: 2px; + border-radius: 2px; + background-color: #333; + } + .map-card :global(.poi-map__popup-navigation-close-button-icon:before) { + transform: rotate(45deg); + } + .map-card :global(.poi-map__popup-navigation-close-button-icon:after) { + transform: rotate(-45deg); + } + /* --- CONTROLS --- */ .map-card :global(.maplibregl-ctrl.maplibregl-ctrl-group) { margin-top: 13px; diff --git a/packages/visualizations/src/components/MapPoi/constants.ts b/packages/visualizations/src/components/MapPoi/constants.ts index b9cefbd7..773eaaaf 100644 --- a/packages/visualizations/src/components/MapPoi/constants.ts +++ b/packages/visualizations/src/components/MapPoi/constants.ts @@ -15,12 +15,17 @@ export const DEFAULT_DARK_GREY: Color = '#515457'; export const POPUP_WIDTH = 300; -// Update styles in ./MapRender.svelte if this classname changes -export const POPUP_CLASSNAME = 'poi-map__popup'; -// Update styles in ./MapRender.svelte if this classname changes +// Update styles in ./MapRender.svelte if one of these classnames must change. +const POPUP_CLASSNAME = 'poi-map__popup'; +export const POPUP_CONTENT = 'poi-map__popup-content'; +export const POPUP_LOADING_CONTENT = 'poi-map__popup-content--loading'; export const POPUP_NAVIGATION_CONTROLS_CLASSNAME = 'poi-map__popup-navigation-controls'; -// Update styles in ./MapRender.svelte if this classname changes -export const POPUP_NAVIGATION_ARROW_CLASSNAME = 'poi-map__popup-navigation-arrow'; +export const POPUP_NAVIGATION_ARROW_BUTTON_CLASSNAME = 'poi-map__popup-navigation-arrow-button'; +export const POPUP_NAVIGATION_ARROW_BUTTON_ICON_CLASSNAME = + 'poi-map__popup-navigation-arrow-button-icon'; +export const POPUP_NAVIGATION_CLOSE_BUTTON_CLASSNAME = 'poi-map__popup-navigation-close-button'; +export const POPUP_NAVIGATION_CLOSE_BUTTON_ICON_CLASSNAME = + 'poi-map__popup-navigation-close-button-icon'; export const POPUP_DISPLAY_CLASSNAME_MODIFIER: Record = { [POPUP_DISPLAY.tooltip]: `${POPUP_CLASSNAME}--as-tooltip`, @@ -30,6 +35,6 @@ export const POPUP_DISPLAY_CLASSNAME_MODIFIER: Record export const POPUP_OPTIONS: PopupOptions = { className: POPUP_CLASSNAME, - closeButton: true, + closeButton: false, closeOnClick: false, };