import { useEffect, useRef } from 'react'; import L from 'leaflet'; import { api } from '../api.js'; export default function MapView({ members, groups, sos, selected, onSelect }) { const elRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef(new Map()); const trailRef = useRef(null); const didFit = useRef(false); useEffect(() => { const map = L.map(elRef.current).setView([48.8566, 2.3522], 12); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, }).addTo(map); mapRef.current = map; return () => { map.remove(); mapRef.current = null; }; }, []); useEffect(() => { const map = mapRef.current; if (!map) return; const sosIds = new Set(sos.filter((a) => a.state !== 'resolved').map((a) => a.member_id)); const colorOf = (gid) => groups.find((g) => g.id === gid)?.color || '#64748b'; const positioned = members.filter((m) => m.lat != null && m.lng != null); const seen = new Set(); for (const m of positioned) { seen.add(m.id); const cls = `pin ${m.status === 'online' ? 'online' : 'offline'} ${sosIds.has(m.id) ? 'sos' : ''}`; const icon = L.divIcon({ className: 'pin-wrap', html: `
${m.name.slice(0, 2).toUpperCase()}
`, iconSize: [26, 26], iconAnchor: [13, 13], }); let mk = markersRef.current.get(m.id); if (!mk) { mk = L.marker([m.lat, m.lng], { icon }).addTo(map); mk.on('click', () => onSelect(m.id)); markersRef.current.set(m.id, mk); } else { mk.setLatLng([m.lat, m.lng]); mk.setIcon(icon); } mk.bindPopup( `${m.name}${m.callsign ? ` · ${m.callsign}` : ''} · ${m.group_name || 'no group'}
` + `${m.status} · battery ${m.battery != null ? m.battery + '%' : '–'}
` + `${m.lat.toFixed(5)}, ${m.lng.toFixed(5)}` ); } for (const [id, mk] of markersRef.current) { if (!seen.has(id)) { map.removeLayer(mk); markersRef.current.delete(id); } } if (!didFit.current && positioned.length) { didFit.current = true; map.fitBounds(L.latLngBounds(positioned.map((m) => [m.lat, m.lng])).pad(0.3)); } }, [members, groups, sos, onSelect]); // GPS trail of the selected member useEffect(() => { const map = mapRef.current; if (!map) return; trailRef.current?.remove(); trailRef.current = null; if (!selected) return; let cancelled = false; api.get(`/members/${selected}/trail`).then((rows) => { if (cancelled || rows.length < 2) return; trailRef.current = L.polyline(rows.map((r) => [r.lat, r.lng]), { color: '#4fc3f7', weight: 3, opacity: 0.7 }).addTo(map); }).catch(() => {}); return () => { cancelled = true; }; }, [selected]); return
; }