const lmeState = { map: null, baseLayers: {}, currentBase: null, currentBaseName: "satellite", cadastre: null, cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"], cadastreOpacity: 0.75, imported: null, drawings: null, drawControl: null, drawEventsBound: false, gpsMarker: null, gpsAccuracy: null, gpsTrail: null, selectedParcelsLayer: null, gpsParcelLayer: null, gpsTrailEnabled: false, gpsTrailPoints: [], gpsWatchId: null, lastPosition: null, fileImportBound: false, resizeBound: false, googleMapsLoading: null, azureMapsKey: null, azureAttribution: null }; // AdE publishes the cadastral WMS in ETRS89. WMS 1.1.1 keeps the longitude, // latitude BBOX order expected by Leaflet while sending the required CRS code. const cadastreCrs = L.extend({}, L.CRS.EPSG4326, { code: "EPSG:4258" }); window.lmeMap = { init, showSearchResult, displayOfficialParcel, exportCadastreView, closeCadastrePreview, clearSelectedParcels, clearMapElements, fitImported, clearImported, addProjectMarker, setDrawToolsVisible, clearDrawings, saveWorkspace, loadWorkspace, setBaseLayer, configureGoogleMaps, configureAzureMaps, clearAzureMaps, setCadastreOpacity, toggleCadastreLayer, startGps, stopGps, centerOnGps, toggleTrail, setNativeGpsPosition, setGpsError, resize, showInfo: setInfo }; function init() { if (lmeState.map) { return; } lmeState.map = L.map("map", { zoomControl: false }).setView([41.9028, 12.4964], 17); L.control.zoom({ position: "bottomright" }).addTo(lmeState.map); lmeState.baseLayers.osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxNativeZoom: 19, maxZoom: 22, attribution: "© OpenStreetMap contributors" }); lmeState.baseLayers.satellite = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { maxNativeZoom: 19, maxZoom: 22, attribution: "Tiles © Esri" }); lmeState.baseLayers.topographic = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}", { maxNativeZoom: 19, maxZoom: 22, attribution: "Tiles © Esri" }); lmeState.baseLayers["streets-esri"] = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}", { maxNativeZoom: 19, maxZoom: 22, attribution: "Tiles © Esri" }); lmeState.baseLayers.terrain = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", { maxNativeZoom: 17, maxZoom: 22, attribution: "Map data © OpenStreetMap contributors, SRTM | Map style © OpenTopoMap" }); lmeState.baseLayers.carto = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", { subdomains: "abcd", maxNativeZoom: 20, maxZoom: 22, attribution: "© OpenStreetMap contributors © CARTO" }); lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map); lmeState.imported = L.featureGroup().addTo(lmeState.map); lmeState.drawings = L.featureGroup().addTo(lmeState.map); lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map); lmeState.gpsParcelLayer = L.featureGroup().addTo(lmeState.map); lmeState.gpsTrail = L.polyline([], { color: "#f97316", weight: 4, opacity: 0.85 }).addTo(lmeState.map); refreshCadastreLayer(); bindFileImport(); if (!lmeState.resizeBound) { window.addEventListener("resize", resize); window.addEventListener("orientationchange", resize); lmeState.resizeBound = true; } lmeState.map.on("click", async event => { const info = await queryFeatureInfo(event.latlng); if (info) { L.popup().setLatLng(event.latlng).setContent(info).openOn(lmeState.map); } }); lmeState.map.on("moveend", () => void updateAzureAttribution()); document.getElementById("closeWmsPreview")?.addEventListener("click", closeCadastrePreview); document.getElementById("wmsPreview")?.addEventListener("click", event => { if (event.target?.id === "wmsPreview") { closeCadastrePreview(); } }); document.addEventListener("keydown", event => { if (event.key === "Escape") { closeCadastrePreview(); } }); setInfo("Mappa pronta: satellite + catasto AdE"); } function showSearchResult(latitude, longitude, label) { const lat = Number(latitude); const lon = Number(longitude); if (!Number.isFinite(lat) || !Number.isFinite(lon)) { setInfo("La ricerca ha restituito coordinate non valide."); return; } lmeState.map.setView([lat, lon], 18); setInfo(label || `${lat.toFixed(6)}, ${lon.toFixed(6)}`); } function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shouldFit) { const lat = Number(latitude); const lon = Number(longitude); if (!Number.isFinite(lat) || !Number.isFinite(lon)) { setInfo("Coordinate della particella non valide."); return; } try { const geojson = gmlParcelsToGeoJson(xmlText); const normalizedParcel = normalizeParcel(parcel); const featureByNumber = normalizedParcel ? (geojson.features || []).find(item => { const props = item.properties || {}; return String(props.LABEL || "").trim().toUpperCase() === normalizedParcel || String(props.NATIONALCADASTRALREFERENCE || "").toUpperCase().endsWith(`.${normalizedParcel}`); }) : null; const feature = featureByNumber || (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [lon, lat])) || geojson.features?.[0]; if (!feature) { if (shouldFit) { lmeState.map.setView([lat, lon], 20); } setInfo("Posizione trovata, ma il servizio WFS dell'Agenzia non ha restituito il confine. Riprova tra poco."); return; } const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || label; const targetGroup = normalizedParcel ? lmeState.selectedParcelsLayer : lmeState.gpsParcelLayer; if (!normalizedParcel) { targetGroup.clearLayers(); } const existingLayer = normalizedParcel ? targetGroup.getLayers().find(item => item.lmeParcelReference === ref) : null; if (existingLayer) { if (shouldFit) { fitLayer(existingLayer); } setText("parcelState", `Particella selezionata: ${ref}`); setInfo(`La particella ${ref} e' gia evidenziata sulla mappa.`); return; } const layer = L.geoJSON(feature, { style: normalizedParcel ? { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 } : { color: "#1d4ed8", weight: 4, opacity: 1, fillColor: "#38bdf8", fillOpacity: 0.2 } }).addTo(targetGroup); layer.lmeParcelReference = ref; layer.bindPopup(`${escapeHtml(ref)}`); if (shouldFit) { fitLayer(layer); } if (normalizedParcel) { setText("parcelState", `Particella selezionata: ${ref}`); const count = lmeState.selectedParcelsLayer.getLayers().length; setInfo(`Aggiunta ${ref}. Particelle evidenziate: ${count}.`); } else { setText("parcelState", `Dentro particella AdE: ${ref}`); } } catch (error) { setInfo(`Geometria catastale non leggibile: ${error.message}`); } } function normalizeParcel(parcel) { return String(parcel || "").trim().toUpperCase(); } function resize() { if (!lmeState.map) { return; } window.setTimeout(() => lmeState.map.invalidateSize({ pan: false }), 50); } function exportCadastreView() { const layers = activeCadastreLayers(); if (!layers.length) { setInfo("Attiva almeno un layer catastale prima di aprire la vista WMS."); return; } const bounds = lmeState.map.getBounds(); const size = lmeState.map.getSize(); const scale = Math.min(1, 1200 / size.x, 1200 / size.y); const width = Math.max(256, Math.round(size.x * scale)); const height = Math.max(256, Math.round(size.y * scale)); const bbox = [bounds.getSouth(), bounds.getWest(), bounds.getNorth(), bounds.getEast()].join(","); const params = new URLSearchParams({ service: "WMS", version: "1.3.0", request: "GetMap", layers: layers.join(","), styles: "", format: "image/png", transparent: "true", width: String(width), height: String(height), crs: "EPSG:4258", bbox }); const preview = document.getElementById("wmsPreview"); const previewBody = preview?.querySelector(".wms-preview-body"); const previewMap = document.getElementById("wmsPreviewMap"); const image = document.getElementById("wmsPreviewImage"); const overlay = document.getElementById("wmsPreviewOverlay"); const loading = document.getElementById("wmsPreviewLoading"); if (!preview || !previewBody || !previewMap || !image || !overlay || !loading) { setInfo("Vista WMS interna non disponibile."); return; } preview.hidden = false; const availableWidth = Math.max(1, previewBody.clientWidth - 24); const availableHeight = Math.max(1, previewBody.clientHeight - 24); const displayScale = Math.min(1, availableWidth / width, availableHeight / height); previewMap.style.width = `${Math.max(1, Math.round(width * displayScale))}px`; previewMap.style.height = `${Math.max(1, Math.round(height * displayScale))}px`; previewMap.hidden = true; renderPreviewParcelOverlay(overlay, bounds, width, height); loading.textContent = "Caricamento..."; loading.hidden = false; image.onload = () => { loading.hidden = true; previewMap.hidden = false; const highlightedCount = lmeState.selectedParcelsLayer.getLayers().length + lmeState.gpsParcelLayer.getLayers().length; const zoomHint = layers.includes("codice_plla") && lmeState.map.getZoom() < 17 ? " Ingrandisci almeno fino a zoom 17 per leggere i numeri delle particelle." : ""; setInfo(`Vista WMS caricata con ${layers.length} layer e ${highlightedCount} particelle evidenziate.${zoomHint}`); }; image.onerror = () => { previewMap.hidden = true; loading.textContent = "Il servizio WMS non ha restituito l'immagine. Riprova tra poco."; setInfo("Vista WMS non disponibile."); }; image.src = `${cadastreUrl()}${params}`; } function closeCadastrePreview() { const preview = document.getElementById("wmsPreview"); const previewMap = document.getElementById("wmsPreviewMap"); const image = document.getElementById("wmsPreviewImage"); const overlay = document.getElementById("wmsPreviewOverlay"); if (preview) { preview.hidden = true; } if (previewMap) { previewMap.hidden = true; } if (image) { image.onload = null; image.onerror = null; image.removeAttribute("src"); } overlay?.replaceChildren(); } function renderPreviewParcelOverlay(svg, bounds, width, height) { svg.replaceChildren(); svg.setAttribute("viewBox", `0 0 ${width} ${height}`); appendPreviewGeometry(svg, lmeState.selectedParcelsLayer.toGeoJSON(), "wms-preview-parcel", bounds, width, height); appendPreviewGeometry(svg, lmeState.gpsParcelLayer.toGeoJSON(), "wms-preview-gps-parcel", bounds, width, height); } function appendPreviewGeometry(svg, featureCollection, className, bounds, width, height) { const namespace = "http://www.w3.org/2000/svg"; for (const feature of featureCollection?.features || []) { for (const polygon of geometryPolygons(feature.geometry)) { const pathData = polygon .map(ring => ring .map((coordinate, index) => { const point = previewPixel(coordinate, bounds, width, height); return `${index ? "L" : "M"}${point.x.toFixed(2)} ${point.y.toFixed(2)}`; }) .join(" ") + " Z") .join(" "); if (!pathData.trim()) { continue; } const path = document.createElementNS(namespace, "path"); path.setAttribute("d", pathData); path.setAttribute("class", className); svg.appendChild(path); } } } function geometryPolygons(geometry) { if (!geometry) { return []; } if (geometry.type === "Polygon") { return [geometry.coordinates || []]; } if (geometry.type === "MultiPolygon") { return geometry.coordinates || []; } if (geometry.type === "GeometryCollection") { return (geometry.geometries || []).flatMap(geometryPolygons); } return []; } function previewPixel(coordinate, bounds, width, height) { const longitude = Number(coordinate?.[0]); const latitude = Number(coordinate?.[1]); return { x: ((longitude - bounds.getWest()) / (bounds.getEast() - bounds.getWest())) * width, y: ((bounds.getNorth() - latitude) / (bounds.getNorth() - bounds.getSouth())) * height }; } function bindFileImport() { if (lmeState.fileImportBound) { return; } document.addEventListener("change", async event => { if (event.target?.id !== "gisFileInput") { return; } const file = event.target.files?.[0]; if (!file) { return; } try { const geojson = await readGisFile(file); const layer = L.geoJSON(geojson, { style: { color: "#22c55e", weight: 3, opacity: 0.95, fillOpacity: 0.18 }, pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 7, color: "#22c55e", fillColor: "#ffffff", fillOpacity: 0.9 }), onEachFeature: (feature, layer) => { const name = feature?.properties?.name || file.name; layer.bindPopup(`${escapeHtml(name)}`); } }).addTo(lmeState.imported); fitLayer(layer); setInfo(`Caricato ${file.name}`); updateGpsInsideState(); } catch (error) { setInfo(`File non leggibile: ${error.message}`); } }); lmeState.fileImportBound = true; } async function readGisFile(file) { const lowerName = file.name.toLowerCase(); if (lowerName.endsWith(".geojson") || lowerName.endsWith(".json")) { return JSON.parse(await file.text()); } if (lowerName.endsWith(".kml")) { return kmlTextToGeoJson(await file.text()); } if (lowerName.endsWith(".kmz")) { const zip = await JSZip.loadAsync(await file.arrayBuffer()); const kmlName = Object.keys(zip.files).find(name => name.toLowerCase().endsWith(".kml")); if (!kmlName) { throw new Error("KMZ senza file KML interno"); } return kmlTextToGeoJson(await zip.files[kmlName].async("text")); } throw new Error("Formato supportato: KML, KMZ, GeoJSON"); } function kmlTextToGeoJson(text) { const xml = new DOMParser().parseFromString(text, "text/xml"); return toGeoJSON.kml(xml); } function fitImported() { fitLayer(lmeState.imported); } function clearImported() { lmeState.imported.clearLayers(); setInfo("File GIS rimossi."); updateGpsInsideState(); } function addProjectMarker(name) { const center = lmeState.map.getCenter(); const label = (name || "Nuovo POI").trim(); const marker = L.marker(center, { draggable: true }).addTo(lmeState.drawings); marker.bindPopup(`${escapeHtml(label)}
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}`).openPopup(); setInfo(`POI aggiunto: ${label}`); } function enableDrawTools() { if (lmeState.drawControl) { setInfo("Strumenti di disegno gia attivi."); return; } lmeState.drawControl = new L.Control.Draw({ position: "topright", edit: { featureGroup: lmeState.drawings }, draw: { circle: false, circlemarker: false, marker: true, polygon: true, polyline: true, rectangle: true } }); lmeState.map.addControl(lmeState.drawControl); if (!lmeState.drawEventsBound) { lmeState.map.on(L.Draw.Event.CREATED, event => { lmeState.drawings.addLayer(event.layer); updateGpsInsideState(); }); lmeState.drawEventsBound = true; } setInfo("Disegno attivo: crea POI, linee e aree sulla mappa."); } function setDrawToolsVisible(visible) { if (visible) { enableDrawTools(); return; } if (!lmeState.drawControl) { return; } Object.values(lmeState.drawControl._toolbars || {}).forEach(toolbar => { if (typeof toolbar.disable === "function") { toolbar.disable(); } }); lmeState.map.removeControl(lmeState.drawControl); lmeState.drawControl = null; setInfo("Strumenti di disegno nascosti."); } function clearDrawings() { const count = lmeState.drawings.getLayers().length; if (!count) { setInfo("Non ci sono disegni o POI da eliminare."); return; } if (!window.confirm(`Eliminare ${count} disegni e POI dalla mappa?`)) { return; } lmeState.drawings.clearLayers(); updateGpsInsideState(); setInfo("Disegni e POI eliminati dalla mappa."); } function saveWorkspace() { const data = { drawings: lmeState.drawings.toGeoJSON(), imported: lmeState.imported.toGeoJSON(), trail: lmeState.gpsTrailPoints }; localStorage.setItem("lmeMapWorkspace", JSON.stringify(data)); setInfo("Progetti, POI, file e percorso salvati sul dispositivo."); } function loadWorkspace() { const raw = localStorage.getItem("lmeMapWorkspace"); if (!raw) { setInfo("Nessun progetto salvato trovato."); return; } const data = JSON.parse(raw); lmeState.drawings.clearLayers(); lmeState.imported.clearLayers(); L.geoJSON(data.drawings, { style: { color: "#f97316", weight: 3, fillOpacity: 0.16 } }).addTo(lmeState.drawings); L.geoJSON(data.imported, { style: { color: "#22c55e", weight: 3, fillOpacity: 0.18 } }).addTo(lmeState.imported); lmeState.gpsTrailPoints = data.trail || []; lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints); fitLayer(L.featureGroup([lmeState.drawings, lmeState.imported])); updateGpsInsideState(); setInfo("Progetto caricato."); } function setBaseLayer(name) { const selected = lmeState.baseLayers[name] || lmeState.baseLayers.satellite; if (lmeState.currentBase) { lmeState.map.removeLayer(lmeState.currentBase); } lmeState.currentBase = selected.addTo(lmeState.map); lmeState.currentBaseName = selected === lmeState.baseLayers[name] ? name : "satellite"; lmeState.currentBase.bringToBack(); const labels = { satellite: "Satellite Esri", osm: "OpenStreetMap", "streets-esri": "Strade dettagliate Esri", topographic: "Topografica Esri", terrain: "OpenTopoMap", carto: "CARTO Voyager", "azure-road": "Strade Azure Maps", "azure-satellite": "Satellite Azure Maps", "google-satellite": "Satellite Google Maps", "google-roadmap": "Strade Google Maps" }; setInfo(`Sfondo attivo: ${labels[name] || labels.satellite}. Il catasto AdE resta sovrapposto.`); if (lmeState.currentBaseName.startsWith("azure-")) { void updateAzureAttribution(); } else { clearAzureAttribution(); } } function configureGoogleMaps(apiKey) { const key = (apiKey || "").trim(); if (!key) { return Promise.reject(new Error("Chiave Google Maps mancante.")); } if (window.google?.maps && L.gridLayer.googleMutant) { createGoogleLayers(); return Promise.resolve(); } if (lmeState.googleMapsLoading) { return lmeState.googleMapsLoading; } lmeState.googleMapsLoading = new Promise((resolve, reject) => { const callbackName = `lmeGoogleMapsReady${Date.now()}`; const script = document.createElement("script"); window[callbackName] = () => { delete window[callbackName]; createGoogleLayers(); resolve(); }; script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(key)}&loading=async&callback=${callbackName}`; script.async = true; script.onerror = () => { delete window[callbackName]; lmeState.googleMapsLoading = null; reject(new Error("Impossibile caricare Google Maps. Controlla chiave, API e connessione.")); }; document.head.appendChild(script); }); return lmeState.googleMapsLoading; } function createGoogleLayers() { if (!L.gridLayer.googleMutant) { throw new Error("Modulo Google Maps non disponibile."); } lmeState.baseLayers["google-satellite"] ||= L.gridLayer.googleMutant({ type: "hybrid", maxZoom: 22 }); lmeState.baseLayers["google-roadmap"] ||= L.gridLayer.googleMutant({ type: "roadmap", maxZoom: 22 }); } function configureAzureMaps(subscriptionKey) { const key = (subscriptionKey || "").trim(); if (!key) { throw new Error("Chiave Azure Maps mancante."); } if (lmeState.azureMapsKey === key && lmeState.baseLayers["azure-road"] && lmeState.baseLayers["azure-satellite"]) { return; } const activeAzureLayer = lmeState.currentBaseName.startsWith("azure-") ? lmeState.currentBaseName : null; if (activeAzureLayer && lmeState.currentBase) { lmeState.map.removeLayer(lmeState.currentBase); } lmeState.azureMapsKey = key; lmeState.baseLayers["azure-road"] = createAzureLayer(key, "microsoft.base.road", 22); lmeState.baseLayers["azure-satellite"] = createAzureLayer(key, "microsoft.imagery", 19); if (activeAzureLayer) { lmeState.currentBase = lmeState.baseLayers[activeAzureLayer].addTo(lmeState.map); lmeState.currentBase.bringToBack(); void updateAzureAttribution(); } } function clearAzureMaps() { if (lmeState.currentBaseName.startsWith("azure-") && lmeState.currentBase) { lmeState.map.removeLayer(lmeState.currentBase); lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map); lmeState.currentBase.bringToBack(); lmeState.currentBaseName = "satellite"; } delete lmeState.baseLayers["azure-road"]; delete lmeState.baseLayers["azure-satellite"]; lmeState.azureMapsKey = null; clearAzureAttribution(); } function createAzureLayer(key, tilesetId, maxNativeZoom) { const url = `https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=${tilesetId}&zoom={z}&x={x}&y={y}&tileSize=256&language=it-IT&view=Auto&subscription-key=${encodeURIComponent(key)}`; const layer = L.tileLayer(url, { minZoom: tilesetId === "microsoft.imagery" ? 1 : 0, maxNativeZoom, maxZoom: 22, attribution: "Microsoft Azure Maps" }); let errorShown = false; layer.on("tileerror", () => { if (!errorShown) { errorShown = true; setInfo("Azure Maps non ha restituito le tessere. Controlla quota e permessi Render della chiave."); } }); return layer; } async function updateAzureAttribution() { if (!lmeState.azureMapsKey || !lmeState.currentBaseName.startsWith("azure-")) { return; } const requestedLayer = lmeState.currentBaseName; const tilesetId = requestedLayer === "azure-satellite" ? "microsoft.imagery" : "microsoft.base.road"; const bounds = lmeState.map.getBounds(); const zoom = Math.min(lmeState.map.getZoom(), tilesetId === "microsoft.imagery" ? 19 : 22); const bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()].join(","); const url = `https://atlas.microsoft.com/map/attribution?api-version=2024-04-01&tilesetId=${tilesetId}&zoom=${zoom}&bounds=${bbox}`; try { const response = await fetch(url, { headers: { Accept: "application/json", "subscription-key": lmeState.azureMapsKey } }); if (!response.ok) { return; } const payload = await response.json(); if (lmeState.currentBaseName !== requestedLayer) { return; } const attribution = [...new Set((payload.copyrights || []).map(htmlToText).filter(Boolean))].join(" | "); setAzureAttribution(attribution); } catch { clearAzureAttribution(); } } function setAzureAttribution(text) { clearAzureAttribution(); if (!text) { return; } lmeState.azureAttribution = escapeHtml(text); lmeState.map.attributionControl.addAttribution(lmeState.azureAttribution); } function clearAzureAttribution() { if (lmeState.azureAttribution) { lmeState.map.attributionControl.removeAttribution(lmeState.azureAttribution); lmeState.azureAttribution = null; } } function htmlToText(value) { const element = document.createElement("div"); element.innerHTML = String(value || ""); return element.textContent?.trim() || ""; } function setCadastreOpacity(value) { lmeState.cadastreOpacity = Number(value || 0.75); if (lmeState.cadastre) { lmeState.cadastre.setOpacity(lmeState.cadastreOpacity); } } function toggleCadastreLayer(layer, enabled) { const exists = lmeState.cadastreLayers.includes(layer); if (enabled && !exists) { lmeState.cadastreLayers.push(layer); } if (!enabled && exists) { lmeState.cadastreLayers = lmeState.cadastreLayers.filter(item => item !== layer); } refreshCadastreLayer(); const layerNames = { "CP.CadastralParcel": "Confini particelle", codice_plla: "Numeri particelle", "CP.CadastralZoning": "Fogli catastali", fabbricati: "Fabbricati", strade: "Strade catastali", acque: "Acque" }; if (enabled && layer === "codice_plla" && lmeState.map.getZoom() < 17) { setInfo("Numeri particelle attivati. Ingrandisci la mappa almeno fino a zoom 17 per renderli visibili."); return; } setInfo(`${layerNames[layer] || layer}: ${enabled ? "attivo" : "nascosto"}.`); } function refreshCadastreLayer() { if (lmeState.cadastre) { lmeState.map.removeLayer(lmeState.cadastre); } const layers = activeCadastreLayers(); lmeState.cadastre = null; if (!layers.length) { return; } const cadastreLayer = L.tileLayer.wms(cadastreUrl(), { layers: layers.join(","), format: "image/png", transparent: true, version: "1.1.1", crs: cadastreCrs, maxZoom: 22, opacity: lmeState.cadastreOpacity, attribution: "Agenzia delle Entrate - CC BY 4.0" }); let errorShown = false; cadastreLayer.on("tileerror", () => { if (errorShown) { return; } errorShown = true; setInfo("Layer catastali non caricati dal servizio AdE. Controlla la connessione e riprova."); }); cadastreLayer.on("load", () => { errorShown = false; }); lmeState.cadastre = cadastreLayer.addTo(lmeState.map); } function activeCadastreLayers() { const drawOrder = [ "CP.CadastralZoning", "strade", "acque", "fabbricati", "CP.CadastralParcel", "codice_plla" ]; return drawOrder.filter(layer => lmeState.cadastreLayers.includes(layer)); } function clearSelectedParcels() { const count = lmeState.selectedParcelsLayer.getLayers().length; lmeState.selectedParcelsLayer.clearLayers(); updateGpsInsideState(); setInfo(count ? `${count} particelle selezionate rimosse.` : "Non ci sono particelle selezionate da rimuovere."); } function clearMapElements() { const count = lmeState.selectedParcelsLayer.getLayers().length + lmeState.gpsParcelLayer.getLayers().length + lmeState.imported.getLayers().length + lmeState.drawings.getLayers().length + lmeState.gpsTrailPoints.length; if (!count) { setInfo("La mappa non contiene elementi da pulire."); return; } if (!window.confirm("Rimuovere particelle evidenziate, file importati, disegni, POI e traccia GPS dalla mappa?")) { return; } lmeState.selectedParcelsLayer.clearLayers(); lmeState.gpsParcelLayer.clearLayers(); lmeState.imported.clearLayers(); lmeState.drawings.clearLayers(); lmeState.gpsTrailPoints = []; lmeState.gpsTrail.setLatLngs([]); updateGpsInsideState(); setInfo("Elementi della mappa rimossi. Sfondo e layer catastali restano attivi."); } function startGps() { if (!navigator.geolocation) { setInfo("Geolocalizzazione non disponibile sul dispositivo."); setText("gpsState", "GPS non disponibile"); return; } if (lmeState.gpsWatchId !== null) { setInfo("Tracking GPS gia attivo."); return; } lmeState.gpsWatchId = navigator.geolocation.watchPosition(onGpsPosition, onGpsError, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 }); setText("gpsState", "Aggancio GPS in corso..."); setInfo("Tracking GPS avviato."); } function stopGps() { if (lmeState.gpsWatchId !== null) { navigator.geolocation.clearWatch(lmeState.gpsWatchId); lmeState.gpsWatchId = null; } setText("gpsState", "GPS fermo"); setInfo("Tracking GPS fermato."); } function setNativeGpsPosition(location) { onGpsPosition({ coords: { latitude: location.latitude, longitude: location.longitude, accuracy: location.accuracy || 0, altitude: location.altitude, heading: location.course, speed: location.speed }, timestamp: location.timestamp }); } function setGpsError(error) { setText("gpsState", "GPS non disponibile"); setInfo(error || "Errore GPS."); } function centerOnGps() { if (!lmeState.lastPosition) { setInfo("Nessuna posizione GPS ancora disponibile."); return; } lmeState.map.setView(lmeState.lastPosition, Math.max(lmeState.map.getZoom(), 19)); } function toggleTrail(enabled) { lmeState.gpsTrailEnabled = enabled; if (!enabled) { setInfo("Registrazione percorso disattivata."); } } function onGpsPosition(position) { const { latitude, longitude, accuracy } = position.coords; const latlng = [latitude, longitude]; lmeState.lastPosition = latlng; if (!lmeState.gpsMarker) { lmeState.gpsMarker = L.marker(latlng, { title: "La tua posizione" }).addTo(lmeState.map); lmeState.gpsAccuracy = L.circle(latlng, { radius: accuracy, color: "#2563eb", weight: 2, fillColor: "#60a5fa", fillOpacity: 0.16 }).addTo(lmeState.map); lmeState.map.setView(latlng, 19); } else { lmeState.gpsMarker.setLatLng(latlng); lmeState.gpsAccuracy.setLatLng(latlng).setRadius(accuracy); } if (lmeState.gpsTrailEnabled) { lmeState.gpsTrailPoints.push(latlng); lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints); } setText("gpsState", `${latitude.toFixed(6)}, ${longitude.toFixed(6)} - precisione ${Math.round(accuracy)} m`); updateGpsInsideState(); } function onGpsError(error) { setText("gpsState", "GPS non autorizzato o non disponibile"); setInfo(error.message || "Errore GPS."); } function updateGpsInsideState() { if (!lmeState.lastPosition) { return; } const point = [lmeState.lastPosition[1], lmeState.lastPosition[0]]; const officialMatch = findContainingPolygon(point, lmeState.selectedParcelsLayer.toGeoJSON()) || findContainingPolygon(point, lmeState.gpsParcelLayer.toGeoJSON()); if (officialMatch) { const reference = officialMatch.properties?.NATIONALCADASTRALREFERENCE || officialMatch.properties?.INSPIREID_LOCALID || officialMatch.properties?.LABEL || "particella AdE"; setText("parcelState", `Dentro particella AdE: ${reference}`); return; } const match = findContainingPolygon(point, lmeState.imported.toGeoJSON()) || findContainingPolygon(point, lmeState.drawings.toGeoJSON()); if (match) { const name = match.properties?.name || match.properties?.Name || "area caricata/disegnata"; setText("parcelState", `Dentro: ${name}`); return; } setText( "parcelState", lmeState.selectedParcelsLayer.getLayers().length || lmeState.gpsParcelLayer.getLayers().length ? "Fuori dalla particella catastale evidenziata" : "Fuori dai poligoni caricati"); } function gmlParcelsToGeoJson(xmlText) { const xml = new DOMParser().parseFromString(xmlText, "text/xml"); const members = Array.from(xml.getElementsByTagNameNS("*", "member")); const features = members.map(member => parcelMemberToFeature(member)).filter(Boolean); return { type: "FeatureCollection", features }; } function parcelMemberToFeature(member) { const posList = member.getElementsByTagNameNS("*", "posList")[0]?.textContent?.trim(); if (!posList) { return null; } const values = posList.split(/\s+/).map(Number).filter(value => !Number.isNaN(value)); const ring = []; for (let index = 0; index < values.length; index += 2) { ring.push([values[index + 1], values[index]]); } const properties = {}; ["INSPIREID_LOCALID", "INSPIREID_NAMESPACE", "LABEL", "NATIONALCADASTRALREFERENCE", "ADMINISTRATIVEUNIT"].forEach(name => { const value = member.getElementsByTagNameNS("*", name)[0]?.textContent; if (value) { properties[name] = value; } }); return { type: "Feature", properties, geometry: { type: "Polygon", coordinates: [ring] } }; } function findContainingPolygon(point, geojson) { const features = geojson?.features || []; return features.find(feature => feature.geometry && geometryContainsPoint(feature.geometry, point)); } function geometryContainsPoint(geometry, point) { if (geometry.type === "Polygon") { return polygonContainsPoint(geometry.coordinates, point); } if (geometry.type === "MultiPolygon") { return geometry.coordinates.some(polygon => polygonContainsPoint(polygon, point)); } if (geometry.type === "GeometryCollection") { return geometry.geometries.some(item => geometryContainsPoint(item, point)); } return false; } function polygonContainsPoint(rings, point) { if (!rings.length || !ringContainsPoint(rings[0], point)) { return false; } return !rings.slice(1).some(ring => ringContainsPoint(ring, point)); } function ringContainsPoint(ring, point) { const [x, y] = point; let inside = false; for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { const xi = ring[i][0], yi = ring[i][1]; const xj = ring[j][0], yj = ring[j][1]; const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi); if (intersect) { inside = !inside; } } return inside; } async function queryFeatureInfo(latlng) { if (!lmeState.cadastreLayers.includes("CP.CadastralParcel")) { return ""; } const point = lmeState.map.latLngToContainerPoint(latlng, lmeState.map.getZoom()); const size = lmeState.map.getSize(); const bbox = lmeState.map.getBounds(); const wmsBbox = `${bbox.getSouth()},${bbox.getWest()},${bbox.getNorth()},${bbox.getEast()}`; const url = `${cadastreUrl()}service=WMS&version=1.3.0&request=GetFeatureInfo&layers=CP.CadastralParcel&query_layers=CP.CadastralParcel&styles=&crs=EPSG:4258&bbox=${wmsBbox}&width=${size.x}&height=${size.y}&i=${Math.round(point.x)}&j=${Math.round(point.y)}&info_format=text/html`; try { const response = await fetch(url); const text = await response.text(); return text.includes("", ">") .replaceAll('"', """) .replaceAll("'", "'"); }