/* ============================================================ map.js — Leaflet map initialisation + layer management ============================================================ */ 'use strict'; // ---- Map instance + layer groups --------------------------- let MAP; let layerCenters, layerHouses, layerReports; let layerVisible = { centers: true, houses: true, reports: true }; // ---- Placement mode: 'center' | 'house' | null ------------- let placementMode = null; let tempMarker = null; // ---- Init -------------------------------------------------- function initMap() { MAP = L.map('map', { center: [-0.0236, 109.3426], // Pontianak, Kalimantan Barat zoom: 13, zoomControl: false, }); L.control.zoom({ position: 'bottomright' }).addTo(MAP); // Tile layers const osmTile = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, }); const satelliteTile = L.tileLayer( 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri', maxZoom: 19, }); osmTile.addTo(MAP); // Expose for layer switcher MAP._baseLayers = { osm: osmTile, satellite: satelliteTile }; MAP._activeBase = 'osm'; // Layer groups layerCenters = L.layerGroup().addTo(MAP); layerHouses = L.layerGroup().addTo(MAP); layerReports = L.layerGroup().addTo(MAP); // Map click → placement MAP.on('click', onMapClick); // Double-click suppression MAP.on('dblclick', (e) => e.originalEvent.preventDefault()); } // ---- Map click handler ------------------------------------- async function onMapClick(e) { if (!placementMode) return; const { lat, lng } = e.latlng; // Remove temp marker if (tempMarker) { MAP.removeLayer(tempMarker); tempMarker = null; } // Place temp crosshair marker tempMarker = L.marker([lat, lng], { icon: L.divIcon({ html: `
`, iconSize: [14, 14], iconAnchor: [7, 7], className: '', }), }).addTo(MAP); // Reverse geocode showLoading(true); const address = await reverseGeocode(lat, lng); showLoading(false); if (placementMode === 'center') { openCenterModal(null, lat, lng, address); } else if (placementMode === 'house') { openHouseModal(null, lat, lng, address); } } // ---- Reverse Geocode -------------------------------- async function reverseGeocode(lat, lng) { try { const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&accept-language=id&addressdetails=1&zoom=18`; const res = await fetch(url, { headers: { 'Accept-Language': 'id', 'User-Agent': 'WebGIS-PovertyMapping/2.0' } }); const data = await res.json(); if (data && data.display_name) { return data.display_name; } // Fallback: build address from components if (data && data.address) { const addr = data.address; const parts = []; if (addr.road) parts.push(addr.road); if (addr.house_number) parts.push('No. ' + addr.house_number); if (addr.neighbourhood) parts.push(addr.neighbourhood); if (addr.suburb) parts.push(addr.suburb); if (addr.village) parts.push(addr.village); if (addr.city || addr.town || addr.municipality) parts.push(addr.city || addr.town || addr.municipality); if (addr.state) parts.push(addr.state); if (parts.length > 0) return parts.join(', '); } return `${lat.toFixed(6)}, ${lng.toFixed(6)}`; } catch (err) { console.warn('Reverse geocoding failed:', err.message); return `${lat.toFixed(6)}, ${lng.toFixed(6)}`; } } // ---- Placement mode control -------------------------------- function setPlacementMode(mode) { placementMode = mode; const map = document.getElementById('map'); map.classList.remove('cursor-add-center', 'cursor-add-house'); if (mode === 'center') map.classList.add('cursor-add-center'); if (mode === 'house') map.classList.add('cursor-add-house'); // Highlight nav tabs document.querySelectorAll('.nav-tab').forEach(t => { t.classList.toggle('active', t.dataset.tab === (mode === 'center' ? 'centers' : mode === 'house' ? 'houses' : 'overview')); }); } function cancelPlacementMode() { placementMode = null; document.getElementById('map').classList.remove('cursor-add-center', 'cursor-add-house'); if (tempMarker) { MAP.removeLayer(tempMarker); tempMarker = null; } } // ---- Layer toggle ------------------------------------------ function toggleLayer(name) { layerVisible[name] = !layerVisible[name]; const checkbox = document.getElementById('layerCenters')?.parentElement?.querySelector('#layer' + capitalize(name)); const grp = name === 'centers' ? layerCenters : name === 'houses' ? layerHouses : layerReports; if (layerVisible[name]) { grp.addTo(MAP); } else { MAP.removeLayer(grp); } const cb = document.getElementById('layer' + capitalize(name)); if (cb) cb.checked = layerVisible[name]; } function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } // ---- Fly to point ------------------------------------------ function flyTo(lat, lng, zoom = 17) { MAP.flyTo([lat, lng], zoom, { duration: 0.8 }); } // ---- Clear + rebuild all layers ---------------------------- function renderAllLayers() { renderCenters(); renderHouses(); } // ---- Update layer count badges ----------------------------- function updateLayerCounts() { const cc = document.getElementById('layerCenterCount'); const hc = document.getElementById('layerHouseCount'); const rc = document.getElementById('layerReportCount'); if (cc) cc.textContent = State.centers.length; if (hc) hc.textContent = State.houses.length; }