Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinFabre-ods committed Nov 28, 2023
1 parent 6fae7f4 commit 4f9aba5
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 105 deletions.
6 changes: 3 additions & 3 deletions packages/visualizations/src/components/MapPoi/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
getMapStyle,
getMapSources,
getMapLayers,
getPopupsConfiguration,
getPopupConfigurationByLayers,
getMapOptions,
} from './utils';
import type { PoiMapData, PoiMapOptions } from './types';
Expand All @@ -21,7 +21,7 @@
$: style = getMapStyle(styleOptions);
$: sources = getMapSources(data.value?.sources);
$: layers = getMapLayers(data.value?.layers);
$: popupsConfiguration = getPopupsConfiguration(data.value?.layers);
$: popupConfigurationByLayers = getPopupConfigurationByLayers(data.value?.layers);
$: ({
bbox: _bbox,
Expand Down Expand Up @@ -53,7 +53,7 @@
{style}
{sources}
{layers}
{popupsConfiguration}
{popupConfigurationByLayers}
bbox={$bbox}
center={$center}
{zoom}
Expand Down
217 changes: 125 additions & 92 deletions packages/visualizations/src/components/MapPoi/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
POPUP_OPTIONS,
POPUP_WIDTH,
} from './constants';
import type { PopupsConfiguration, CenterZoomOptions, PopupDisplayTypes } from './types';

import type { PopupConfigurationByLayers, CenterZoomOptions, PopupDisplayTypes } from './types';
import { POPUP_DISPLAY } from './types';

const CURSOR = {
Expand All @@ -25,8 +26,23 @@ const CURSOR = {
DRAG: 'move',
};

const POPUP_FEATURE_STATE_KEY = 'popup-feature';

type MapFunction = (map: maplibregl.Map) => unknown;

type ActiveFeatureType = MapGeoJSONFeature | null;

function updateFeatureState(
map: maplibregl.Map,
feature: ActiveFeatureType,
state: string,
stateValue: unknown
) {
if (!feature) return;
const { id, source, sourceLayer } = feature;
const featureState = map.getFeatureState({ source, sourceLayer, id });
map.setFeatureState({ id, source, sourceLayer }, { ...featureState, [state]: stateValue });
}
export default class MapPOI {
/** The Map object representing the maplibregl.Map instance. */
private map: maplibregl.Map | null = null;
Expand All @@ -43,20 +59,20 @@ export default class MapPOI {
/** Array of layer IDs that are not from the base style of the map */
private layerIds: string[] = [];

/** Array of source IDs used in the map */
private sourceIds: string[] = [];

/** A navigation control for the map. */
private navigationControl = new maplibregl.NavigationControl({ showCompass: false });

/** A popup for displaying information on the map. */
private popup = new maplibregl.Popup(POPUP_OPTIONS);

/** Popups configurations. One configuration by layer */
private popupsConfiguration: PopupsConfiguration = {};
/** An object to store popup configurations for each layers */
private popupConfigurationByLayers: PopupConfigurationByLayers = {};

/** Value to represent the active display of the popup */
private activePopupDisplay: PopupDisplayTypes | null = null;

/** An active GeoJSONFeature. Its information are displayed within the popup. */
private activeFeature: MapGeoJSONFeature | null = null;
private activeFeature: ActiveFeatureType = null;

/** An array of functions to be executed when the map is ready. */
private queuedFunctions: Array<MapFunction> = [];
Expand Down Expand Up @@ -94,9 +110,7 @@ export default class MapPOI {
const features = map.queryRenderedFeatures(point, { layers: this.layerIds });
const isMovingOverFeatureWithPopup =
features.length &&
features.some((feature) =>
Object.keys(this.popupsConfiguration).includes(feature.layer.id)
);
features.some((feature) => feature.layer.id in this.popupConfigurationByLayers);
canvas.style.cursor = isMovingOverFeatureWithPopup ? CURSOR.HOVER : CURSOR.DEFAULT;
});
}
Expand Down Expand Up @@ -128,49 +142,100 @@ export default class MapPOI {
* We ask for features that are not in base style layers
*/
const features = map.queryRenderedFeatures(point, { layers: this.layerIds });
this.updatePopup(map, features);
this.onFeaturesClick(map, features);
});
}

private bindedOnClick = this.onClick.bind(this);

