import { useEffect, useState } from 'react'; import LoadingSpinner from './LoadingSpinner'; import loadData from '../lib/loadData'; import chroma from 'chroma-js'; import { MapContainer, TileLayer, GeoJSON as GeoJSONLayer, LayersControl, } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import * as L from 'leaflet'; export type MapProps = { layers: { data: string | GeoJSON.GeoJSON; name: string; colorScale?: { starting: string; ending: string; }; tooltip?: | { propNames: string[]; } | boolean; _id?: number; }[]; title?: string; center?: { latitude: number | undefined; longitude: number | undefined }; zoom?: number; }; export function Map({ layers = [ { data: null, name: null, colorScale: { starting: 'blue', ending: 'red' }, tooltip: true, }, ], center = { latitude: 45, longitude: 45 }, zoom = 2, title = '', }: MapProps) { const [isLoading, setIsLoading] = useState(false); const [layersData, setLayersData] = useState([]); useEffect(() => { const loadDataPromises = layers.map(async (layer) => { let layerData: any; 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([layer.colorScale.starting, layer.colorScale.ending]) .mode('lch') .colors(layerData.features.length); layerData.features.forEach((feature, i) => { // Only style if the feature doesn't have a color prop if (feature.color === undefined) { feature.color = colorScaleAr[i]; } }); } return { name: layer.name, data: layerData }; }); Promise.all(loadDataPromises).then((values) => { setLayersData(values); setIsLoading(false); }); }, []); return isLoading ? (
) : ( { // Enable zoom using scroll wheel map.target.scrollWheelZoom.enable(); // Create the title box var info = new L.Control() as any; info.onAdd = function () { this._div = L.DomUtil.create('div', 'info'); this.update(); return this._div; }; info.update = function () { this._div.innerHTML = `

${title}

`; }; if (title) info.addTo(map.target); }} > {layers.map((layer) => { const data = layersData.find( (layerData) => layerData.name === layer.name )?.data; return ( data && ( { // This resolver an issue in which the bundled map was // not finding the images const leafletBase = 'https://unpkg.com/leaflet@1.9.4/dist/images/'; const icon = new L.Icon({ iconUrl: leafletBase + 'marker-icon.png', iconRetinaUrl: leafletBase + 'marker-icon-2x.png', shadowUrl: leafletBase + 'marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], tooltipAnchor: [16, -28], shadowSize: [41, 41], }); const iconMarker = L.marker(latlng, { icon }); return iconMarker; }} 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) => `${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, }); } }, }); }); }, }} /> ;
) ); })}
); }