Skip to content

Commit

Permalink
feat(map): handle multiple sources and layers
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinFabre-ods committed Sep 1, 2023
1 parent 0831ad0 commit 557767a
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 400 deletions.
9 changes: 0 additions & 9 deletions packages/visualizations-react/src/components/PoiGeoJson.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/visualizations-react/src/components/PoiMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PoiMap as _PoiMap, PoiMapOptions, PoiMapData, Async } from '@opendatasoft/visualizations';
import { FC } from 'react';
import wrap from './ReactImpl';

// Explicit name and type are needed for Storybook
const PoiMap: FC<{ data: Async<PoiMapData>; options: PoiMapOptions }> = wrap(_PoiMap);
export default PoiMap;
2 changes: 1 addition & 1 deletion packages/visualizations-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export { default as KpiCard } from './components/KpiCard';
export { default as ChoroplethGeoJson } from './components/ChoroplethGeoJson';
export { default as ChoroplethVectorTiles } from './components/ChoroplethVectorTiles';
export { default as ChoroplethSvg } from './components/ChoroplethSvg';
export { default as PoiGeoJson } from './components/PoiGeoJson';
export { default as PoiMap } from './components/PoiMap';
69 changes: 0 additions & 69 deletions packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx

This file was deleted.

82 changes: 82 additions & 0 deletions packages/visualizations-react/stories/Poi/PoiMap.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import { BBox } from 'geojson';
import { PoiMapData } from '@opendatasoft/visualizations';
import { ComponentMeta, ComponentStory } from '@storybook/react';

import { shapes as data } from './data';
import { PoiMap } from '../../src';

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<typeof PoiMap> = {
title: 'Poi/PoiMap',
component: PoiMap,
};

export default meta;

const Template: ComponentStory<typeof PoiMap> = args => (
<div
style={{
width: '50%',
minHeight: '100px',
minWidth: '100px',
margin: 'auto',
border: '1px solid black',
}}
>
<PoiMap {...args} />
</div>
);

/**
* STORY: No layer params
*/
export const PoiMapNoLayersParams : ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapNoLayersParamsArgs = {
data: {},
options: {style: BASE_STYLE, bbox}
};
PoiMapNoLayersParams.args = PoiMapNoLayersParamsArgs;

/**
* STORY: No interactive
*/
export const PoiMapNonInteractive : ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapNonInteractiveArgs = {
data: {value:{ layers, sources: { [layers[0].source] : {type: "geojson" as const, data}}}},
options: {
style: BASE_STYLE,
layers,
interactive: false,
},
};
PoiMapNonInteractive.args = PoiMapNonInteractiveArgs;

/**
* STORY: With match expression
*/
export const PoiMapMatchExpression : ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapMatchExpressionArgs = {
data: {
value: {
layers,
sources: {[layers[0].source] : {type: "geojson" as const, data}}
}
},
options: {style: BASE_STYLE, bbox },
};
PoiMapMatchExpression.args = PoiMapMatchExpressionArgs;
15 changes: 5 additions & 10 deletions packages/visualizations-react/stories/Poi/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,7 @@ export const shapes: FeatureCollection = {
type: 'Point',
},
properties: {
key: 'Corsica',
cat: 'Red',
key: 'Corsica'
},
},
{
Expand All @@ -529,8 +528,7 @@ export const shapes: FeatureCollection = {
coordinates: [2.357573,48.837904],
},
properties: {
key: 'Paris',
cat: 'Red',
key: 'Paris'
},
},
{
Expand All @@ -540,8 +538,7 @@ export const shapes: FeatureCollection = {
coordinates: [-0.563328,44.838245],
},
properties: {
key: 'Bordeaux',
cat: 'Blue',
key: 'Bordeaux'
},
},
{
Expand All @@ -551,8 +548,7 @@ export const shapes: FeatureCollection = {
coordinates: [-1.552924,47.214847],
},
properties: {
key: 'Nantes',
cat: 'Blue',
key: 'Nantes'
},
},
{
Expand All @@ -562,8 +558,7 @@ export const shapes: FeatureCollection = {
coordinates: [5.360529,43.303114],
},
properties: {
key: 'Marseille',
cat: 'Red',
key: 'Marseille'
},
},
],
Expand Down
26 changes: 26 additions & 0 deletions packages/visualizations/src/components/MapPoi/Map.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import MapRender from './MapRender.svelte';
import type { Async } from '../../types';
import { getMapStyle, getMapSources, getMapLayers, getMapOptions } from './utils';
import type { PoiMapData, PoiMapOptions } from './types';
export let data: Async<PoiMapData>;
export let options: PoiMapOptions;
$: style = getMapStyle(options.style);
$: sources = getMapSources(data.value?.sources);
$: layers = getMapLayers(data.value?.layers);
$: computedOptions = getMapOptions(options);
</script>

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

<style>
</style>
118 changes: 118 additions & 0 deletions packages/visualizations/src/components/MapPoi/Map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { BBox } from 'geojson';
import maplibregl, {
LngLatBoundsLike,
LngLatLike,
MapOptions,
StyleSpecification,
} from 'maplibre-gl';

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

const DEFAULT_CENTER: LngLatLike = [3.5, 46];

export default class MapPOI {
private map: maplibregl.Map | null = null;

private isReady = false;

private baseStyle: StyleSpecification | null = null;

private queuedFunctions: Array<MapFunction> = [];

private navigation = new maplibregl.NavigationControl({ showCompass: false });

private queue(fn: MapFunction) {
if (this.isReady && this.map) fn(this.map);
else this.queuedFunctions.push(fn);
}

private enqueue(map: maplibregl.Map) {
this.queuedFunctions.forEach((fn) => fn(map));
this.queuedFunctions = [];
}

initialize(
style: MapOptions['style'],
container: HTMLElement,
options: Omit<MapOptions, 'style' | 'container'>
) {
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(this.map);
}
});
}

remove() {
this.queue((map) => 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((map) => {
if (this.baseStyle) {
map.setStyle({
...this.baseStyle,
sources: {
...sources,
...this.baseStyle.sources,
},
layers: [...this.baseStyle.layers, ...layers],
});
}
});
}

setBbox(bbox?: BBox) {
this.queue((map) => {
if (!bbox) {
// zoom-out to bounds defined in the initialization
map.setZoom(map.getMinZoom());
return;
}

// Cancel any saved max bounds to properly fitBounds
map.setMaxBounds(null);
// Using padding, keep enough room for controls (zoom) to make sure they don't hide anything
map.fitBounds(bbox as LngLatBoundsLike, {
animate: false,
padding: 40,
});
});
}

toggleInteractivity(interaction: 'enable' | 'disable') {
this.queue((map) => {
map.boxZoom[interaction]();
map.doubleClickZoom[interaction]();
map.dragPan[interaction]();
map.dragRotate[interaction]();
map.keyboard[interaction]();
map.scrollZoom[interaction]();
map.touchZoomRotate[interaction]();

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

if (interaction === 'disable' && hasNavigation) {
map.removeControl(this.navigation);
}
if (!hasNavigation && interaction === 'enable') {
map.addControl(this.navigation, 'top-right');
}
});
}
}
Loading

0 comments on commit 557767a

Please sign in to comment.