From 805e2f08177f5e8fbeb66373a66f68f96ffcc796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Demenech?= Date: Fri, 7 Jul 2023 17:55:49 -0300 Subject: [PATCH] [components,maps][l]: leaflet map now supports multiple layers and tooltip props setting --- packages/components/src/components/Map.tsx | 206 ++++++++++++++------- packages/components/stories/Map.stories.ts | 52 +++++- 2 files changed, 183 insertions(+), 75 deletions(-) diff --git a/packages/components/src/components/Map.tsx b/packages/components/src/components/Map.tsx index e55b2769..0ecd7f7f 100644 --- a/packages/components/src/components/Map.tsx +++ b/packages/components/src/components/Map.tsx @@ -6,93 +6,86 @@ import { MapContainer, TileLayer, GeoJSON as GeoJSONLayer, + LayersControl } from 'react-leaflet'; import * as L from 'leaflet'; export type MapProps = { - data: string | GeoJSON.GeoJSON; + layers: { + data: string | GeoJSON.GeoJSON; + name: string; + colorScale?: { + starting: string; + ending: string; + }; + tooltip?: + | { + propNames: string[]; + } + | boolean; + _id?: number; + }[]; title?: string; - colorScale?: { - starting: string; - ending: string; - }; center?: { latitude: number | undefined; longitude: number | undefined }; zoom?: number; - tooltip?: { - prop: string; - }; }; export function Map({ - data, - title = '', - colorScale = { starting: 'blue', ending: 'red' }, + layers = [ + { + data: null, + name: null, + colorScale: { starting: 'blue', ending: 'red' }, + tooltip: true, + }, + ], center = { latitude: 45, longitude: 45 }, zoom = 2, - tooltip = { - prop: '', - }, + title = '', }: MapProps) { const [isLoading, setIsLoading] = useState(false); - - // By default, assumes data is an Array... - const [geoJsonData, setGeoJsonData] = useState(null); + const [layersData, setLayersData] = useState([]); useEffect(() => { - // If data is string, assume it's a URL - if (typeof data === 'string') { - setIsLoading(true); + const loadDataPromises = layers.map(async (layer) => { + let layerData: any; - loadData(data).then((res: any) => { - const geoJsonObject = JSON.parse(res); + if (typeof layer.data === 'string') { + // If "data" is string, assume it's a URL + setIsLoading(true); + layerData = await loadData(layer.data).then((res: any) => { + return JSON.parse(res); + }); + } else { + // Else, expect raw GeoJSON + layerData = layer.data; + } + if (layer.colorScale) { const colorScaleAr = chroma - .scale([colorScale.starting, colorScale.ending]) + .scale([layer.colorScale.starting, layer.colorScale.ending]) .mode('lch') - .colors(geoJsonObject.features.length); + .colors(layerData.features.length); - geoJsonObject.features.forEach((feature, i) => { + layerData.features.forEach((feature, i) => { + // Only style if the feature doesn't have a color prop if (feature.color === undefined) { feature.color = colorScaleAr[i]; } }); + } - setGeoJsonData(geoJsonObject); - setIsLoading(false); - }); - } else { - setGeoJsonData(data); - } + return { name: layer.name, data: layerData }; + }); + + Promise.all(loadDataPromises).then((values) => { + setLayersData(values); + setIsLoading(false); + }); }, []); - const onEachFeature = (feature, layer) => { - const geometryType = feature.type; - - if (tooltip.prop) - layer.bindTooltip(feature.properties[tooltip.prop], { - direction: 'center', - }); - - layer.on({ - mouseover: (event) => { - if (['Polygon', 'MultiPolygon'].includes(geometryType)) { - event.target.setStyle({ - fillColor: '#B85042', - }); - } - }, - mouseout: (event) => { - if (['Polygon', 'MultiPolygon'].includes(geometryType)) { - event.target.setStyle({ - fillColor: '#A7BEAE', - }); - } - }, - }); - }; - - return isLoading || !geoJsonData ? ( + return isLoading ? (
@@ -104,8 +97,10 @@ export function Map({ className="h-80 w-full" // @ts-ignore whenReady={(map: any) => { + // Enable zoom using scroll wheel map.target.scrollWheelZoom.enable(); + // Create the title box var info = new L.Control() as any; info.onAdd = function () { @@ -119,21 +114,98 @@ export function Map({ }; if (title) info.addTo(map.target); - - setTimeout(() => map.target.invalidateSize(), 5000); }} > - { - return { color: geoJsonFeature?.color }; - }} - onEachFeature={onEachFeature} - /> + + {layers.map((layer, idx) => { + const data = layersData.find( + (layerData) => layerData.name === layer.name + )?.data; + + return ( + data && ( + + { + // Set the fill color of each feature when appliable + if ( + !['Point', 'MultiPoint'].includes(geoJsonFeature.type) + ) { + return { color: geoJsonFeature?.color }; + } + }} + eventHandlers={{ + add: (e) => { + const featureGroup = e.target; + const tooltip = layer.tooltip; + + featureGroup.eachLayer((featureLayer) => { + const feature = featureLayer.feature; + const geometryType = feature.geometry.type; + + if (tooltip) { + const featurePropNames = Object.keys( + feature.properties + ); + let includedFeaturePropNames; + + if (tooltip === true) { + includedFeaturePropNames = featurePropNames; + } else { + includedFeaturePropNames = tooltip.propNames.filter( + (name) => featurePropNames.includes(name) + ); + } + + if (includedFeaturePropNames) { + const tooltipContent = includedFeaturePropNames + .map( + (name) => + `${name}: ${feature.properties[name]}` + ) + .join('
'); + + featureLayer.bindTooltip(tooltipContent, { + direction: 'center', + }); + } + } + + featureLayer.on({ + mouseover: (event) => { + if ( + ['Polygon', 'MultiPolygon'].includes(geometryType) + ) { + event.target.setStyle({ + fillOpacity: 0.5, + }); + } + }, + mouseout: (event) => { + if ( + ['Polygon', 'MultiPolygon'].includes(geometryType) + ) { + event.target.setStyle({ + fillOpacity: 0.2, + }); + } + }, + }); + }); + }, + }} + /> + ; +
+ ) + ); + })} +
); } diff --git a/packages/components/stories/Map.stories.ts b/packages/components/stories/Map.stories.ts index c39cdc9e..9c49e896 100644 --- a/packages/components/stories/Map.stories.ts +++ b/packages/components/stories/Map.stories.ts @@ -8,7 +8,7 @@ const meta: Meta = { component: Map, tags: ['autodocs'], argTypes: { - data: { + layers: { description: 'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object', }, @@ -21,9 +21,6 @@ const meta: Meta = { zoom: { description: 'Zoom level', }, - tooltip: { - description: 'Tooltip settings' - } }, }; @@ -35,21 +32,60 @@ type Story = StoryObj; export const GeoJSONPolygons: Story = { name: 'GeoJSON polygons map', args: { - data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', + layers: [ + { + data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', + name: 'Polygons', + tooltip: { propNames: ['name'] }, + colorScale: { + starting: '#ff0000', + ending: '#00ff00', + }, + }, + ], title: 'Seas and Oceans Map', center: { latitude: 45, longitude: 0 }, zoom: 2, - tooltip: { prop: 'name' }, }, }; export const GeoJSONPoints: Story = { name: 'GeoJSON points map', args: { - data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', + layers: [ + { + data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', + name: 'Points', + tooltip: { propNames: ['Location'] }, + }, + ], title: 'Roads in York', center: { latitude: 53.9614, longitude: -1.0739 }, zoom: 12, - tooltip: { prop: 'Location' }, + }, +}; + +export const GeoJSONMultipleLayers: Story = { + name: 'GeoJSON polygons and points map', + args: { + layers: [ + { + data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', + name: 'Points', + tooltip: true, + }, + { + data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', + name: 'Polygons', + tooltip: true, + colorScale: { + starting: '#ff0000', + ending: '#00ff00', + }, + }, + ], + title: 'Polygons and points', + center: { latitude: 45, longitude: 0 }, + zoom: 2, }, };