secutorpro commited on
Commit
6c972ac
·
verified ·
1 Parent(s): d460a13

map with selection + trail

Browse files
Files changed (1) hide show
  1. web/src/components/MapView.jsx +30 -13
web/src/components/MapView.jsx CHANGED
@@ -1,52 +1,54 @@
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
  }
@@ -60,7 +62,22 @@ export default function MapView({ members, groups, sos }) {
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
  }
 
1
  import { useEffect, useRef } from 'react';
2
  import L from 'leaflet';
3
+ import { api } from '../api.js';
4
 
5
+ export default function MapView({ members, groups, sos, selected, onSelect }) {
6
  const elRef = useRef(null);
7
  const mapRef = useRef(null);
8
  const markersRef = useRef(new Map());
9
+ const trailRef = useRef(null);
10
  const didFit = useRef(false);
11
 
12
  useEffect(() => {
13
+ const map = L.map(elRef.current).setView([48.8566, 2.3522], 12);
14
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
15
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
16
  maxZoom: 19,
17
  }).addTo(map);
18
  mapRef.current = map;
19
+ return () => { map.remove(); mapRef.current = null; };
20
  }, []);
21
 
22
  useEffect(() => {
23
  const map = mapRef.current;
24
  if (!map) return;
25
 
26
+ const sosIds = new Set(sos.filter((a) => a.state !== 'resolved').map((a) => a.member_id));
27
  const colorOf = (gid) => groups.find((g) => g.id === gid)?.color || '#64748b';
28
  const positioned = members.filter((m) => m.lat != null && m.lng != null);
29
  const seen = new Set();
30
 
31
  for (const m of positioned) {
32
  seen.add(m.id);
33
+ const cls = `pin ${m.status === 'online' ? 'online' : 'offline'} ${sosIds.has(m.id) ? 'sos' : ''}`;
34
  const icon = L.divIcon({
35
+ className: 'pin-wrap',
36
+ html: `<div class="${cls}" style="background:${colorOf(m.group_id)}">${m.name.slice(0, 2).toUpperCase()}</div>`,
37
+ iconSize: [26, 26],
38
+ iconAnchor: [13, 13],
39
  });
40
  let mk = markersRef.current.get(m.id);
41
  if (!mk) {
42
  mk = L.marker([m.lat, m.lng], { icon }).addTo(map);
43
+ mk.on('click', () => onSelect(m.id));
44
  markersRef.current.set(m.id, mk);
45
  } else {
46
  mk.setLatLng([m.lat, m.lng]);
47
  mk.setIcon(icon);
48
  }
49
  mk.bindPopup(
50
+ `<b>${m.name}</b>${m.callsign ? ` · ${m.callsign}` : ''} · ${m.group_name || 'no group'}<br/>` +
51
+ `${m.status} · battery ${m.battery != null ? m.battery + '%' : '–'}<br/>` +
 
52
  `${m.lat.toFixed(5)}, ${m.lng.toFixed(5)}`
53
  );
54
  }
 
62
  didFit.current = true;
63
  map.fitBounds(L.latLngBounds(positioned.map((m) => [m.lat, m.lng])).pad(0.3));
64
  }
65
+ }, [members, groups, sos, onSelect]);
66
+
67
+ // GPS trail of the selected member
68
+ useEffect(() => {
69
+ const map = mapRef.current;
70
+ if (!map) return;
71
+ trailRef.current?.remove();
72
+ trailRef.current = null;
73
+ if (!selected) return;
74
+ let cancelled = false;
75
+ api.get(`/members/${selected}/trail`).then((rows) => {
76
+ if (cancelled || rows.length < 2) return;
77
+ trailRef.current = L.polyline(rows.map((r) => [r.lat, r.lng]), { color: '#4fc3f7', weight: 3, opacity: 0.7 }).addTo(map);
78
+ }).catch(() => {});
79
+ return () => { cancelled = true; };
80
+ }, [selected]);
81
 
82
  return <div ref={elRef} className="map" />;
83
  }