File size: 3,059 Bytes
025301c
 
6c972ac
025301c
6c972ac
025301c
 
 
6c972ac
025301c
 
 
6c972ac
025301c
 
 
 
 
6c972ac
025301c
 
 
 
 
 
6c972ac
025301c
 
 
 
 
 
6c972ac
025301c
6c972ac
 
 
 
025301c
 
 
 
6c972ac
025301c
 
 
 
 
 
6c972ac
 
025301c
 
 
 
 
 
 
 
 
 
 
 
 
6c972ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
025301c
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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: '&copy; <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" />;
}