Files
LMEMappeCatastaliApp/wwwroot/js/lme-map.js
T

760 lines
25 KiB
JavaScript

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(`<strong>${escapeHtml(name)}</strong>`);
}
}).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(`<strong>${escapeHtml(label)}</strong><br>${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("<body") ? text : escapeHtml(text);
} catch {
return "";
}
}
function parseCoordinates(text) {
const parts = text.split(/[,\s;]+/).map(Number).filter(value => !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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}