[components,maps][l]: leaflet map now supports multiple layers and tooltip props setting

This commit is contained in:
João Demenech 2023-07-07 17:55:49 -03:00
parent f04b86dda4
commit 805e2f0817
2 changed files with 183 additions and 75 deletions

View File

@ -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<boolean>(false);
// By default, assumes data is an Array...
const [geoJsonData, setGeoJsonData] = useState<any>(null);
const [layersData, setLayersData] = useState<any>([]);
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 ? (
<div className="w-full flex items-center justify-center w-[600px] h-[300px]">
<LoadingSpinner />
</div>
@ -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);
}}
>
<GeoJSONLayer
data={geoJsonData}
style={(geoJsonFeature: any) => {
return { color: geoJsonFeature?.color };
}}
onEachFeature={onEachFeature}
/>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LayersControl position="bottomright">
{layers.map((layer, idx) => {
const data = layersData.find(
(layerData) => layerData.name === layer.name
)?.data;
return (
data && (
<LayersControl.Overlay key={layer.name} checked name={layer.name}>
<GeoJSONLayer
data={data}
style={(geoJsonFeature: any) => {
// 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) =>
`<b>${name}:</b> ${feature.properties[name]}`
)
.join('<br />');
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,
});
}
},
});
});
},
}}
/>
;
</LayersControl.Overlay>
)
);
})}
</LayersControl>
</MapContainer>
);
}

View File

@ -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<MapProps>;
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,
},
};