private resetLeftPaddingPopup() {
this.queue((map) => map.easeTo({ padding: { left: 0 } }));
this.popup.off('close', this.bindedResetLeftPaddingPopup);
/** Update popup display between tooltip, sidebar and modal mode */
private updatePopupDisplay() {
if (!this.activeFeature) return;
const {
layer: { id: layerId },
} = this.activeFeature;
const oldDisplay = this.activePopupDisplay;
const { display: newDisplay } = this.popupConfigurationByLayers[layerId] || {};
if (oldDisplay !== newDisplay) {
if (oldDisplay) {
this.popup.removeClassName(POPUP_DISPLAY_CLASSNAME_MODIFIER[oldDisplay]);
}
if (!newDisplay) {
this.popup.remove();
} else {
this.popup.addClassName(POPUP_DISPLAY_CLASSNAME_MODIFIER[newDisplay]);
this.activePopupDisplay = newDisplay;
}
}
this.onPopupDisplayUpdate(oldDisplay, newDisplay);
}

private bindedResetLeftPaddingPopup = this.resetLeftPaddingPopup.bind(this);
private updatePopupContent() {
if (!this.activeFeature) return;
const {
id,
properties,
layer: { id: layerId },
} = this.activeFeature;
const popupLayerConfiguration = this.popupConfigurationByLayers[layerId];
if (!popupLayerConfiguration) return;
const { getLoadingContent, getContent } = popupLayerConfiguration;

/** Update popup display between tooltip, sidebar and modal mode */
private updatePopupDisplay(display: PopupDisplayTypes) {
// Navigation controls could have been removed if the previous popup display was 'modal'.
this.queue((map) => map.addControl(this.navigationControl));
this.popup.addClassName(`${POPUP_CLASSNAME}--loading`);
this.popup.setHTML(getLoadingContent());

// Remove all popup display classname modifier before adding the current modifier.
Object.keys(POPUP_DISPLAY_CLASSNAME_MODIFIER).forEach((d) => {
this.popup.removeClassName(POPUP_DISPLAY_CLASSNAME_MODIFIER[d as PopupDisplayTypes]);
getContent(id, properties).then((content) => {
this.popup.setHTML(content);
this.popup.removeClassName(`${POPUP_CLASSNAME}--loading`);
});
this.popup.addClassName(POPUP_DISPLAY_CLASSNAME_MODIFIER[display]);

/*
* Remove navigation controls when popup display is 'modal'.
* When the popup closes, add navigation controls to the maps.
*/
if (display === POPUP_DISPLAY.modal) {
this.queue((map) => map.removeControl(this.navigationControl));
this.popup.once('close', () =>
this.queue((map) => map.addControl(this.navigationControl))
);
}
}

/** Set the popup content and positioning */
private updatePopup(map: maplibregl.Map, features: MapGeoJSONFeature[]) {
private onPopupDisplayUpdate(
oldDisplay: PopupDisplayTypes | null,
newDisplay: PopupDisplayTypes | null
) {
if (oldDisplay === newDisplay) return;
this.queue(map => {
if (oldDisplay === POPUP_DISPLAY.sidebar) {
map.easeTo({ padding: { left: 0 } });
}
if (
newDisplay === POPUP_DISPLAY.sidebar &&
this.activeFeature &&
this.activeFeature.geometry.type === 'Point'
) {
map.easeTo({
center: this.activeFeature.geometry.coordinates as LngLatLike,
padding: { left: POPUP_WIDTH },
});
}
if (newDisplay === POPUP_DISPLAY.modal) {
map.removeControl(this.navigationControl);
} else {
map.addControl(this.navigationControl);
}
});

}

/**
* Is triggered when a click has been made on the map.
* Is responsible for closing or opening the popup.
*
* Closing the popup happens when:
* - No features are closed to the click
* - When the feature clicked is the active feature displayed in the popup
*
* Opening the popup happens when:
* - A feature is clicked for which a popup configuration is available (popup configuration are set by layer)
*
* @param map The map instance
* @param features Features closed where the map has been clicked
*/
private onFeaturesClick(map: maplibregl.Map, features: MapGeoJSONFeature[]) {
// Removing feature state for the obsolete active feature.
if (this.activeFeature) {
const { id, source, sourceLayer } = this.activeFeature;
map.setFeatureState({ source, sourceLayer, id }, { 'popup-feature': false });
}
updateFeatureState(map, 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.
Expand All @@ -192,55 +257,17 @@ export default class MapPOI {
// eslint-disable-next-line prefer-destructuring
this.activeFeature = features[0];

const {
id: featureId,
layer: { id: layerId },
geometry,
properties,
source,
sourceLayer,
} = this.activeFeature;
const { geometry } = this.activeFeature;

if (geometry.type !== 'Point') return;

/*
* We remove the popup if:
* - no popup configuration for a layer
* - popup's source is no longer used in the map
*/
const popupConfiguration = this.popupsConfiguration[layerId];
if (!popupConfiguration || !this.sourceIds.includes(source)) {
this.popup.remove();
return;
}

const { display, getContent, getLoadingContent } = popupConfiguration;

if (!this.popup.isOpen()) {
this.popup.addTo(map);
}
this.popup.addClassName(`${POPUP_CLASSNAME}--loading`);
this.popup.setLngLat(geometry.coordinates as LngLatLike).setHTML(getLoadingContent());
this.popup.setLngLat(geometry.coordinates as LngLatLike);

getContent(featureId, properties).then((content) => {
this.popup.setHTML(content);
this.popup.removeClassName(`${POPUP_CLASSNAME}--loading`);
});
this.updatePopupDisplay(display);
map.setFeatureState({ source, sourceLayer, id: featureId }, { 'popup-feature': true });

this.popup.once('close', () => {
this.activeFeature = null;
map.setFeatureState({ source, sourceLayer, id: featureId }, { 'popup-feature': false });
});

if (display === 'sidebar') {
map.easeTo({
center: geometry.coordinates as LngLatLike,
padding: { left: POPUP_WIDTH },
});
this.popup.on('close', this.bindedResetLeftPaddingPopup);
}
this.updatePopupContent();
this.updatePopupDisplay();
updateFeatureState(map, this.activeFeature, POPUP_FEATURE_STATE_KEY, true);
}

initialize(
Expand All @@ -264,8 +291,6 @@ export default class MapPOI {
}

destroy() {
this.activeFeature = null;
this.popup.remove();
this.queue((map) => map.remove());
this.mapResizeObserver?.disconnect();
}
Expand Down Expand Up @@ -298,7 +323,6 @@ export default class MapPOI {
});
}
this.layerIds = layers.map(({ id }) => id);
this.sourceIds = Object.keys(sources);
});
}

Expand Down Expand Up @@ -340,12 +364,10 @@ export default class MapPOI {
this.queue((map) => map.jumpTo(options));
}

setPopupsConfiguration(config: PopupsConfiguration) {
this.popupsConfiguration = config;
if (!this.activeFeature) return;
const newPopupConfiguration = this.popupsConfiguration[this.activeFeature.layer.id];
if (!newPopupConfiguration) return;
this.updatePopupDisplay(newPopupConfiguration.display);
setpopupConfigurationByLayers(config: PopupConfigurationByLayers) {
this.popupConfigurationByLayers = config;
this.updatePopupContent();
this.updatePopupDisplay();
}

/**
Expand Down Expand Up @@ -412,4 +434,15 @@ export default class MapPOI {
}
});
}

constructor() {
this.popup.on('close', () => {
this.queue((map) => {
updateFeatureState(map, this.activeFeature, POPUP_FEATURE_STATE_KEY, false);
});
this.onPopupDisplayUpdate(this.activePopupDisplay, null);
this.activePopupDisplay = null;
this.activeFeature = null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import Map from './Map';
import { getCenterZoomOptions } from './utils';
import type { PopupsConfiguration, PoiMapOptions } from './types';
import type { PopupConfigurationByLayers, PoiMapOptions } from './types';
// Base style, sources and layers
export let style: MapOptions['style'];
Expand Down Expand Up @@ -45,7 +45,7 @@
// Used in front of console and error messages to debug multiple maps on a same page
const mapId = Math.floor(Math.random() * 1000);
export let popupsConfiguration: PopupsConfiguration;
export let popupConfigurationByLayers: PopupConfigurationByLayers;
let container: HTMLElement;
const map = new Map();
Expand All @@ -59,7 +59,7 @@
$: map.setMinZoom(minZoom);
$: map.setMaxZoom(maxZoom);
$: map.setSourcesAndLayers(sources, layers);
$: map.setPopupsConfiguration(popupsConfiguration);
$: map.setpopupConfigurationByLayers(popupConfigurationByLayers);
$: map.jumpTo(getCenterZoomOptions({ zoom, center }));
$: map.loadImages(images);
$: cssVarStyles = `--aspect-ratio:${aspectRatio};`;
Expand Down
4 changes: 2 additions & 2 deletions packages/visualizations/src/components/MapPoi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export type GeoPoint = {
lat: number;
lon: number;
};

export type PopupsConfiguration = { [key: string]: PopupLayer };
/** A configuration map for popups where keys are layer ids and values are PopupLayer object. */
export type PopupConfigurationByLayers = { [key: string]: PopupLayer };

export type CenterZoomOptions = { zoom?: number; center?: LngLatLike };
Loading

0 comments on commit 4f9aba5

Please sign in to comment.