const lmeState = { map: null, baseLayers: {}, currentBase: null, cadastre: null, cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"], cadastreOpacity: 0.75, imported: null, drawings: null, drawControl: null, gpsMarker: null, gpsAccuracy: null, gpsTrail: null, currentParcelLayer: null, gpsTrailEnabled: true, gpsTrailPoints: [], gpsWatchId: null, lastPosition: null, lastWfsLookupAt: 0, wfsLookupInFlight: false, duckDb: null, duckDbConnection: null }; window.lmeMap = { init, search, locateParcelArea, exportCadastreView, fitImported, clearImported, addProjectMarker, enableDrawTools, saveWorkspace, loadWorkspace, setBaseLayer, setCadastreOpacity, toggleCadastreLayer, startGps, stopGps, centerOnGps, toggleTrail, setNativeGpsPosition, setGpsError }; 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", { maxZoom: 22, attribution: "© OpenStreetMap contributors" }); lmeState.baseLayers.satellite = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { maxZoom: 22, attribution: "Tiles © Esri" }); lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map); lmeState.imported = L.featureGroup().addTo(lmeState.map); lmeState.drawings = L.featureGroup().addTo(lmeState.map); lmeState.currentParcelLayer = L.featureGroup().addTo(lmeState.map); lmeState.gpsTrail = L.polyline([], { color: "#f97316", weight: 4, opacity: 0.85 }).addTo(lmeState.map); refreshCadastreLayer(); bindFileImport(); lmeState.map.on("click", async event => { const info = await queryFeatureInfo(event.latlng); if (info) { L.popup().setLatLng(event.latlng).setContent(info).openOn(lmeState.map); } }); setInfo("Mappa pronta: satellite + catasto AdE"); } async function search(text) { const query = (text || "").trim(); if (!query) { setInfo("Inserisci una localita o coordinate."); return; } const coordinate = parseCoordinates(query); if (coordinate) { lmeState.map.setView(coordinate, 19); setInfo(`Coordinate: ${coordinate[0].toFixed(6)}, ${coordinate[1].toFixed(6)}`); return; } const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(query)}`, { headers: { "Accept": "application/json" } }); const results = await response.json(); if (!results.length) { setInfo("Localita non trovata."); return; } const place = results[0]; lmeState.map.setView([Number(place.lat), Number(place.lon)], 18); setInfo(place.display_name); } async function locateParcelArea(region, municipality, municipalityCode, sheet, parcel, label) { if (!sheet || !parcel || (!municipality && !municipalityCode)) { setInfo("Inserisci Comune o codice catastale, Foglio e Particella."); return; } setInfo("Cerco la particella nell'indice catastale..."); try { const match = await findParcelIndexPoint(region, municipality, municipalityCode, sheet, parcel); if (!match) { const query = [municipality, region, "Italia"].filter(Boolean).join(", "); await search(query); setInfo("Particella non trovata nell'indice. Ho aperto il Comune: usa tap/GPS per interrogare AdE."); return; } const meters = 8; const deltaLat = meters / 111320; const deltaLon = meters / (111320 * Math.cos(match.lat * Math.PI / 180)); const geojson = await fetchOfficialParcelsByBbox(match.lat - deltaLat, match.lon - deltaLon, match.lat + deltaLat, match.lon + deltaLon); const normalizedParcel = normalizeParcel(parcel); const feature = (geojson.features || []).find(item => { const props = item.properties || {}; return String(props.LABEL || "").trim().toUpperCase() === normalizedParcel.toUpperCase() || String(props.NATIONALCADASTRALREFERENCE || "").toUpperCase().endsWith(`.${normalizedParcel.toUpperCase()}`); }) || (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [match.lon, match.lat])) || geojson.features?.[0]; if (!feature) { lmeState.map.setView([match.lat, match.lon], 20); setInfo("Punto interno trovato, ma il WFS AdE non ha restituito il poligono in quel momento."); return; } lmeState.currentParcelLayer.clearLayers(); const layer = L.geoJSON(feature, { style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 } }).addTo(lmeState.currentParcelLayer); fitLayer(layer); const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || `${match.comune}_${match.foglio}.${match.particella}`; setText("parcelState", `Particella selezionata: ${ref}`); setInfo(`Trovata ${ref}. Ora puoi attivare il GPS e camminare dentro il confine evidenziato.`); } catch (error) { setInfo(`Ricerca catastale non completata: ${error.message}`); } } async function findParcelIndexPoint(region, municipality, municipalityCode, sheet, parcel) { const conn = await getDuckDbConnection(); const code = (municipalityCode || "").trim().toUpperCase(); const name = (municipality || "").trim().toUpperCase().replaceAll("'", "''"); const normalizedSheet = normalizeSheet(sheet); const normalizedParcel = normalizeParcel(parcel).replaceAll("'", "''"); let indexWhere = ""; if (code) { indexWhere = `comune = '${code.replaceAll("'", "''")}'`; } else { indexWhere = `upper(DENOMINAZIONE_IT) = '${name}'`; } const indexUrl = "https://raw.githubusercontent.com/ondata/dati_catastali/main/S_0000_ITALIA/anagrafica/index.parquet"; const indexRows = await queryDuckDb(conn, ` SELECT comune, file, DENOMINAZIONE_IT FROM '${indexUrl}' WHERE ${indexWhere} LIMIT 5 `); if (!indexRows.length) { return null; } const row = chooseMunicipalityRow(indexRows, region); const dataUrl = `https://raw.githubusercontent.com/ondata/dati_catastali/main/S_0000_ITALIA/anagrafica/${row.file}`; const parcelRows = await queryDuckDb(conn, ` SELECT INSPIREID_LOCALID, comune, foglio, particella, x, y FROM '${dataUrl}' WHERE comune = '${row.comune}' AND foglio = '${normalizedSheet}' AND upper(particella) = upper('${normalizedParcel}') LIMIT 1 `); if (!parcelRows.length) { return null; } const parcelRow = parcelRows[0]; return { inspireId: parcelRow.INSPIREID_LOCALID, comune: parcelRow.comune, foglio: parcelRow.foglio, particella: parcelRow.particella, lon: Number(parcelRow.x) / 1000000, lat: Number(parcelRow.y) / 1000000 }; } function chooseMunicipalityRow(rows, region) { const wanted = (region || "").trim().toUpperCase(); if (!wanted) { return rows[0]; } return rows.find(row => String(row.file || "").toUpperCase().includes(wanted)) || rows[0]; } async function getDuckDbConnection() { if (lmeState.duckDbConnection) { return lmeState.duckDbConnection; } const duckdb = await import("https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/+esm"); const bundles = duckdb.getJsDelivrBundles(); const bundle = await duckdb.selectBundle(bundles); const workerUrl = URL.createObjectURL(new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" })); const worker = new Worker(workerUrl); const logger = new duckdb.ConsoleLogger(); const db = new duckdb.AsyncDuckDB(logger, worker); await db.instantiate(bundle.mainModule, bundle.pthreadWorker); URL.revokeObjectURL(workerUrl); const conn = await db.connect(); try { await conn.query("INSTALL httpfs; LOAD httpfs;"); } catch { // Some DuckDB-WASM bundles already include HTTP support. } lmeState.duckDb = db; lmeState.duckDbConnection = conn; return conn; } async function queryDuckDb(conn, sql) { const table = await conn.query(sql); return table.toArray().map(row => { if (typeof row.toJSON === "function") { return row.toJSON(); } const output = {}; for (const key of Object.keys(row)) { output[key] = row[key]; } return output; }); } function normalizeSheet(sheet) { const value = String(sheet || "").trim().toUpperCase(); return /^\d+$/.test(value) ? value.padStart(4, "0") : value; } function normalizeParcel(parcel) { return String(parcel || "").trim().toUpperCase(); } function exportCadastreView() { const center = lmeState.map.getCenter(); const zoom = lmeState.map.getZoom(); const layers = lmeState.cadastreLayers.join(","); const url = `${cadastreUrl()}service=WMS&version=1.3.0&request=GetMap&layers=${encodeURIComponent(layers)}&styles=&format=image/png&transparent=true&width=1200&height=1200&crs=EPSG:4258&bbox=${center.lat - 0.01},${center.lng - 0.01},${center.lat + 0.01},${center.lng + 0.01}`; window.open(url, "_blank"); setInfo(`Vista catastale aperta per download. Zoom corrente: ${zoom}.`); } function bindFileImport() { const input = document.getElementById("gisFileInput"); if (!input) { return; } input.addEventListener("change", async event => { 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}`); } }); } 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); lmeState.map.on(L.Draw.Event.CREATED, event => { lmeState.drawings.addLayer(event.layer); updateGpsInsideState(); }); setInfo("Disegno attivo: crea POI, linee e aree sulla 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.currentBase.bringToBack(); } 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(); } function refreshCadastreLayer() { if (lmeState.cadastre) { lmeState.map.removeLayer(lmeState.cadastre); } if (!lmeState.cadastreLayers.length) { return; } lmeState.cadastre = L.tileLayer.wms(cadastreUrl(), { layers: lmeState.cadastreLayers.join(","), format: "image/png", transparent: true, version: "1.3.0", crs: L.CRS.EPSG4326, maxZoom: 22, opacity: lmeState.cadastreOpacity, attribution: "Agenzia delle Entrate - CC BY 4.0" }).addTo(lmeState.map); } 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(); updateOfficialParcelFromPoint(latitude, longitude, accuracy); } 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 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", "Fuori dai poligoni caricati"); } async function updateOfficialParcelFromPoint(latitude, longitude, accuracy) { const now = Date.now(); if (lmeState.wfsLookupInFlight || now - lmeState.lastWfsLookupAt < 2500) { return; } lmeState.wfsLookupInFlight = true; lmeState.lastWfsLookupAt = now; const meters = Math.max(2, Math.min(Number(accuracy || 5), 25)); const deltaLat = meters / 111320; const deltaLon = meters / (111320 * Math.cos(latitude * Math.PI / 180)); try { const geojson = await fetchOfficialParcelsByBbox(latitude - deltaLat, longitude - deltaLon, latitude + deltaLat, longitude + deltaLon); const feature = (geojson.features || []).find(item => geometryContainsPoint(item.geometry, [longitude, latitude])) || geojson.features?.[0]; if (!feature) { return; } lmeState.currentParcelLayer.clearLayers(); L.geoJSON(feature, { style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.24 } }).addTo(lmeState.currentParcelLayer); const ref = feature.properties?.NATIONALCADASTRALREFERENCE || feature.properties?.INSPIREID_LOCALID || feature.properties?.LABEL || "particella AdE"; setText("parcelState", `Dentro particella AdE: ${ref}`); } finally { lmeState.wfsLookupInFlight = false; } } async function fetchOfficialParcelsByBbox(south, west, north, east) { const url = `${wfsUrl()}language=ita&SERVICE=WFS&VERSION=2.0.0&TYPENAMES=CP:CadastralParcel&SRSNAME=urn:ogc:def:crs:EPSG::6706&BBOX=${south},${west},${north},${east}&REQUEST=GetFeature&COUNT=20`; const response = await fetch(url); if (!response.ok) { return { type: "FeatureCollection", features: [] }; } const xmlText = await response.text(); return gmlParcelsToGeoJson(xmlText); } 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(" !Number.isNaN(value)); if (parts.length < 2) { return null; } const [lat, lng] = parts; if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) { return [lat, lng]; } return null; } function fitLayer(layer) { try { const bounds = layer.getBounds(); if (bounds.isValid()) { lmeState.map.fitBounds(bounds.pad(0.15)); } } catch { setInfo("Nessuna geometria da inquadrare."); } } function cadastreUrl() { return "https://wms.cartografia.agenziaentrate.gov.it/inspire/wms/ows01.php?language=ita&"; } function wfsUrl() { return "https://wfs.cartografia.agenziaentrate.gov.it/inspire/wfs/owfs01.php?"; } function setInfo(text) { setText("mapInfo", text); } function setText(id, text) { const element = document.getElementById(id); if (element) { element.textContent = text; } } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); }