/* Kart-side: vis alle automater med markører, klikk for å pinpoint posisjon. * Bruker Leaflet + OpenStreetMap (ingen API-key). * * For v1 (legacy): kun manuell pin-via-klikk. * For v2: viser live GPS-feed med "manual override"-knapp. */ // HTML-escape for Leaflet-popup: bindPopup tar HTML, og automatnavn er // tenant-styrt tekst (latent lagret XSS — H17, 2026-09-05). const mapEscHtml = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const MapPage = ({ lang, tenant, dataVersion }) => { const machines = (tenant && tenant.id !== '__all__') ? DATA.machines.filter(m => m.tenantId === tenant.id) : DATA.machines; const [selected, setSelected] = React.useState(null); const [editingMode, setEditingMode] = React.useState(false); const [error, setError] = React.useState(null); const [refreshKey, setRefreshKey] = React.useState(0); const mapRef = React.useRef(null); const markersRef = React.useRef({}); const tempMarkerRef = React.useRef(null); // Click-handleren registreres KUN én gang, så den må lese editingMode/selected // via refs — ellers fanger den den første rendringens (false/null) verdier og // klikk-for-å-plassere virker aldri. const editingModeRef = React.useRef(false); const selectedRef = React.useRef(null); editingModeRef.current = editingMode; selectedRef.current = selected; // Init kart (kun én gang) React.useEffect(() => { if (mapRef.current || !window.L) return; const map = window.L.map('jgass-map', { center: [60.5, 7.0], // Norge zoom: 6, preferCanvas: true, }); window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-bidragsytere', }).addTo(map); mapRef.current = map; // Click handler: når i edit-mode, pinpoint posisjon (leser refs, ikke stale closure) map.on('click', (e) => { if (!editingModeRef.current || !selectedRef.current) return; placeTempMarker(e.latlng.lat, e.latlng.lng); }); }, []); const placeTempMarker = (lat, lon) => { if (!mapRef.current) return; if (tempMarkerRef.current) { tempMarkerRef.current.setLatLng([lat, lon]); } else { // Touch-enheter: større markør så den kan treffes/dras med finger const tempD = window.matchMedia('(pointer: coarse)').matches ? 32 : 24; tempMarkerRef.current = window.L.marker([lat, lon], { draggable: true, icon: window.L.divIcon({ html: `
`, className: '', iconSize: [tempD, tempD], iconAnchor: [tempD / 2, tempD / 2], }), }).addTo(mapRef.current); tempMarkerRef.current.on('dragend', (e) => { const ll = e.target.getLatLng(); placeTempMarker(ll.lat, ll.lng); }); } }; // Plot markører når machines endres React.useEffect(() => { if (!mapRef.current) return; // Fjern gamle Object.values(markersRef.current).forEach(m => m.remove()); markersRef.current = {}; const bounds = []; // Touch: 28px markører (18px er for smått å treffe med finger) const mrkD = window.matchMedia('(pointer: coarse)').matches ? 28 : 18; machines.forEach(m => { if (!m.lat || !m.lon) return; const color = m.kind === 'legacy' ? '#fb923c' : m.gps_source === 'gps' ? '#10b981' : '#3b82f6'; const marker = window.L.marker([m.lat, m.lon], { icon: window.L.divIcon({ html: ``, className: '', iconSize: [mrkD, mrkD], iconAnchor: [mrkD / 2, mrkD / 2], }), title: m.location, }).addTo(mapRef.current); marker.bindPopup( `${mapEscHtml(m.location)}