import { useEffect, useRef } from 'react'; import { Map as MlMap, Marker, NavigationControl } from 'maplibre-gl'; import type { MapAssetFeature, MapAssetFeatureCollection, MapEntityKind } 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' }], }; type GpsPoint = { feature: MapAssetFeature; coordinates: [number, number] }; function pointCoordinates(feature: MapAssetFeature): [number, number] | null { const geometry = feature.geometry as { type: string; coordinates: unknown }; if (geometry.type.toLowerCase() !== 'point' || !Array.isArray(geometry.coordinates)) return null; const [longitude, latitude] = geometry.coordinates; if (typeof longitude !== 'number' || typeof latitude !== 'number') return null; if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null; if (longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90) return null; return [longitude, latitude]; } function gpsPoints(collection: MapAssetFeatureCollection): GpsPoint[] { return collection.features.flatMap((feature) => { const coordinates = pointCoordinates(feature); return coordinates ? [{ feature, coordinates }] : []; }); } function fitToPoints(map: MlMap, points: GpsPoint[]) { if (!points.length) return; const bounds = points.reduce<[number, number, number, number]>((result, point) => [ Math.min(result[0], point.coordinates[0]), Math.min(result[1], point.coordinates[1]), Math.max(result[2], point.coordinates[0]), Math.max(result[3], point.coordinates[1]), ], [points[0]!.coordinates[0], points[0]!.coordinates[1], points[0]!.coordinates[0], points[0]!.coordinates[1]]); if (bounds[0] === bounds[2] && bounds[1] === bounds[3]) map.flyTo({ center: [bounds[0], bounds[1]], zoom: 15 }); else map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 80, maxZoom: 15 }); } function visualOffsets(points: GpsPoint[]) { const groups = new Map(); points.forEach((point) => { const key = `${point.coordinates[0].toFixed(5)}:${point.coordinates[1].toFixed(5)}`; groups.set(key, [...(groups.get(key) ?? []), point]); }); const offsets = new Map(); groups.forEach((group) => { group.forEach((point, index) => { if (group.length === 1) { offsets.set(String(point.feature.id), [0, 0]); return; } const angle = (Math.PI * 2 * index) / group.length; const radius = Math.min(18, 8 + group.length); offsets.set(String(point.feature.id), [Math.cos(angle) * radius, Math.sin(angle) * radius]); }); }); return offsets; } 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 markersRef = useRef([]); const onSelectRef = useRef(onSelect); onSelectRef.current = onSelect; useEffect(() => { if (!containerRef.current) return; const map = new MlMap({ container: containerRef.current, style: osmStyle, center: [-68.8458, -32.8895], zoom: 6 }); mapRef.current = map; map.addControl(new NavigationControl(), 'top-right'); map.on('click', () => onSelectRef.current(null)); return () => { markersRef.current.forEach((marker) => marker.remove()); markersRef.current = []; mapRef.current = null; map.remove(); }; }, []); useEffect(() => { const map = mapRef.current; if (!map) return; markersRef.current.forEach((marker) => marker.remove()); markersRef.current = []; const points = gpsPoints(data); const offsets = visualOffsets(points); points.forEach(({ feature, coordinates }) => { const kind = (feature.properties.entityKind ?? 'ASSET') as MapEntityKind; const element = document.createElement('button'); element.type = 'button'; element.className = `map-gps-marker kind-${kind.toLowerCase()}${String(feature.id) === selectedId ? ' selected' : ''}`; element.title = `${feature.properties.typeName}: ${feature.properties.name}`; element.setAttribute('aria-label', element.title); element.addEventListener('click', (event) => { event.stopPropagation(); onSelectRef.current(String(feature.id)); }); const marker = new Marker({ element, offset: offsets.get(String(feature.id)) ?? [0, 0] }).setLngLat(coordinates).addTo(map); markersRef.current.push(marker); }); fitToPoints(map, points); }, [data, selectedId]); return
; }