secutorpro commited on
Commit
025301c
·
verified ·
1 Parent(s): 2b8468d

live Leaflet map with status markers and SOS pulse

Browse files
Files changed (1) hide show
  1. web/src/components/MapView.jsx +66 -0
web/src/components/MapView.jsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from 'react';
2
+ import L from 'leaflet';
3
+
4
+ export default function MapView({ members, groups, sos }) {
5
+ const elRef = useRef(null);
6
+ const mapRef = useRef(null);
7
+ const markersRef = useRef(new Map());
8
+ const didFit = useRef(false);
9
+
10
+ useEffect(() => {
11
+ const map = L.map(elRef.current, { center: [48.8566, 2.3522], zoom: 12 });
12
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
13
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
14
+ maxZoom: 19,
15
+ }).addTo(map);
16
+ mapRef.current = map;
17
+ return () => map.remove();
18
+ }, []);
19
+
20
+ useEffect(() => {
21
+ const map = mapRef.current;
22
+ if (!map) return;
23
+
24
+ const sosMembers = new Set(sos.filter((a) => a.state !== 'resolved').map((a) => a.member_id));
25
+ const colorOf = (gid) => groups.find((g) => g.id === gid)?.color || '#64748b';
26
+ const positioned = members.filter((m) => m.lat != null && m.lng != null);
27
+ const seen = new Set();
28
+
29
+ for (const m of positioned) {
30
+ seen.add(m.id);
31
+ const isSos = sosMembers.has(m.id);
32
+ const icon = L.divIcon({
33
+ className: 'marker-wrap',
34
+ html: `<div class="marker ${m.status} ${isSos ? 'sos' : ''}" style="--c:${colorOf(m.group_id)}"></div>`,
35
+ iconSize: [16, 16],
36
+ iconAnchor: [8, 8],
37
+ });
38
+ let mk = markersRef.current.get(m.id);
39
+ if (!mk) {
40
+ mk = L.marker([m.lat, m.lng], { icon }).addTo(map);
41
+ markersRef.current.set(m.id, mk);
42
+ } else {
43
+ mk.setLatLng([m.lat, m.lng]);
44
+ mk.setIcon(icon);
45
+ }
46
+ mk.bindPopup(
47
+ `<b>${m.name}</b>${m.callsign ? ` · ${m.callsign}` : ''}<br/>` +
48
+ `${m.group_name || 'no group'} · ${m.status}<br/>` +
49
+ `battery ${m.battery != null ? m.battery + '%' : '–'}<br/>` +
50
+ `${m.lat.toFixed(5)}, ${m.lng.toFixed(5)}`
51
+ );
52
+ }
53
+ for (const [id, mk] of markersRef.current) {
54
+ if (!seen.has(id)) {
55
+ map.removeLayer(mk);
56
+ markersRef.current.delete(id);
57
+ }
58
+ }
59
+ if (!didFit.current && positioned.length) {
60
+ didFit.current = true;
61
+ map.fitBounds(L.latLngBounds(positioned.map((m) => [m.lat, m.lng])).pad(0.3));
62
+ }
63
+ }, [members, groups, sos]);
64
+
65
+ return <div ref={elRef} className="map" />;
66
+ }