Spaces:
Paused
Paused
Download web/src/components/MapView.jsx from secutorpro/dispatch-console: direct link, hf CLI and curl.
- Browser
- Download file 3.06 kB
-
https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/web/src/components/MapView.jsx
- Command line
-
hf download hf://spaces/secutorpro/dispatch-console/web/src/components/MapView.jsx
-
curl -L -o MapView.jsx https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/web/src/components/MapView.jsx
3.06 kB
| 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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: `<div class="${cls}" style="background:${colorOf(m.group_id)}">${m.name.slice(0, 2).toUpperCase()}</div>`, | |
| 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( | |
| `<b>${m.name}</b>${m.callsign ? ` · ${m.callsign}` : ''} · ${m.group_name || 'no group'}<br/>` + | |
| `${m.status} · battery ${m.battery != null ? m.battery + '%' : '–'}<br/>` + | |
| `${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 <div ref={elRef} className="map" />; | |
| } |