From a9eb851df0a81383a12db829d3b745fdb35e628d Mon Sep 17 00:00:00 2001 From: Kevin Fabre Date: Fri, 28 Jul 2023 18:04:00 +0200 Subject: [PATCH] feat(map): handle multiple sources and layers --- .../src/components/PoiGeoJson.tsx | 11 +- .../stories/Poi/PoiGeoJson.stories.tsx | 79 +++++--- .../visualizations-react/stories/Poi/data.ts | 15 +- .../src/components/MapPoi/Map.ts | 118 +++++++++++ .../src/components/MapPoi/MapRender.svelte | 189 ++---------------- .../src/components/MapPoi/PoiGeoJson.svelte | 67 ++----- .../src/components/MapPoi/constants.ts | 29 +-- .../src/components/MapPoi/index.ts | 5 +- .../src/components/MapPoi/types.ts | 61 ++++-- .../src/components/MapPoi/utils.ts | 79 +++++--- 10 files changed, 312 insertions(+), 341 deletions(-) create mode 100644 packages/visualizations/src/components/MapPoi/Map.ts diff --git a/packages/visualizations-react/src/components/PoiGeoJson.tsx b/packages/visualizations-react/src/components/PoiGeoJson.tsx index bf7ac7e7..07c527b3 100644 --- a/packages/visualizations-react/src/components/PoiGeoJson.tsx +++ b/packages/visualizations-react/src/components/PoiGeoJson.tsx @@ -1,9 +1,12 @@ -import { PoiGeoJson as _PoiGeoJson, PoiMapOptions } from '@opendatasoft/visualizations'; -import { FeatureCollection } from 'geojson'; +import { + PoiGeoJson as _PoiGeoJson, + PoiMapOptions, + PoiMapData, + Async, +} from '@opendatasoft/visualizations'; import { FC } from 'react'; -import { Props } from './Props'; import wrap from './ReactImpl'; // Explicit name and type are needed for Storybook -const PoiGeoJson: FC> = wrap(_PoiGeoJson); +const PoiGeoJson: FC<{ data: Async; options: PoiMapOptions }> = wrap(_PoiGeoJson); export default PoiGeoJson; diff --git a/packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx b/packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx index 5f309b43..0d307ea0 100644 --- a/packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx +++ b/packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx @@ -1,11 +1,25 @@ import React from 'react'; +import { BBox } from 'geojson'; +import { PoiMapData } from '@opendatasoft/visualizations'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { PoiMapOptions } from '@opendatasoft/visualizations'; -import { FeatureCollection } from 'geojson'; -import { shapes } from './data'; -import { PoiGeoJson, Props } from '../../src'; -const DEMO_BASEMAP = 'https://demotiles.maplibre.org/style.json'; +import { PoiGeoJson } from '../../src'; +import { shapes as data } from './data'; + +const BASE_STYLE = 'https://demotiles.maplibre.org/style.json'; + +const layers : PoiMapData["layers"] = [{ + id: 'data-layer-001', + source: "data", + type: "circle", + color: '#B42222', + colorMatch: { + key: 'key', + colors: {Paris: 'blue', Nantes: 'yellow', Bordeaux: 'purple', Corsica: 'white', Marseille : 'lightblue' } + } +}]; + +const bbox : BBox = [ -6.855469,41.343825,11.645508,51.371780]; const meta: ComponentMeta = { title: 'Poi/GeoJson', @@ -14,9 +28,7 @@ const meta: ComponentMeta = { export default meta; -const Template: ComponentStory = ( - args: Props -) => ( +const Template: ComponentStory = args => (
= (
); -export const PoiMapNoLayersParams = Template.bind({}); -const PoiMapNoLayersParamsArgs: Props = { - data: { value: shapes }, - options: { - style: DEMO_BASEMAP, - }, +/** + * STORY: No layer params + */ +export const PoiMapNoLayersParams : ComponentStory = Template.bind({}); +const PoiMapNoLayersParamsArgs = { + data: {}, + options: {style: BASE_STYLE, bbox} }; PoiMapNoLayersParams.args = PoiMapNoLayersParamsArgs; -const layerParams = { - colors: ['#B42222', 'Green'], - matchValues: ['Paris', 'Nantes'], - matchKey: 'key', -}; - -export const PoiMapNonInteractive = Template.bind({}); - -const PoiMapNonInteractiveArgs: Props = { - data: { value: shapes }, +/** + * STORY: No interactive + */ +export const PoiMapNonInteractive : ComponentStory = Template.bind({}); +const PoiMapNonInteractiveArgs = { + data: {value:{ layers, sources: { [layers[0].source] : {type: "geojson" as const, data}}}}, options: { - layerParams, - style: DEMO_BASEMAP, + style: BASE_STYLE, + layers, interactive: false, }, }; PoiMapNonInteractive.args = PoiMapNonInteractiveArgs; -export const PoiMapMatchExpression = Template.bind({}); - -const PoiMapMatchExpressionArgs: Props = { - data: { value: shapes }, - options: { - layerParams, - style: DEMO_BASEMAP, +/** + * STORY: With match expression + */ +export const PoiMapMatchExpression : ComponentStory = Template.bind({}); +const PoiMapMatchExpressionArgs = { + data: { + value: { + layers, + sources: {[layers[0].source] : {type: "geojson" as const, data}} + } }, + options: {style: BASE_STYLE, bbox }, }; PoiMapMatchExpression.args = PoiMapMatchExpressionArgs; diff --git a/packages/visualizations-react/stories/Poi/data.ts b/packages/visualizations-react/stories/Poi/data.ts index 424cadb1..0445bb62 100644 --- a/packages/visualizations-react/stories/Poi/data.ts +++ b/packages/visualizations-react/stories/Poi/data.ts @@ -518,8 +518,7 @@ export const shapes: FeatureCollection = { type: 'Point', }, properties: { - key: 'Corsica', - cat: 'Red', + key: 'Corsica' }, }, { @@ -529,8 +528,7 @@ export const shapes: FeatureCollection = { coordinates: [2.357573,48.837904], }, properties: { - key: 'Paris', - cat: 'Red', + key: 'Paris' }, }, { @@ -540,8 +538,7 @@ export const shapes: FeatureCollection = { coordinates: [-0.563328,44.838245], }, properties: { - key: 'Bordeaux', - cat: 'Blue', + key: 'Bordeaux' }, }, { @@ -551,8 +548,7 @@ export const shapes: FeatureCollection = { coordinates: [-1.552924,47.214847], }, properties: { - key: 'Nantes', - cat: 'Blue', + key: 'Nantes' }, }, { @@ -562,8 +558,7 @@ export const shapes: FeatureCollection = { coordinates: [5.360529,43.303114], }, properties: { - key: 'Marseille', - cat: 'Red', + key: 'Marseille' }, }, ], diff --git a/packages/visualizations/src/components/MapPoi/Map.ts b/packages/visualizations/src/components/MapPoi/Map.ts new file mode 100644 index 00000000..45d7e5c1 --- /dev/null +++ b/packages/visualizations/src/components/MapPoi/Map.ts @@ -0,0 +1,118 @@ +import type { BBox } from 'geojson'; +import maplibregl, { + LngLatBoundsLike, + LngLatLike, + MapOptions, + StyleSpecification, +} from 'maplibre-gl'; + +type MapFunction = () => unknown; + +const DEFAULT_CENTER: LngLatLike = [3.5, 46]; + +export default class MapPOI { + map: maplibregl.Map | null = null; + + isReady = false; + + baseStyle: StyleSpecification | null = null; + + queuedFunctions: Array = []; + + navigation = new maplibregl.NavigationControl({ showCompass: false }); + + initialize( + style: MapOptions['style'], + container: HTMLElement, + options: Omit + ) { + 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) { + this.baseStyle = this.map?.getStyle(); + } + this.enqueue(); + }); + } + + queue(fn: MapFunction) { + if (this.isReady) fn(); + else this.queuedFunctions.push(fn); + } + + enqueue() { + this.queuedFunctions.forEach((fn) => fn()); + this.queuedFunctions = []; + } + + remove() { + this.map?.remove(); + } + + // TODO: add tests to check that layers are at the end of the array + /* + * TODO: When updating Maplibre to a 3.2.2 version or up + * - Update this code to use the option transformStyle. + * https://maplibre.org/maplibre-gl-js/docs/API/types/maplibregl.TransformStyleFunction/ + * - `baseStyle` could be removed + * - The key block could also be removed in MapRender.svelte + */ + setSourcesAndLayers( + sources: StyleSpecification['sources'], + layers: StyleSpecification['layers'] + ) { + this.queue(() => { + if (this.baseStyle) { + this.map?.setStyle({ + ...this.baseStyle, + sources: { + ...sources, + ...this.baseStyle.sources, + }, + layers: [...this.baseStyle.layers, ...layers], + }); + } + }); + } + + setBbox(bbox?: BBox) { + this.queue(() => { + if (!bbox) { + // zoom-out to bounds defined in the initialization + this.map?.setZoom(this.map?.getMinZoom()); + return; + } + + // Cancel any saved max bounds to properly fitBounds + this.map?.setMaxBounds(null); + // Using padding, keep enough room for controls (zoom) to make sure they don't hide anything + this.map?.fitBounds(bbox as LngLatBoundsLike, { + animate: false, + padding: 40, + }); + }); + } + + toggleInteractivity(interaction: 'enable' | 'disable') { + this.queue(() => { + this.map?.boxZoom[interaction](); + this.map?.doubleClickZoom[interaction](); + this.map?.dragPan[interaction](); + this.map?.dragRotate[interaction](); + this.map?.keyboard[interaction](); + this.map?.scrollZoom[interaction](); + this.map?.touchZoomRotate[interaction](); + + const hasNavigation = this.map?.hasControl(this.navigation); + + if (interaction === 'disable' && hasNavigation) { + this.map?.removeControl(this.navigation); + } + if (!hasNavigation && interaction === 'enable') { + this.map?.addControl(this.navigation, 'top-right'); + } + }); + } +} diff --git a/packages/visualizations/src/components/MapPoi/MapRender.svelte b/packages/visualizations/src/components/MapPoi/MapRender.svelte index c88bfcb6..243af702 100644 --- a/packages/visualizations/src/components/MapPoi/MapRender.svelte +++ b/packages/visualizations/src/components/MapPoi/MapRender.svelte @@ -1,186 +1,33 @@
diff --git a/packages/visualizations/src/components/MapPoi/PoiGeoJson.svelte b/packages/visualizations/src/components/MapPoi/PoiGeoJson.svelte index 2f0ef84e..05346d3b 100644 --- a/packages/visualizations/src/components/MapPoi/PoiGeoJson.svelte +++ b/packages/visualizations/src/components/MapPoi/PoiGeoJson.svelte @@ -1,68 +1,25 @@
- + {#key style} + + {/key}