Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinFabre-ods committed Sep 5, 2023
1 parent 70f6134 commit 1221894
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 24 deletions.
4 changes: 4 additions & 0 deletions packages/visualizations-react/stories/Poi/PoiMap.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const layers : PoiMapData["layers"] = [{
colorMatch: {
key: 'key',
colors: {Paris: 'blue', Nantes: 'yellow', Bordeaux: 'purple', Corsica: 'white', Marseille : 'lightblue' }
},
popup: {
display: 'tooltip',
'getContent': () => "data-layer-001 popup"
}
}];

Expand Down
5 changes: 3 additions & 2 deletions packages/visualizations/src/components/MapPoi/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { Async } from '../../types';
import { getMapStyle, getMapSources, getMapLayers, getMapOptions } from './utils';
import { getMapStyle, getMapSources, getMapLayers, getPopupsConfiguration, getMapOptions } from './utils';
import type { PoiMapData, PoiMapOptions } from './types';
export let data: Async<PoiMapData>;
Expand All @@ -12,13 +12,14 @@
$: style = getMapStyle(options.style);
$: sources = getMapSources(data.value?.sources);
$: layers = getMapLayers(data.value?.layers);
$: popupsConfiguration = getPopupsConfiguration(data.value?.layers);
$: computedOptions = getMapOptions(options);
</script>

<div>
{#key style}
<MapRender {style} {sources} {layers} {...computedOptions} />
<MapRender {style} {sources} {layers} {popupsConfiguration} {...computedOptions} />
{/key}
</div>

Expand Down
83 changes: 75 additions & 8 deletions packages/visualizations/src/components/MapPoi/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import type { BBox } from 'geojson';
import maplibregl, {
LngLatBoundsLike,
LngLatLike,
MapGeoJSONFeature,
MapLayerMouseEvent,
MapOptions,
StyleSpecification,
} from 'maplibre-gl';

import type { PopupsConfiguration } from './types';

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

const DEFAULT_CENTER: LngLatLike = [3.5, 46];
Expand All @@ -17,10 +21,18 @@ export default class MapPOI {

private baseStyle: StyleSpecification | null = null;

private queuedFunctions: Array<MapFunction> = [];

private layerIds : string[] = [];
private navigation = new maplibregl.NavigationControl({ showCompass: false });

private popup = new maplibregl.Popup({className: 'poi-map__popup'});

private popupsConfiguration : PopupsConfiguration = {};

private popupFeatures : MapGeoJSONFeature[] = [];

private queuedFunctions: Array<MapFunction> = [];

private queue(fn: MapFunction) {
if (this.isReady && this.map) fn(this.map);
else this.queuedFunctions.push(fn);
Expand All @@ -31,6 +43,51 @@ export default class MapPOI {
this.queuedFunctions = [];
}

private onClick({point}: MapLayerMouseEvent) {
this.queue(map => {
/**
* Get features closed to the click.
* We ask for features that are not in base style layers
*/
const features = map.queryRenderedFeatures(point, {layers: this.layerIds});
this.popupFeatures = features;
this.setPopup(map);
});
}

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

private setPopup(map: maplibregl.Map) {
if(!this.popupFeatures.length) return;

// Current rule: use the first feature to build the popup.
// TO DO: Create a menu to display a list of feature to choose from.
const {id, layer: {id: layerId}, geometry, properties} = this.popupFeatures[0];

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

// Get the popup configuration for a layer
const popupConfiguration = this.popupsConfiguration[layerId];

// If no popup configuration for a layer, we remove the popup
if (!popupConfiguration) {
this.popup.remove();
this.popupFeatures = [];
return;
}


const {display, getContent} = popupConfiguration;

this.popup
.setLngLat(geometry.coordinates.slice() as LngLatLike)
.setHTML(getContent(id, properties))
.addTo(map);

const classnameModifier = display === 'sidebar' ? 'addClassName' : 'removeClassName';
this.popup[classnameModifier]("poi-map__popup--as-sidebar");
}

initialize(
style: MapOptions['style'],
container: HTMLElement,
Expand All @@ -39,9 +96,10 @@ export default class MapPOI {
this.map = new maplibregl.Map({ style, container, center: DEFAULT_CENTER, ...options });
this.map.on('load', () => {
this.isReady = true;
// Store base style after the first loads
if (this.map) {
// Store base style after the first load
this.baseStyle = this.map?.getStyle();
this.map.on("click", this.bindedOnClick);
this.enqueue(this.map);
}
});
Expand Down Expand Up @@ -74,6 +132,7 @@ export default class MapPOI {
layers: [...this.baseStyle.layers, ...layers],
});
}
this.layerIds = layers.map(({id}) => id);
});
}

Expand All @@ -95,6 +154,11 @@ export default class MapPOI {
});
}

setPopupsConfiguration(config : PopupsConfiguration) {
this.popupsConfiguration = config;
this.queue((map) => this.setPopup(map));
}

