Skip to content

Commit

Permalink
feat(POI Map): allow symbol layers
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinFabre-ods committed Nov 14, 2023
1 parent d4ef0e1 commit 1e373b5
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 79 deletions.
66 changes: 54 additions & 12 deletions packages/visualizations-react/stories/Poi/PoiMap.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { BBox } from 'geojson';
import { CATEGORY_ITEM_VARIANT, PopupDisplayTypes } from '@opendatasoft/visualizations';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import type { Layer } from '@opendatasoft/visualizations';
import type { Layer, PoiMapOptions } from '@opendatasoft/visualizations';

import { defaultSource, timeout } from '../utils';

Expand Down Expand Up @@ -31,9 +31,8 @@ const layer1: Layer = {
const layer2: Layer = {
id: 'layer-002',
source: 'battles',
type: 'circle',
color: 'red',
borderColor: 'white',
type: 'symbol',
iconImageId: 'battle-icon',
popup: {
display: PopupDisplayTypes.Sidebar,
getContent: async (_, properties) => {
Expand All @@ -57,6 +56,11 @@ const citiesColorMatch = {
borderColors: { Paris: 'white', Nantes: 'black', Bordeaux: 'white', Marseille: 'black' },
};

const battleImageMatch = {
key: 'name',
imageIds: { 'Battle of Verdun': 'battle-icon-red' },
};

const bbox: BBox = [-6.855469, 41.343825, 11.645508, 51.37178];

const legend = {
Expand Down Expand Up @@ -91,13 +95,19 @@ const legend = {
align: 'start' as const,
};

const options = {
const options: PoiMapOptions = {
style: BASE_STYLE,
bbox,
title: 'Lorem Ipsum',
subtitle: 'Dolor Sit Amet',
desciption: 'More aria description',
description: 'More aria description',
sourceLink: defaultSource,
images: {
'battle-icon':
'https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Big_battle_symbol.svg/14px-Big_battle_symbol.svg.png',
'battle-icon-red':
'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Battle_icon_gladii_red.svg/14px-Battle_icon_gladii_red.svg.png',
},
};

const meta: ComponentMeta<typeof PoiMap> = {
Expand All @@ -107,7 +117,7 @@ const meta: ComponentMeta<typeof PoiMap> = {

export default meta;

const Template: ComponentStory<typeof PoiMap> = args => (
const Template: ComponentStory<typeof PoiMap> = (args) => (
<div
style={{
width: '50%',
Expand Down Expand Up @@ -146,7 +156,15 @@ PoiMapNonInteractive.args = PoiMapNonInteractiveArgs;
*/
export const PoiMapMatchExpression: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapMatchExpressionArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
data: {
value: {
layers: [
{ ...layer1, colorMatch: citiesColorMatch },
{ ...layer2, iconImageMatch: battleImageMatch },
],
sources,
},
},
options,
};
PoiMapMatchExpression.args = PoiMapMatchExpressionArgs;
Expand All @@ -156,7 +174,15 @@ PoiMapMatchExpression.args = PoiMapMatchExpressionArgs;
*/
export const PoiMapLegendStart: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapLegendStartArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
data: {
value: {
layers: [
{ ...layer1, colorMatch: citiesColorMatch },
{ ...layer2, iconImageMatch: battleImageMatch },
],
sources,
},
},
options: { ...options, legend },
};
PoiMapLegendStart.args = PoiMapLegendStartArgs;
Expand All @@ -166,7 +192,15 @@ PoiMapLegendStart.args = PoiMapLegendStartArgs;
*/
export const PoiMapLegendCenter: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapLegendCenterArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
data: {
value: {
layers: [
{ ...layer1, colorMatch: citiesColorMatch },
{ ...layer2, iconImageMatch: battleImageMatch },
],
sources,
},
},
options: { ...options, legend: { ...legend, align: 'center' as const } },
};
PoiMapLegendCenter.args = PoiMapLegendCenterArgs;
Expand All @@ -176,8 +210,16 @@ PoiMapLegendCenter.args = PoiMapLegendCenterArgs;
*/
export const PoiMapMinMaxZooms: ComponentStory<typeof PoiMap> = Template.bind({});
const PoiMapMinMaxZoomsArgs = {
data: { value: { layers: [{ ...layer1, colorMatch: citiesColorMatch }, layer2], sources } },
options: {
data: {
value: {
layers: [
{ ...layer1, colorMatch: citiesColorMatch },
{ ...layer2, iconImageMatch: battleImageMatch },
],
sources,
},
},
options: {
...options,
legend,
minZoom: 3,
Expand Down
5 changes: 2 additions & 3 deletions packages/visualizations/src/components/MapPoi/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
sourceLink,
aspectRatio,
interactive,
images,
transformRequest,
} = getMapOptions(options));
Expand Down Expand Up @@ -63,10 +64,8 @@
{sourceLink}
{aspectRatio}
{interactive}
{images}
{transformRequest}
/>
{/key}
</div>

<style>
</style>
32 changes: 31 additions & 1 deletion packages/visualizations/src/components/MapPoi/Map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BBox } from 'geojson';
import { debounce } from 'lodash';
import { debounce, difference } from 'lodash';
import maplibregl, {
LngLatBoundsLike,
LngLatLike,
Expand Down Expand Up @@ -318,6 +318,36 @@ export default class MapPOI {
this.queue((map) => this.setPopup(map));
}

/**
* Load images into the map.
* Remove automatically any images previously loaded that are no longer defined in the images object.
*/
loadImages(images?: Record<string, string>) {
if (!images) return;
this.queue((map) => {
const loadedImages = map.listImages();
const imagesIds = Object.keys(images);

const imagesToRemove = difference(loadedImages, imagesIds);
const imagesToAdd = difference(imagesIds, loadedImages);

imagesToRemove.forEach((imageId) => {
map.removeImage(imageId);
});

imagesToAdd.forEach((imageId) => {
map.loadImage(images[imageId], (error, image) => {
if (error || !image) {
// eslint-disable-next-line no-console
console.warn(`Fail to load image: ${imageId}`);
} else {
map.addImage(imageId, image);
}
});
});
});
}

toggleInteractivity(
interaction: 'enable' | 'disable',
{ onDisable, onEnable }: { onDisable?: () => void; onEnable?: () => void }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import Map from './Map';
import { getCenterZoomOptions } from './utils';
import type { PopupsConfiguration } from './types';
import type { PopupsConfiguration, PoiMapOptions } from './types';
// Base style, sources and layers
export let style: MapOptions['style'];
Expand All @@ -25,6 +25,7 @@
export let minZoom: number | undefined;
export let maxZoom: number | undefined;
export let center: LngLatLike | undefined;
export let images: PoiMapOptions['images'];
export let aspectRatio: number;
export let interactive: boolean;
export let title: string | undefined;
Expand Down Expand Up @@ -53,6 +54,7 @@
$: map.setSourcesAndLayers(sources, layers);
$: map.setPopupsConfiguration(popupsConfiguration);
$: map.jumpTo(getCenterZoomOptions({ zoom, center }));
$: map.loadImages(images);
$: cssVarStyles = `--aspect-ratio:${aspectRatio};`;
// Lifecycle
Expand Down
38 changes: 30 additions & 8 deletions packages/visualizations/src/components/MapPoi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
GeoJSONFeature,
LngLatLike,
RequestTransformFunction,
SymbolLayerSpecification,
} from 'maplibre-gl';
import type { BBox, GeoJsonProperties } from 'geojson';

Expand Down Expand Up @@ -50,22 +51,30 @@ export interface PoiMapOptions {
legend?: CategoryLegend;
/** Link button to source */
sourceLink?: Source;
/** Images to load by the Map. keys are image ids */
images?: Record<string, string>;
}

export type PoiMapStyleOption = Partial<Pick<StyleSpecification, 'sources' | 'layers'>>;

// Supported layers
export type LayerSpecification = CircleLayerSpecification;
export type LayerSpecification = CircleLayerSpecification | SymbolLayerSpecification;

type BaseLayer = {
id: string;
source: string;
sourceLayer?: string;
/**
* A feature for which a popup is defined will update the cursor style in pointer mode
*/
popup?: PopupLayer;
};
/**
* Base on Maplibre layers https://maplibre.org/maplibre-style-spec/layers/
* Use only part of the configuration to build layers with consistent styles.
*/
export type Layer = {
id: string;
source: string;
sourceLayer?: string;
type: LayerSpecification['type'];
export type CircleLayer = BaseLayer & {
type: CircleLayerSpecification['type'];
color: Color;
borderColor?: Color;
circleRadius?: number;
Expand All @@ -79,12 +88,25 @@ export type Layer = {
colors: { [key: string]: Color };
borderColors?: { [key: string]: Color };
};
};

export type SymbolLayer = BaseLayer & {
type: SymbolLayerSpecification['type'];
/** id of the image to use as icon-image */
iconImageId: string;
/**
* A feature for which a popup is defined will update the cursor style in pointer mode
* Set a icon image based on a value.
* If no match, default icon image comes from `iconImageId`
*/
popup?: PopupLayer;
iconImageMatch?: {
key: string;
/** Keys must match from the options.images keys */
imageIds: Record<string, string>;
};
};

export type Layer = CircleLayer | SymbolLayer;

export enum PopupDisplayTypes {
Tooltip = 'tooltip',
Sidebar = 'sidebar',
Expand Down
Loading

0 comments on commit 1e373b5

Please sign in to comment.