import { useEffect, useRef } from 'react'; import { Map, NavigationControl, type GeoJSONSource, type MapGeoJSONFeature, } from 'maplibre-gl'; import type { MapAssetFeatureCollection } from '../../lib/api'; const osmStyle = { version: 8 as const, sources: { osm: { type: 'raster' as const, tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize: 256, attribution: '© OpenStreetMap contributors', }, }, layers: [{ id: 'osm', type: 'raster' as const, source: 'osm' }], }; const interactiveLayers = ['assets-points', 'assets-lines', 'assets-polygons']; function boundsFromFeatures(collection: MapAssetFeatureCollection) { const positions: Array<[number, number]> = []; collection.features.forEach((feature) => { if (feature.geometry.type === 'Point') positions.push(feature.geometry.coordinates); if (feature.geometry.type === 'LineString') positions.push(...feature.geometry.coordinates); if (feature.geometry.type === 'Polygon') feature.geometry.coordinates.forEach((ring) => positions.push(...ring)); }); if (!positions.length) return null; return positions.reduce<[number, number, number, number]>((result, point) => [ Math.min(result[0], point[0]), Math.min(result[1], point[1]), Math.max(result[2], point[0]), Math.max(result[3], point[1]), ], [positions[0]![0], positions[0]![1], positions[0]![0], positions[0]![1]]); } export function DhMap({ data, selectedId, onSelect, }: { data: MapAssetFeatureCollection; selectedId: string | null; onSelect: (id: string | null) => void; }) { const containerRef = useRef(null); const mapRef = useRef(null); const onSelectRef = useRef(onSelect); const dataRef = useRef(data); onSelectRef.current = onSelect; dataRef.current = data; useEffect(() => { if (!containerRef.current) return; const map = new Map({ container: containerRef.current, style: osmStyle, center: [-68.8458, -32.8895], zoom: 6, }); mapRef.current = map; map.addControl(new NavigationControl(), 'top-right'); map.on('load', () => { map.addSource('assets', { type: 'geojson', data: dataRef.current as never }); map.addLayer({ id: 'assets-polygons', type: 'fill', source: 'assets', filter: ['==', ['geometry-type'], 'Polygon'], paint: { 'fill-color': '#2864dc', 'fill-opacity': 0.22, 'fill-outline-color': '#184caf' }, }); map.addLayer({ id: 'assets-lines', type: 'line', source: 'assets', filter: ['==', ['geometry-type'], 'LineString'], paint: { 'line-color': '#2864dc', 'line-width': 3 }, }); map.addLayer({ id: 'assets-points', type: 'circle', source: 'assets', filter: ['==', ['geometry-type'], 'Point'], paint: { 'circle-radius': 7, 'circle-color': '#2864dc', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2, }, }); map.addLayer({ id: 'assets-selected-polygons', type: 'line', source: 'assets', filter: ['all', ['==', ['geometry-type'], 'Polygon'], ['==', ['get', 'id'], '']], paint: { 'line-color': '#f59e0b', 'line-width': 4 }, }); map.addLayer({ id: 'assets-selected-lines', type: 'line', source: 'assets', filter: ['all', ['==', ['geometry-type'], 'LineString'], ['==', ['get', 'id'], '']], paint: { 'line-color': '#f59e0b', 'line-width': 6 }, }); map.addLayer({ id: 'assets-selected-points', type: 'circle', source: 'assets', filter: ['all', ['==', ['geometry-type'], 'Point'], ['==', ['get', 'id'], '']], paint: { 'circle-radius': 10, 'circle-color': '#f59e0b', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 3, }, }); const click = (event: { features?: MapGeoJSONFeature[] }) => { const id = event.features?.[0]?.properties?.id; onSelectRef.current(typeof id === 'string' ? id : null); }; interactiveLayers.forEach((layer) => { map.on('click', layer, click); map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; }); }); map.on('click', (event) => { const hits = map.queryRenderedFeatures(event.point, { layers: interactiveLayers }); if (!hits.length) onSelectRef.current(null); }); const bounds = boundsFromFeatures(dataRef.current); if (bounds) { if (bounds[0] === bounds[2] && bounds[1] === bounds[3]) { map.flyTo({ center: [bounds[0], bounds[1]], zoom: 13 }); } else { map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 55, maxZoom: 15 }); } } }); return () => { mapRef.current = null; map.remove(); }; }, []); useEffect(() => { const map = mapRef.current; if (!map?.isStyleLoaded()) return; (map.getSource('assets') as GeoJSONSource | undefined)?.setData(data as never); const bounds = boundsFromFeatures(data); if (bounds) { if (bounds[0] === bounds[2] && bounds[1] === bounds[3]) { map.flyTo({ center: [bounds[0], bounds[1]], zoom: 13 }); } else { map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 55, maxZoom: 15 }); } } }, [data]); useEffect(() => { const map = mapRef.current; if (!map?.isStyleLoaded()) return; const id = selectedId ?? '__none__'; const filters: Array<[string, string]> = [ ['assets-selected-points', 'Point'], ['assets-selected-lines', 'LineString'], ['assets-selected-polygons', 'Polygon'], ]; filters.forEach(([layer, geometryType]) => { map.setFilter(layer, ['all', ['==', ['geometry-type'], geometryType], ['==', ['get', 'id'], id]]); }); }, [selectedId]); return
; }