toggleInteractivity(interaction: 'enable' | 'disable') {
this.queue((map) => {
map.boxZoom[interaction]();
Expand All @@ -105,13 +169,16 @@ export default class MapPOI {
map.scrollZoom[interaction]();
map.touchZoomRotate[interaction]();

const hasNavigation = map.hasControl(this.navigation);
const hasControl = map.hasControl(this.navigation);

if (interaction === 'disable' && hasNavigation) {
map.removeControl(this.navigation);
if (interaction === 'disable') {
this.popup.remove();
map.off('click', this.bindedOnClick);
if (hasControl) {map.removeControl(this.navigation);}
}
if (!hasNavigation && interaction === 'enable') {
map.addControl(this.navigation, 'top-right');
if (interaction === 'enable') {
if(!hasControl) {map.addControl(this.navigation, 'top-right');}
map.on('click', this.bindedOnClick);
}
});
}
Expand Down
16 changes: 5 additions & 11 deletions packages/visualizations/src/components/MapPoi/MapRender.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { onDestroy, onMount } from 'svelte';
import Map from './Map';
import type { PopupsConfiguration } from './types';
// Base style, sources and layers
export let style: MapOptions['style'];
Expand All @@ -16,13 +17,15 @@
export let bbox: BBox;
export let aspectRatio: number;
export let interactive: boolean;
export let popupsConfiguration: PopupsConfiguration;
let container: HTMLElement;
const map = new Map();
$: map.toggleInteractivity(interactive ? 'enable' : 'disable');
$: map.setBbox(bbox);
$: map.setSourcesAndLayers(sources, layers);
$: map.setPopupsConfiguration(popupsConfiguration);
$: cssVarStyles = `--aspect-ratio:${aspectRatio};`;
// Lifecycle
Expand Down Expand Up @@ -53,18 +56,9 @@
position: relative;
}
/* To add classes programmatically in svelte we will use a global selector. We place it inside a local selector to obtain some encapsulation and avoid side effects */
.map-card :global(.tooltip-on-hover > .maplibregl-popup-content) {
border-radius: 6px;
box-shadow: 0px 6px 13px rgba(0, 0, 0, 0.26);
padding: 13px;
.map-card :global(.poi-map__popup.poi-map__popup--as-sidebar) {
transform: none !important;
}
.map-card :global(.tooltip-on-hover .maplibregl-popup-tip) {
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
border-right-color: transparent;
}
.main {
aspect-ratio: var(--aspect-ratio);
flex-grow: 1;
Expand Down
17 changes: 15 additions & 2 deletions packages/visualizations/src/components/MapPoi/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CircleLayerSpecification, StyleSpecification } from 'maplibre-gl';
import type { BBox } from 'geojson';
import type { CircleLayerSpecification, GeoJSONFeature, StyleSpecification } from 'maplibre-gl';
import type { BBox, GeoJsonProperties } from 'geojson';
import type { Color } from '../types';

// To render data layers on the map
Expand Down Expand Up @@ -41,6 +41,7 @@ export type Layer = {
sourceLayer?: string;
type: LayerSpecification['type'];
color: Color;
popup?: PopupLayer
/**
* Set a marker color based on a value.
* If no match, default color comes from `color`
Expand All @@ -51,7 +52,19 @@ export type Layer = {
};
};

export type PopupLayer = {
/**
* Control where to display the popup
* - `sidebar`: As a side element (on the left)
* - `tooltip`: Above the feature that has been clicked
*/
display: 'sidebar' | 'tooltip';
getContent: (id: GeoJSONFeature["id"], properties?: GeoJsonProperties) => string;
};

export type GeoPoint = {
lat: number;
lon: number;
};

export type PopupsConfiguration = {[key: string] : PopupLayer};
12 changes: 11 additions & 1 deletion packages/visualizations/src/components/MapPoi/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {

import type { Color } from '../types';

import type { Layer, PoiMapData, PoiMapOptions } from './types';
import type { Layer, PoiMapData, PoiMapOptions, PopupsConfiguration } from './types';
import { DEFAULT_BASEMAP_STYLE, DEFAULT_ASPECT_RATIO, DEFAULT_BBOX } from './constants';

export const getMapStyle = (style: PoiMapOptions['style']): MapOptions['style'] => {
Expand Down Expand Up @@ -52,6 +52,16 @@ export const getMapLayers = (layers?: Layer[]): CircleLayerSpecification[] => {
});
};

export const getPopupsConfiguration = (layers?: Layer[]): PopupsConfiguration => {
const configuration : PopupsConfiguration = {};
layers?.forEach(({id, popup}) => {
if(popup) {
configuration[id] = popup;
}
});
return configuration;
};

export const getMapOptions = (options: PoiMapOptions) => {
const { aspectRatio = DEFAULT_ASPECT_RATIO, bbox = DEFAULT_BBOX, interactive = true } = options;
return {
Expand Down

0 comments on commit 1221894

Please sign in to comment.