Compare commits
3 Commits
main
...
feature/le
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba438e49d0 | ||
|
|
8a3669c2c9 | ||
|
|
805e2f0817 |
@ -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 = {
|
||||||
|
layers: {
|
||||||
data: string | GeoJSON.GeoJSON;
|
data: string | GeoJSON.GeoJSON;
|
||||||
title?: string;
|
name: string;
|
||||||
colorScale?: {
|
colorScale?: {
|
||||||
starting: string;
|
starting: string;
|
||||||
ending: string;
|
ending: string;
|
||||||
};
|
};
|
||||||
|
tooltip?:
|
||||||
|
| {
|
||||||
|
propNames: string[];
|
||||||
|
}
|
||||||
|
| boolean;
|
||||||
|
_id?: number;
|
||||||
|
}[];
|
||||||
|
title?: 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;
|
||||||
|
|
||||||
|
if (typeof layer.data === 'string') {
|
||||||
|
// If "data" is string, assume it's a URL
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
layerData = await loadData(layer.data).then((res: any) => {
|
||||||
|
return JSON.parse(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Else, expect raw GeoJSON
|
||||||
|
layerData = layer.data;
|
||||||
|
}
|
||||||
|
|
||||||
loadData(data).then((res: any) => {
|
if (layer.colorScale) {
|
||||||
const geoJsonObject = JSON.parse(res);
|
|
||||||
|
|
||||||
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 };
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(loadDataPromises).then((values) => {
|
||||||
|
setLayersData(values);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setGeoJsonData(data);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
|
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: {
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
|
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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user