Compare commits

...

3 Commits

Author SHA1 Message Date
Luccas Mateus
ba438e49d0
Update vite.config.ts 2023-07-07 18:21:01 -03:00
João Demenech
8a3669c2c9 Remove unused var 2023-07-07 18:17:39 -03:00
João Demenech
805e2f0817 [components,maps][l]: leaflet map now supports multiple layers and tooltip props setting 2023-07-07 17:55:49 -03:00
3 changed files with 184 additions and 76 deletions

View File

@ -6,93 +6,86 @@ import {
MapContainer, MapContainer,
TileLayer, TileLayer,
GeoJSON as GeoJSONLayer, GeoJSON as GeoJSONLayer,
LayersControl
} from 'react-leaflet'; } from 'react-leaflet';
import * as L from 'leaflet'; import * as L from 'leaflet';
export type MapProps = { 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; title?: string;
colorScale?: {
starting: string;
ending: string;
};
center?: { latitude: number | undefined; longitude: number | undefined }; center?: { latitude: number | undefined; longitude: number | undefined };
zoom?: number; zoom?: number;
tooltip?: {
prop: string;
};
}; };
export function Map({ export function Map({
data, layers = [
title = '', {
colorScale = { starting: 'blue', ending: 'red' }, data: null,
name: null,
colorScale: { starting: 'blue', ending: 'red' },
tooltip: true,
},
],
center = { latitude: 45, longitude: 45 }, center = { latitude: 45, longitude: 45 },
zoom = 2, zoom = 2,
tooltip = { title = '',
prop: '',
},
}: MapProps) { }: MapProps) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [layersData, setLayersData] = useState<any>([]);
// By default, assumes data is an Array...
const [geoJsonData, setGeoJsonData] = useState<any>(null);
useEffect(() => { useEffect(() => {
// If data is string, assume it's a URL const loadDataPromises = layers.map(async (layer) => {
if (typeof data === 'string') { let layerData: any;
setIsLoading(true);
loadData(data).then((res: any) => { if (typeof layer.data === 'string') {
const geoJsonObject = JSON.parse(res); // 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 const colorScaleAr = chroma
.scale([colorScale.starting, colorScale.ending]) .scale([layer.colorScale.starting, layer.colorScale.ending])
.mode('lch') .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) { if (feature.color === undefined) {
feature.color = colorScaleAr[i]; feature.color = colorScaleAr[i];
} }
}); });
}
setGeoJsonData(geoJsonObject); return { name: layer.name, data: layerData };
setIsLoading(false); });
});
} else { Promise.all(loadDataPromises).then((values) => {
setGeoJsonData(data); setLayersData(values);
} setIsLoading(false);
});
}, []); }, []);
const onEachFeature = (feature, layer) => { return isLoading ? (
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 ? (
<div className="w-full flex items-center justify-center w-[600px] h-[300px]"> <div className="w-full flex items-center justify-center w-[600px] h-[300px]">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
@ -104,8 +97,10 @@ export function Map({
className="h-80 w-full" className="h-80 w-full"
// @ts-ignore // @ts-ignore
whenReady={(map: any) => { whenReady={(map: any) => {
// Enable zoom using scroll wheel
map.target.scrollWheelZoom.enable(); map.target.scrollWheelZoom.enable();
// Create the title box
var info = new L.Control() as any; var info = new L.Control() as any;
info.onAdd = function () { info.onAdd = function () {
@ -119,21 +114,98 @@ export function Map({
}; };
if (title) info.addTo(map.target); if (title) info.addTo(map.target);
setTimeout(() => map.target.invalidateSize(), 5000);
}} }}
> >
<GeoJSONLayer
data={geoJsonData}
style={(geoJsonFeature: any) => {
return { color: geoJsonFeature?.color };
}}
onEachFeature={onEachFeature}
/>
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/> />
<LayersControl position="bottomright">
{layers.map((layer) => {
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> </MapContainer>
); );
} }

View File

@ -8,7 +8,7 @@ const meta: Meta = {
component: Map, component: Map,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
data: { layers: {
description: description:
'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object', 'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object',
}, },
@ -21,9 +21,6 @@ const meta: Meta = {
zoom: { zoom: {
description: 'Zoom level', description: 'Zoom level',
}, },
tooltip: {
description: 'Tooltip settings'
}
}, },
}; };
@ -35,21 +32,60 @@ type Story = StoryObj<MapProps>;
export const GeoJSONPolygons: Story = { export const GeoJSONPolygons: Story = {
name: 'GeoJSON polygons map', name: 'GeoJSON polygons map',
args: { 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', title: 'Seas and Oceans Map',
center: { latitude: 45, longitude: 0 }, center: { latitude: 45, longitude: 0 },
zoom: 2, zoom: 2,
tooltip: { prop: 'name' },
}, },
}; };
export const GeoJSONPoints: Story = { export const GeoJSONPoints: Story = {
name: 'GeoJSON points map', name: 'GeoJSON points map',
args: { 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', title: 'Roads in York',
center: { latitude: 53.9614, longitude: -1.0739 }, center: { latitude: 53.9614, longitude: -1.0739 },
zoom: 12, 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,
}, },
}; };

View File

@ -1,4 +1,4 @@
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react';
import path from 'node:path'; import path from 'node:path';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import dts from 'vite-plugin-dts'; import dts from 'vite-plugin-dts';