function initSiteMap() { const mapNode = document.getElementById("site-map"); if (!mapNode || mapNode.dataset.ready === "true" || typeof L === "undefined") return false; mapNode.dataset.ready = "true"; const map = L.map("site-map", { zoomControl: true }).setView([20.92, 70.36], 13); const tileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); let mode = "pin"; let points = []; let markers = []; let shapeLayer = null; let pinMarker = null; let radiusCircle = null; const status = document.getElementById("map-status"); const radiusSelect = document.getElementById("radius-select"); const buttons = { polygon: document.getElementById("mode-polygon"), rectangle: document.getElementById("mode-rectangle"), pin: document.getElementById("mode-pin") }; function setStatus(text) { if (status) status.textContent = text; } let tileErrorCount = 0; let tileLoadCount = 0; tileLayer.on("tileload", () => { tileLoadCount += 1; }); tileLayer.on("tileerror", () => { tileErrorCount += 1; if (tileErrorCount === 3) { setStatus("Base map tiles are unavailable in this browser/network. Drawing still works; verify the boundary with CAD/KML/GeoJSON or coordinates."); } }); setTimeout(() => { if (tileLoadCount === 0) { setStatus("Base map tiles are unavailable in this browser/network. Drawing still works; verify the boundary with CAD/KML/GeoJSON or coordinates."); } }, 3500); function setMode(nextMode) { mode = nextMode; Object.entries(buttons).forEach(([key, btn]) => { if (btn) btn.classList.toggle("active", key === mode); }); if (mode === "polygon") setStatus("Polygon mode: click corners, then Finish."); if (mode === "rectangle") setStatus("Rectangle mode: click two opposite corners."); if (mode === "pin") setStatus("Pin mode: click site center and choose radius."); } function setGradioState(state) { const textarea = document.querySelector("#map_state textarea"); if (!textarea) return; textarea.value = JSON.stringify(state); textarea.dispatchEvent(new Event("input", { bubbles: true })); textarea.dispatchEvent(new Event("change", { bubbles: true })); } function clearDrawing(pushState = true) { markers.forEach((m) => map.removeLayer(m)); markers = []; points = []; if (shapeLayer) map.removeLayer(shapeLayer); if (pinMarker) map.removeLayer(pinMarker); if (radiusCircle) map.removeLayer(radiusCircle); shapeLayer = null; pinMarker = null; radiusCircle = null; if (pushState) setGradioState({}); setStatus("Selection cleared."); } function drawPolygonPreview(closeIt = false) { if (shapeLayer) map.removeLayer(shapeLayer); if (points.length < 2) return; const latLngs = closeIt ? [...points, points[0]] : points; shapeLayer = L.polyline(latLngs, { color: "#123b51", weight: 3 }).addTo(map); if (closeIt && points.length >= 3) { map.removeLayer(shapeLayer); shapeLayer = L.polygon(points, { color: "#123b51", fillColor: "#0f766e", fillOpacity: 0.22, weight: 3 }).addTo(map); } } function finishPolygon() { if (points.length < 3) { setStatus("A polygon needs at least three points."); return; } drawPolygonPreview(true); const ring = points.map((p) => [p.lng, p.lat]); ring.push([points[0].lng, points[0].lat]); setGradioState({ mode: "polygon", geometry: { type: "Polygon", coordinates: [ring] } }); setStatus(`Polygon finished with ${points.length} points. Drawn boundaries are approximate.`); } function finishRectangle(a, b) { clearDrawing(false); const corners = [ L.latLng(a.lat, a.lng), L.latLng(a.lat, b.lng), L.latLng(b.lat, b.lng), L.latLng(b.lat, a.lng) ]; points = corners; shapeLayer = L.polygon(corners, { color: "#123b51", fillColor: "#0f766e", fillOpacity: 0.22, weight: 3 }).addTo(map); const ring = corners.map((p) => [p.lng, p.lat]); ring.push([corners[0].lng, corners[0].lat]); setGradioState({ mode: "rectangle", geometry: { type: "Polygon", coordinates: [ring] } }); setStatus("Rectangle selection finished. Drawn boundaries are approximate."); } function placePin(latlng) { if (pinMarker) map.removeLayer(pinMarker); if (radiusCircle) map.removeLayer(radiusCircle); pinMarker = L.circleMarker(latlng, { radius: 7, color: "#123b51", fillColor: "#ffffff", fillOpacity: 1, weight: 3 }).addTo(map); const radius = Number(radiusSelect ? radiusSelect.value : 250); radiusCircle = L.circle(latlng, { radius, color: "#123b51", fillColor: "#0f766e", fillOpacity: 0.16 }).addTo(map); setGradioState({ mode: "pin_radius", lat: latlng.lat, lon: latlng.lng, radius_m: radius }); setStatus(`Pin set at ${latlng.lat.toFixed(6)}, ${latlng.lng.toFixed(6)} with ${radius} m radius.`); } map.on("click", (event) => { if (mode === "pin") { clearDrawing(false); placePin(event.latlng); return; } if (mode === "polygon") { const marker = L.circleMarker(event.latlng, { radius: 5, color: "#123b51", fillColor: "#c88920", fillOpacity: 1 }).addTo(map); markers.push(marker); points.push(event.latlng); drawPolygonPreview(false); setStatus(`Polygon point ${points.length}: ${event.latlng.lat.toFixed(6)}, ${event.latlng.lng.toFixed(6)}`); return; } if (mode === "rectangle") { points.push(event.latlng); markers.push(L.circleMarker(event.latlng, { radius: 5, color: "#123b51", fillColor: "#c88920", fillOpacity: 1 }).addTo(map)); if (points.length === 2) finishRectangle(points[0], points[1]); else setStatus("Rectangle mode: click opposite corner."); } }); if (buttons.polygon) buttons.polygon.addEventListener("click", () => { clearDrawing(); setMode("polygon"); }); if (buttons.rectangle) buttons.rectangle.addEventListener("click", () => { clearDrawing(); setMode("rectangle"); }); if (buttons.pin) buttons.pin.addEventListener("click", () => { clearDrawing(); setMode("pin"); }); const finishBtn = document.getElementById("finish-shape"); if (finishBtn) finishBtn.addEventListener("click", finishPolygon); const undoBtn = document.getElementById("undo-point"); if (undoBtn) undoBtn.addEventListener("click", () => { if (points.length > 0) { points.pop(); const marker = markers.pop(); if (marker) map.removeLayer(marker); drawPolygonPreview(false); setStatus(`Removed last point. ${points.length} points remain.`); } }); const clearBtn = document.getElementById("clear-map"); if (clearBtn) clearBtn.addEventListener("click", () => clearDrawing()); if (radiusSelect) radiusSelect.addEventListener("change", () => { if (pinMarker) placePin(pinMarker.getLatLng()); }); setMode("pin"); setTimeout(() => map.invalidateSize(), 250); return true; } let leafletFallbackRequested = false; function requestLeafletFallback() { if (leafletFallbackRequested) return; leafletFallbackRequested = true; const status = document.getElementById("map-status"); if (status) status.textContent = "Map library is taking longer than expected. Trying fallback map assets..."; const css = document.createElement("link"); css.rel = "stylesheet"; css.href = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css"; document.head.appendChild(css); const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"; script.onload = () => initSiteMap(); script.onerror = () => { if (status) { status.textContent = "Map library could not load. Use the lat/lon field or upload DXF/KML/GeoJSON, then generate the report."; } }; document.head.appendChild(script); } function waitForLeafletAndInit(attempt = 0) { const hasMapNode = Boolean(document.getElementById("site-map")); if (typeof L !== "undefined" && hasMapNode) { if (initSiteMap()) return; } if (typeof L !== "undefined" && !hasMapNode) { const status = document.getElementById("map-status"); if (status) status.textContent = "Map UI is loading..."; } if (attempt === 6 && typeof L === "undefined") requestLeafletFallback(); if (attempt > 80) { const status = document.getElementById("map-status"); if (status) { status.textContent = "Map could not initialize. Use the lat/lon field or upload DXF/KML/GeoJSON, then generate the report."; } return; } setTimeout(() => waitForLeafletAndInit(attempt + 1), 250); } waitForLeafletAndInit();