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 371bb88 commit a9eb851
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 341 deletions.
11 changes: 7 additions & 4 deletions packages/visualizations-react/src/components/PoiGeoJson.tsx
Original file line number Diff line number Diff line change
@@ -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<Props<FeatureCollection, PoiMapOptions>> = wrap(_PoiGeoJson);
const PoiGeoJson: FC<{ data: Async<PoiMapData>; options: PoiMapOptions }> = wrap(_PoiGeoJson);
export default PoiGeoJson;
79 changes: 46 additions & 33 deletions packages/visualizations-react/stories/Poi/PoiGeoJson.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof PoiGeoJson> = {
title: 'Poi/GeoJson',
Expand All @@ -14,9 +28,7 @@ const meta: ComponentMeta<typeof PoiGeoJson> = {

export default meta;

const Template: ComponentStory<typeof PoiGeoJson> = (
args: Props<FeatureCollection, PoiMapOptions>
) => (
const Template: ComponentStory<typeof PoiGeoJson> = args => (
<div
style={{
width: '50%',
Expand All @@ -30,40 +42,41 @@ const Template: ComponentStory<typeof PoiGeoJson> = (
</div>
);

export const PoiMapNoLayersParams = Template.bind({});
const PoiMapNoLayersParamsArgs: Props<FeatureCollection, PoiMapOptions> = {
data: { value: shapes },
options: {
style: DEMO_BASEMAP,
},
/**
* STORY: No layer params
*/
export const PoiMapNoLayersParams : ComponentStory<typeof PoiGeoJson> = 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<FeatureCollection, PoiMapOptions> = {
data: { value: shapes },
/**
* STORY: No interactive
*/
export const PoiMapNonInteractive : ComponentStory<typeof PoiGeoJson> = 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<FeatureCollection, PoiMapOptions> = {
data: { value: shapes },
options: {
layerParams,
style: DEMO_BASEMAP,
/**
* STORY: With match expression
*/
export const PoiMapMatchExpression : ComponentStory<typeof PoiGeoJson> = 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
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 = () => 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<MapFunction> = [];

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

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();
});
}

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');
}
});
}
}
Loading

0 comments on commit a9eb851

Please sign in to comment.