const lmeState = {
map: null,
baseLayers: {},
currentBase: null,
currentBaseName: "satellite",
layerOrder: [
"gpsPosition",
"drawings",
"gpsTrail",
"gpsParcels",
"selectedParcels",
"importedSheets",
"imported",
"cadastre"
],
cadastre: null,
cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"],
cadastreOpacity: 0.75,
imported: null,
importedVisible: true,
importedSheets: null,
importedSheetsVisible: false,
importedMeasurements: null,
drawings: null,
drawingMeasurements: null,
drawControl: null,
activeDrawHandler: null,
drawEventsBound: false,
includeKmlSheetLayer: false,
showImportedMeasurements: false,
showDrawingMeasurements: true,
measurementRefreshTimer: null,
gpsMarker: null,
gpsAccuracy: null,
gpsPositionVisible: true,
hasLiveGpsFix: false,
lastAccuracy: 0,
gpsTrail: null,
selectedParcelsLayer: null,
selectedParcelsVisible: true,
gpsParcelLayer: null,
gpsTrailEnabled: false,
gpsTrailPoints: [],
gpsWatchId: null,
lastPosition: null,
fileImportBound: false,
resizeBound: false,
googleMapsLoading: null,
azureMapsKey: null,
azureAttribution: null,
managerHighlight: null,
managerHighlightedObjectId: null,
managerHighlightedCategoryKey: null,
managerHiddenObjects: new Map(),
managerHiddenObjectIds: new Set(),
managerHiddenCategories: new Set(),
managerObjectOrder: {}
};
// 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,
showInitialPosition,
displayOfficialParcel,
exportCadastreView,
closeCadastrePreview,
clearSelectedParcels,
setSelectedParcelsVisible,
clearMapElements,
fitImported,
clearImported,
setIncludeKmlSheetLayer,
setImportedLayerVisible,
setImportedSheetLayerVisible,
setLayerOrder,
refreshLayerManager,
setImportedMeasurementsVisible,
setDrawingMeasurementsVisible,
addProjectMarker,
startMeasurementDrawing,
setDrawToolsVisible,
clearDrawings,
createMapExportGeoJson,
createProjectSnapshot,
loadProjectSnapshot,
getLegacyProjectSnapshot,
clearLegacyProjectSnapshot,
setBaseLayer,
configureGoogleMaps,
configureAzureMaps,
clearAzureMaps,
setCadastreOpacity,
toggleCadastreLayer,
startGps,
stopGps,
centerOnGps,
toggleTrail,
setNativeGpsPosition,
setGpsPositionVisible,
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);
restoreLayerOrder();
Object.values(managedLayerPanes()).forEach(paneName => {
if (!lmeState.map.getPane(paneName)) {
lmeState.map.createPane(paneName);
}
});
const managerHighlightPane = lmeState.map.createPane("managerHighlightPane");
managerHighlightPane.style.zIndex = "650";
setLayerOrder(lmeState.layerOrder);
lmeState.imported = L.featureGroup().addTo(lmeState.map);
lmeState.importedSheets = L.featureGroup();
lmeState.drawings = L.featureGroup().addTo(lmeState.map);
lmeState.importedMeasurements = L.layerGroup().addTo(lmeState.map);
lmeState.drawingMeasurements = L.layerGroup().addTo(lmeState.map);
lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map);
lmeState.gpsParcelLayer = L.featureGroup().addTo(lmeState.map);
lmeState.gpsTrail = L.polyline([], {
pane: "gpsTrailPane",
color: "#f97316",
weight: 4,
opacity: 0.85
}).addTo(lmeState.map);
lmeState.managerHighlight = L.layerGroup().addTo(lmeState.map);
refreshCadastreLayer();
bindFileImport();
bindDrawingEvents();
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();
scheduleMeasurementRefresh();
});
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 showInitialPosition(latitudeValue, longitudeValue, approximate, label) {
const latitude = Number(latitudeValue);
const longitude = Number(longitudeValue);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
return;
}
const latlng = [latitude, longitude];
if (!approximate) {
lmeState.lastPosition = latlng;
}
lmeState.map.setView(latlng, approximate ? 13 : 19);
setText("gpsState", approximate
? "Posizione approssimativa dalla connessione - GPS non attivo"
: "Posizione iniziale acquisita - GPS non attivo");
setInfo(approximate
? `Mappa centrata in modo approssimativo su ${label || "questa zona"}.`
: "Mappa centrata sulla posizione attuale. GPS continuo non attivo.");
if (!approximate) {
updateGpsInsideState();
}
}
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
? findParcelLayerByReference(targetGroup, 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, {
pane: normalizedParcel ? "selectedParcelsPane" : "gpsParcelsPane",
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}`);
}
refreshLayerManager();
} catch (error) {
setInfo(`Geometria catastale non leggibile: ${error.message}`);
}
}
function normalizeParcel(parcel) {
return String(parcel || "").trim().toUpperCase();
}
function findParcelLayerByReference(group, reference) {
let match = null;
group.eachLayer(layer => {
if (match) {
return;
}
if (layer.lmeParcelReference === reference) {
match = layer;
return;
}
if (typeof layer.eachLayer === "function") {
match = findParcelLayerByReference(layer, reference);
}
});
return match;
}
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 result = await readGisFile(file);
const geojson = result.geojson;
const parcelLayer = addImportedFeatures(geojson);
const sheetGeojson = result.sheetGeojson || emptyFeatureCollection();
const sheetLayer = addImportedSheetFeatures(sheetGeojson);
applyImportedSheetVisibility();
fitLayer(geojson.features.length ? parcelLayer : sheetLayer);
refreshImportedMeasurements();
const featureCount = geojson.features.length;
const sheetCount = sheetGeojson.features.length;
const ignored = result.ignoredCount
? `; esclusi ${result.ignoredCount} elementi del foglio o non particella`
: "";
const includedSheets = sheetCount
? `; ${sheetCount} elementi foglio ${lmeState.importedSheetsVisible ? "visibili" : "nascosti"}`
: "";
const reprojected = result.reprojected ? "; coordinate Web Mercator convertite" : "";
setInfo(`Caricato ${file.name}: ${featureCount} particelle/elementi${includedSheets}${ignored}${reprojected}.`);
updateGpsInsideState();
refreshLayerManager();
} catch (error) {
setInfo(`File non leggibile: ${error.message}`);
} finally {
event.target.value = "";
}
});
lmeState.fileImportBound = true;
}
async function readGisFile(file) {
const lowerName = file.name.toLowerCase();
if (lowerName.endsWith(".geojson") || lowerName.endsWith(".json")) {
const text = (await readFileText(file)).replace(/^\uFEFF/, "").trim();
if (!text) {
throw new Error("GeoJSON vuoto");
}
let parsed;
try {
parsed = JSON.parse(text);
if (typeof parsed === "string") {
parsed = JSON.parse(parsed);
}
} catch (error) {
throw new Error(`GeoJSON non valido: ${error.message}`);
}
return normalizeGeoJson(parsed, file.name);
}
if (lowerName.endsWith(".kml")) {
return normalizeKmlFiles([{ name: file.name, text: await readFileText(file) }]);
}
if (lowerName.endsWith(".kmz")) {
const zip = await JSZip.loadAsync(await file.arrayBuffer());
const kmlEntries = Object.values(zip.files).filter(entry =>
!entry.dir
&& !entry.name.startsWith("__MACOSX/")
&& entry.name.toLowerCase().endsWith(".kml"));
if (!kmlEntries.length) {
throw new Error("KMZ senza file KML interno");
}
const files = await Promise.all(kmlEntries.map(async entry => ({
name: entry.name,
text: await entry.async("text")
})));
return normalizeKmlFiles(files);
}
throw new Error("Formato supportato: KML, KMZ, GeoJSON");
}
function readFileText(file) {
if (typeof file.text === "function") {
return file.text();
}
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ""));
reader.onerror = () => reject(new Error("lettura del file non riuscita"));
reader.readAsText(file);
});
}
function normalizeGeoJson(value, fileName) {
let root;
if (Array.isArray(value)) {
root = {
type: "FeatureCollection",
features: value.map(item => item?.type === "Feature"
? item
: { type: "Feature", properties: {}, geometry: item })
};
} else if (value?.type === "FeatureCollection") {
root = value;
} else if (value?.type === "Feature") {
root = { type: "FeatureCollection", features: [value], crs: value.crs };
} else if (isGeoJsonGeometry(value)) {
root = {
type: "FeatureCollection",
features: [{ type: "Feature", properties: {}, geometry: value }],
crs: value.crs
};
} else {
throw new Error("il contenuto non e' un Feature, una geometria o una FeatureCollection");
}
root.features = (root.features || []).filter(feature => feature?.type === "Feature" && feature.geometry);
if (!root.features.length) {
throw new Error("nessuna geometria presente nel GeoJSON");
}
root.features.forEach(feature => {
feature.properties = feature.properties && typeof feature.properties === "object"
? feature.properties
: {};
feature.properties._lmeSource = feature.properties._lmeSource || fileName;
});
const crsText = root.crs ? JSON.stringify(root.crs).toUpperCase() : "";
const coordinates = collectGeoJsonCoordinates(root);
const hasProjectedCoordinates = coordinates.some(point => Math.abs(point[0]) > 180 || Math.abs(point[1]) > 90);
const webMercator = /EPSG[^0-9]*(3857|900913)/.test(crsText)
|| (!crsText && hasProjectedCoordinates && coordinates.every(point =>
Math.abs(point[0]) <= 20037508.35 && Math.abs(point[1]) <= 20048966.1));
if (webMercator) {
root.features.forEach(feature => {
feature.geometry = transformGeoJsonGeometry(feature.geometry, webMercatorToWgs84);
});
} else if (crsText && !/(4326|4258|CRS84|CRS:84)/.test(crsText)) {
throw new Error(`sistema di coordinate non supportato (${crsText.slice(0, 80)})`);
}
const normalizedCoordinates = collectGeoJsonCoordinates(root);
if (!normalizedCoordinates.length || normalizedCoordinates.some(point =>
!Number.isFinite(point[0])
|| !Number.isFinite(point[1])
|| Math.abs(point[0]) > 180
|| Math.abs(point[1]) > 90)) {
throw new Error("coordinate non valide o non espresse in WGS84/EPSG:4326");
}
delete root.crs;
return {
geojson: root,
sheetGeojson: emptyFeatureCollection(),
ignoredCount: 0,
reprojected: webMercator
};
}
function isGeoJsonGeometry(value) {
return [
"Point",
"MultiPoint",
"LineString",
"MultiLineString",
"Polygon",
"MultiPolygon",
"GeometryCollection"
].includes(value?.type);
}
function collectGeoJsonCoordinates(geojson) {
const points = [];
const visitGeometry = geometry => {
if (!geometry) {
return;
}
if (geometry.type === "GeometryCollection") {
(geometry.geometries || []).forEach(visitGeometry);
return;
}
const visit = coordinates => {
if (Array.isArray(coordinates)
&& coordinates.length >= 2
&& typeof coordinates[0] === "number"
&& typeof coordinates[1] === "number") {
points.push(coordinates);
return;
}
if (Array.isArray(coordinates)) {
coordinates.forEach(visit);
}
};
visit(geometry.coordinates);
};
(geojson.features || []).forEach(feature => visitGeometry(feature.geometry));
return points;
}
function transformGeoJsonGeometry(geometry, transform) {
if (!geometry) {
return geometry;
}
if (geometry.type === "GeometryCollection") {
return {
...geometry,
geometries: (geometry.geometries || []).map(item => transformGeoJsonGeometry(item, transform))
};
}
const visit = coordinates => {
if (Array.isArray(coordinates)
&& coordinates.length >= 2
&& typeof coordinates[0] === "number"
&& typeof coordinates[1] === "number") {
return transform(coordinates);
}
return Array.isArray(coordinates) ? coordinates.map(visit) : coordinates;
};
return { ...geometry, coordinates: visit(geometry.coordinates) };
}
function webMercatorToWgs84(coordinate) {
const longitude = coordinate[0] * 180 / 20037508.34;
const latitudeRadians = Math.PI / 2 - 2 * Math.atan(Math.exp(-coordinate[1] / 6378137));
return [longitude, latitudeRadians * 180 / Math.PI, ...coordinate.slice(2)];
}
function normalizeKmlFiles(files) {
const features = files.flatMap(file => kmlTextToGeoJson(file.text, file.name));
if (!features.length) {
throw new Error("KML senza geometrie utilizzabili");
}
const polygonParts = splitKmlPolygonFeatures(features);
const parcels = filterKmlParcelFeatures(polygonParts);
const parcelSet = new Set(parcels);
const sheetParts = polygonParts.filter(feature => !parcelSet.has(feature));
const nonPolygonFeatures = features.filter(feature => geometryPolygons(feature.geometry).length === 0);
const optionalFeatures = [...sheetParts, ...nonPolygonFeatures];
if (!parcels.length && !(lmeState.includeKmlSheetLayer && optionalFeatures.length)) {
throw new Error("nessuna particella trovata; attiva 'includi layer del foglio' per importare gli altri elementi");
}
return {
geojson: { type: "FeatureCollection", features: parcels },
sheetGeojson: {
type: "FeatureCollection",
features: lmeState.includeKmlSheetLayer ? optionalFeatures : []
},
ignoredCount: lmeState.includeKmlSheetLayer ? 0 : optionalFeatures.length,
reprojected: false
};
}
function kmlTextToGeoJson(text, sourceName) {
const xml = new DOMParser().parseFromString(text, "text/xml");
if (xml.getElementsByTagName("parsererror").length) {
throw new Error(`KML non valido (${sourceName})`);
}
const tree = toGeoJSON.kmlWithFolders(xml, { skipNullGeometry: true });
const features = [];
const visit = (node, folders) => {
if (node?.type === "Feature") {
node.properties = {
...(node.properties || {}),
_lmeFolder: folders.filter(Boolean).join(" / "),
_lmeSource: sourceName
};
features.push(node);
return;
}
const nextFolders = node?.type === "folder"
? [...folders, node.meta?.name || ""]
: folders;
(node?.children || []).forEach(child => visit(child, nextFolders));
};
visit(tree, []);
return features;
}
function filterKmlParcelFeatures(features) {
const filtered = features.filter(feature => {
if (isSheetOrOverlayFeature(feature)) {
return false;
}
const properties = feature.properties || {};
const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const identity = [name, properties.layer, properties.Layer, properties.type, properties.Tipo]
.filter(Boolean)
.join(" ")
.toLowerCase();
const folder = String(properties._lmeFolder || "").toLowerCase();
const context = `${identity} ${folder}`;
const hasParcelNumber = /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
const isSheet = /\bfoglio\b|\bsheet\b|quadro[ _-]?d[' ]?unione|(?:perimetro|bordo|limite|contorno)[ _-]?(?:del[ _-]?)?foglio|sheet[ _-]?boundary/.test(context);
return !isSheet || hasParcelNumber;
});
return removeLikelySheetOutlines(filtered);
}
function isSheetOrOverlayFeature(feature) {
const properties = feature?.properties || {};
const geometryType = String(properties["@geometry-type"] || "").toLowerCase();
const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const context = [
name,
properties.layer,
properties.Layer,
properties.type,
properties.Tipo,
properties._lmeFolder
].filter(Boolean).join(" ").toLowerCase();
if (geometryType === "groundoverlay" || /^doc[_ -]?\d+$/i.test(name)) {
return true;
}
const hasParcelNumber = /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
return !hasParcelNumber
&& /\bfoglio\b|\bsheet\b|quadro[ _-]?d[' ]?unione|(?:perimetro|bordo|limite|contorno)[ _-]?(?:del[ _-]?)?foglio|sheet[ _-]?boundary/.test(context);
}
function splitKmlPolygonFeatures(features) {
const result = [];
const addGeometry = (feature, geometry, partIndex) => {
if (!geometry) {
return;
}
if (geometry.type === "Polygon") {
result.push({
...feature,
properties: { ...(feature.properties || {}), _lmePart: partIndex },
geometry
});
return;
}
if (geometry.type === "MultiPolygon") {
(geometry.coordinates || []).forEach((coordinates, index) => addGeometry(
feature,
{ type: "Polygon", coordinates },
`${partIndex}.${index}`));
return;
}
if (geometry.type === "GeometryCollection") {
(geometry.geometries || []).forEach((item, index) => addGeometry(feature, item, `${partIndex}.${index}`));
}
};
features.forEach((feature, index) => addGeometry(feature, feature.geometry, index));
return result;
}
function removeLikelySheetOutlines(features) {
let remaining = [...features];
while (remaining.length >= 2) {
const candidates = remaining
.map(feature => ({ feature, bounds: geoJsonBounds(feature.geometry) }))
.filter(item => item.bounds);
if (candidates.length < 2) {
break;
}
const largest = candidates.reduce((current, item) =>
boundsArea(item.bounds) > boundsArea(current.bounds) ? item : current);
if (isStrongParcelFeature(largest.feature)) {
break;
}
const others = candidates.filter(item => item !== largest);
const comparisonAreas = others.map(item => boundsArea(item.bounds)).sort((a, b) => a - b);
const referenceArea = comparisonAreas[Math.floor(comparisonAreas.length / 2)];
const contained = others.filter(item => boundsContains(largest.bounds, boundsCenter(item.bounds))).length;
const minimumContained = candidates.length === 2 ? 1 : Math.ceil(others.length * 0.6);
const areaFactor = 2.5;
const looksLikeSheet = referenceArea > 0
&& boundsArea(largest.bounds) > referenceArea * areaFactor
&& contained >= minimumContained;
if (!looksLikeSheet) {
break;
}
remaining = remaining.filter(feature => feature !== largest.feature);
}
return remaining;
}
function isStrongParcelFeature(feature) {
const properties = feature?.properties || {};
const name = String(properties.name || properties.Name || properties.LABEL || "").trim();
const context = [name, properties.layer, properties.Layer, properties.type, properties.Tipo, properties._lmeFolder]
.filter(Boolean)
.join(" ");
return /particell|mappal|parcel/i.test(context)
|| /^[a-z]?\s*\d+[a-z]?(?:[\/. -]\d+[a-z]?)?$/i.test(name);
}
function geoJsonBounds(geometry) {
const wrapper = { features: [{ geometry }] };
const points = collectGeoJsonCoordinates(wrapper);
if (!points.length) {
return null;
}
return points.reduce((bounds, point) => ({
west: Math.min(bounds.west, point[0]),
south: Math.min(bounds.south, point[1]),
east: Math.max(bounds.east, point[0]),
north: Math.max(bounds.north, point[1])
}), { west: Infinity, south: Infinity, east: -Infinity, north: -Infinity });
}
function boundsArea(bounds) {
return Math.max(0, bounds.east - bounds.west) * Math.max(0, bounds.north - bounds.south);
}
function boundsCenter(bounds) {
return [(bounds.west + bounds.east) / 2, (bounds.south + bounds.north) / 2];
}
function boundsContains(bounds, point) {
return point[0] >= bounds.west && point[0] <= bounds.east
&& point[1] >= bounds.south && point[1] <= bounds.north;
}
function setIncludeKmlSheetLayer(enabled) {
lmeState.includeKmlSheetLayer = Boolean(enabled);
setInfo(lmeState.includeKmlSheetLayer
? "Importazione KML/KMZ: verranno inclusi anche foglio e altri elementi."
: "Importazione KML/KMZ: verranno caricate solo le particelle riconosciute.");
}
function setImportedLayerVisible(enabled) {
lmeState.importedVisible = Boolean(enabled);
if (!lmeState.map || !lmeState.imported) {
return;
}
if (lmeState.importedVisible) {
if (!lmeState.map.hasLayer(lmeState.imported)) {
lmeState.imported.addTo(lmeState.map);
}
lmeState.imported.bringToFront();
const count = lmeState.imported.toGeoJSON().features?.length || 0;
setInfo(count
? `${count} particelle o geometrie importate visibili.`
: "Nessun dato importato da mostrare.");
return;
}
if (lmeState.map.hasLayer(lmeState.imported)) {
lmeState.map.removeLayer(lmeState.imported);
}
setInfo("Dati importati nascosti; restano disponibili nel file o progetto.");
}
function managedLayerPanes() {
return {
gpsPosition: "gpsPositionPane",
drawings: "drawingsPane",
gpsTrail: "gpsTrailPane",
gpsParcels: "gpsParcelsPane",
selectedParcels: "selectedParcelsPane",
importedSheets: "importedSheetPane",
imported: "importedPane",
cadastre: "cadastrePane"
};
}
function setLayerOrder(order) {
const panes = managedLayerPanes();
const requested = Array.isArray(order) ? order.filter(key => panes[key]) : [];
const normalized = [...new Set([...requested, ...Object.keys(panes)])];
lmeState.layerOrder = normalized;
if (!lmeState.map) {
return;
}
normalized.forEach((key, index) => {
const pane = lmeState.map.getPane(panes[key]);
if (pane) {
pane.style.zIndex = String(590 - index * 20);
}
});
try {
localStorage.setItem("lmeMapLayerOrder", JSON.stringify(normalized));
} catch {
}
}
function restoreLayerOrder() {
try {
const saved = JSON.parse(localStorage.getItem("lmeMapLayerOrder") || "null");
if (Array.isArray(saved)) {
lmeState.layerOrder = saved;
}
} catch {
}
}
function saveLayerManagerState() {
// Object order and visibility are serialized in the project snapshot.
}
function applyStoredLayerManagerVisibility() {
layerManagerCategories().forEach(category => {
if (lmeState.managerHiddenCategories.has(category.key)) {
category.roots.forEach(layer => {
if (layer && lmeState.map.hasLayer(layer)) {
lmeState.map.removeLayer(layer);
}
});
}
managedRecordsForCategory(category).forEach(record => {
if (lmeState.managerHiddenObjectIds.has(record.id)
&& !lmeState.managerHiddenObjects.has(record.id)) {
lmeState.managerHiddenObjects.set(record.id, record);
record.parent.removeLayer(record.layer);
}
});
});
}
function managedGeoJsonLabel(geojson, fallback) {
const features = geojson?.features || [];
const source = features.map(feature => feature?.properties?._lmeSource).find(Boolean);
return `${source || fallback} (${features.length})`;
}
function drawingLayerLabel(layer, layerType) {
const properties = layer?.feature?.properties || {};
if (properties.name) {
return `POI: ${properties.name}`;
}
const shape = layerType || properties._lmeShape || "disegno";
const labels = {
polygon: "Area",
rectangle: "Rettangolo",
polyline: "Linea",
circle: "Cerchio",
marker: "POI"
};
return labels[shape] || "Disegno";
}
function layerManagerCategories() {
return [
{ key: "gpsPosition", label: "Posizione GPS", roots: [lmeState.gpsMarker, lmeState.gpsAccuracy].filter(Boolean) },
{ key: "drawings", label: "Disegni e POI", roots: [lmeState.drawings].filter(Boolean) },
{ key: "gpsTrail", label: "Traccia GPS", roots: [lmeState.gpsTrail].filter(Boolean) },
{ key: "gpsParcels", label: "Particella rilevata dal GPS", roots: [lmeState.gpsParcelLayer].filter(Boolean) },
{ key: "selectedParcels", label: "Particelle catastali evidenziate", roots: [lmeState.selectedParcelsLayer].filter(Boolean) },
{ key: "importedSheets", label: "Cornici e fogli importati", roots: [lmeState.importedSheets].filter(Boolean), blocks: true },
{ key: "imported", label: "Dati importati", roots: [lmeState.imported].filter(Boolean), blocks: true },
{ key: "cadastre", label: "Layer catastali AdE", roots: [lmeState.cadastre].filter(Boolean) }
];
}
function managedLayerId(layer, categoryKey) {
const properties = layer?.feature?.properties;
if (properties) {
properties._lmeManagerId ||= `${categoryKey}-${L.stamp(layer)}`;
return properties._lmeManagerId;
}
layer.lmeManagerId ||= `${categoryKey}-${L.stamp(layer)}`;
return layer.lmeManagerId;
}
function collectManagedLeafLayers(layer, categoryKey, parent, records) {
if (!layer) {
return;
}
if (typeof layer.getLayers === "function") {
const children = layer.getLayers();
children.forEach(child => collectManagedLeafLayers(child, categoryKey, layer, records));
return;
}
if (layer instanceof L.Polyline && !lineLatLngGroups(layer.getLatLngs()).some(line => line.length)) {
return;
}
const id = managedLayerId(layer, categoryKey);
records.push({
id,
categoryKey,
layer,
parent,
source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente")
});
}
function managedRecordsForCategory(category) {
const records = [];
category.roots.forEach(root => collectManagedLeafLayers(root, category.key, lmeState.map, records));
lmeState.managerHiddenObjects.forEach(record => {
if (record.categoryKey === category.key && !records.some(item => item.id === record.id)) {
records.push(record);
}
});
return records;
}
function managedObjectLabel(record, index) {
const layer = record.layer;
const properties = layer?.feature?.properties || {};
const reference = properties.NATIONALCADASTRALREFERENCE
|| properties.INSPIREID_LOCALID
|| properties.LABEL
|| properties.name
|| properties.Name;
if (reference) {
return String(reference);
}
if (layer.lmeManagerLabel) {
return layer.lmeManagerLabel;
}
if (layer instanceof L.Circle) {
return `Cerchio ${index + 1}`;
}
if (layer instanceof L.Polygon) {
return `Area/particella ${index + 1}`;
}
if (layer instanceof L.Polyline) {
return `Linea ${index + 1}`;
}
if (layer instanceof L.Marker) {
return `POI ${index + 1}`;
}
return `Oggetto ${index + 1}`;
}
function managedObjectDetails(layer) {
const details = [];
const properties = layer?.feature?.properties || {};
if (properties.name || properties.Name) {
details.push(`Nome: ${properties.name || properties.Name}`);
}
if (layer instanceof L.Circle) {
const radius = layer.getRadius();
details.push(`Raggio: ${formatDistance(radius)}`);
details.push(`Superficie: ${formatArea(Math.PI * radius * radius)}`);
details.push(`Circonferenza: ${formatDistance(2 * Math.PI * radius)}`);
} else if (layer instanceof L.Polygon) {
polygonLatLngGroups(layer.getLatLngs()).forEach((polygon, polygonIndex) => {
const outer = polygon[0] || [];
const area = polygon.reduce((sum, ring, ringIndex) => {
const ringArea = Math.abs(L.GeometryUtil.geodesicArea(ring));
return sum + (ringIndex === 0 ? ringArea : -ringArea);
}, 0);
details.push(`${polygonIndex ? `Area ${polygonIndex + 1}` : "Superficie"}: ${formatArea(Math.abs(area))}`);
details.push(`${polygonIndex ? `Perimetro ${polygonIndex + 1}` : "Perimetro"}: ${formatDistance(latLngPathLength(outer, true))}`);
outer.forEach((point, index) => {
const next = outer[(index + 1) % outer.length];
if (next && !point.equals(next)) {
details.push(`Lato ${index + 1}: ${formatDistance(lmeState.map.distance(point, next))}`);
}
});
});
} else if (layer instanceof L.Polyline) {
const lines = lineLatLngGroups(layer.getLatLngs());
details.push(`Lunghezza: ${formatDistance(lines.reduce((sum, line) => sum + latLngPathLength(line, false), 0))}`);
} else if (typeof layer.getLatLng === "function") {
const point = layer.getLatLng();
details.push(`Coordinate: ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}`);
}
return details;
}
function refreshLayerManager() {
const container = document.getElementById("layerManagerTree");
if (!container || !lmeState.map) {
return;
}
applyStoredLayerManagerVisibility();
container.innerHTML = "";
lmeState.managerRecords = new Map();
const categoriesByKey = new Map(layerManagerCategories().map(category => [category.key, category]));
lmeState.layerOrder.forEach(categoryKey => {
const category = categoriesByKey.get(categoryKey);
if (!category) {
return;
}
const records = managedRecordsForCategory(category);
const savedOrder = lmeState.managerObjectOrder[category.key] || [];
const positions = new Map(savedOrder.map((id, index) => [id, index]));
records.sort((left, right) =>
(positions.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (positions.get(right.id) ?? Number.MAX_SAFE_INTEGER));
records.forEach(record => lmeState.managerRecords.set(record.id, record));
container.appendChild(renderLayerManagerCategory(category, records));
});
applyManagedVisualOrder(container);
bindLayerManagerDrag(container);
bindLayerManagerMoveButtons(container);
}
function renderLayerManagerCategory(category, records) {
const group = document.createElement("details");
group.className = "layer-manager-group";
group.dataset.dragType = "category";
group.dataset.categoryKey = category.key;
const visibleCount = records.filter(record => !lmeState.managerHiddenObjectIds.has(record.id)).length;
const categoryVisible = !lmeState.managerHiddenCategories.has(category.key)
&& (!category.roots.length || category.roots.some(layer => lmeState.map.hasLayer(layer)));
const visible = categoryVisible;
group.innerHTML = `${escapeHtml(category.label)}${layerManagerMoveButtons("gruppo")}${records.length}
`;
const checkbox = group.querySelector("input");
checkbox.indeterminate = categoryVisible && records.length > 0 && visibleCount < records.length;
checkbox.addEventListener("click", event => event.stopPropagation());
checkbox.addEventListener("change", () => setManagedCategoryVisible(category, checkbox.checked));
const children = group.querySelector(".layer-manager-children");
if (category.blocks) {
const blocks = new Map();
records.forEach(record => {
const list = blocks.get(record.source) || [];
list.push(record);
blocks.set(record.source, list);
});
blocks.forEach((blockRecords, source) => children.appendChild(renderLayerManagerBlock(category, source, blockRecords)));
} else {
records.forEach((record, index) => children.appendChild(renderLayerManagerObject(record, index)));
}
return group;
}
function renderLayerManagerBlock(category, source, records) {
const block = document.createElement("details");
block.className = "layer-manager-block";
block.dataset.dragType = "block";
block.dataset.categoryKey = category.key;
block.dataset.blockKey = source;
const visibleCount = records.filter(record => !lmeState.managerHiddenObjectIds.has(record.id)).length;
const visible = visibleCount === records.length;
block.innerHTML = `${escapeHtml(source)}${layerManagerMoveButtons("blocco")}${records.length}`;
const checkbox = block.querySelector("input");
checkbox.indeterminate = visibleCount > 0 && !visible;
checkbox.addEventListener("click", event => event.stopPropagation());
checkbox.addEventListener("change", () => {
records.forEach(record => setManagedObjectVisible(record, checkbox.checked, false));
refreshLayerManager();
});
const children = block.querySelector(".layer-manager-children");
records.forEach((record, index) => children.appendChild(renderLayerManagerObject(record, index)));
return block;
}
function renderLayerManagerObject(record, index) {
const object = document.createElement("details");
object.className = "layer-manager-object";
object.dataset.dragType = "object";
object.dataset.objectId = record.id;
const visible = !lmeState.managerHiddenObjectIds.has(record.id);
object.innerHTML = `${escapeHtml(managedObjectLabel(record, index))}${layerManagerMoveButtons("oggetto")}›`;
const checkbox = object.querySelector("input");
checkbox.addEventListener("click", event => event.stopPropagation());
checkbox.addEventListener("change", () => setManagedObjectVisible(record, checkbox.checked, true));
object.querySelector(".layer-manager-title").addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
object.open = true;
highlightManagedObject(record);
});
const details = object.querySelector(".layer-manager-details");
const rows = managedObjectDetails(record.layer);
details.innerHTML = rows.length
? rows.map(value => `${escapeHtml(value)}`).join("")
: "Nessuna misura disponibile";
return object;
}
function setManagedCategoryVisible(category, visible) {
if (category.key === "imported") {
lmeState.importedVisible = visible;
} else if (category.key === "importedSheets") {
lmeState.importedSheetsVisible = visible;
} else if (category.key === "selectedParcels") {
lmeState.selectedParcelsVisible = visible;
} else if (category.key === "gpsPosition") {
lmeState.gpsPositionVisible = visible;
}
if (visible) {
lmeState.managerHiddenCategories.delete(category.key);
category.roots.forEach(layer => {
const objectId = managedLayerId(layer, category.key);
if (layer
&& !lmeState.managerHiddenObjectIds.has(objectId)
&& !lmeState.map.hasLayer(layer)) {
layer.addTo(lmeState.map);
}
});
} else {
lmeState.managerHiddenCategories.add(category.key);
category.roots.forEach(layer => {
if (layer && lmeState.map.hasLayer(layer)) {
lmeState.map.removeLayer(layer);
}
});
if (lmeState.managerHighlightedCategoryKey === category.key) {
clearManagerHighlight();
}
}
saveLayerManagerState();
refreshImportedMeasurements();
refreshDrawingMeasurements();
refreshLayerManager();
}
function setManagedObjectVisible(record, visible, refresh = true) {
if (visible) {
const hidden = lmeState.managerHiddenObjects.get(record.id);
if (hidden) {
if (hidden.parent !== lmeState.map || managedCategoryEnabled(record.categoryKey)) {
hidden.parent.addLayer(hidden.layer);
}
lmeState.managerHiddenObjects.delete(record.id);
}
lmeState.managerHiddenObjectIds.delete(record.id);
} else if (!lmeState.managerHiddenObjects.has(record.id)) {
lmeState.managerHiddenObjects.set(record.id, record);
lmeState.managerHiddenObjectIds.add(record.id);
record.parent.removeLayer(record.layer);
if (lmeState.managerHighlightedObjectId === record.id) {
clearManagerHighlight();
}
}
saveLayerManagerState();
refreshImportedMeasurements();
refreshDrawingMeasurements();
if (refresh) {
refreshLayerManager();
}
}
function managedCategoryEnabled(categoryKey) {
if (lmeState.managerHiddenCategories.has(categoryKey)) {
return false;
}
if (categoryKey === "gpsPosition") {
return lmeState.gpsPositionVisible;
}
if (categoryKey === "imported") {
return lmeState.importedVisible;
}
if (categoryKey === "importedSheets") {
return lmeState.importedSheetsVisible;
}
if (categoryKey === "selectedParcels") {
return lmeState.selectedParcelsVisible;
}
return true;
}
function highlightManagedObject(record) {
lmeState.managerHighlight.clearLayers();
lmeState.managerHighlightedObjectId = record.id;
lmeState.managerHighlightedCategoryKey = record.categoryKey;
const geojson = record.layer.toGeoJSON?.();
if (geojson) {
L.geoJSON(geojson, {
pane: "managerHighlightPane",
interactive: false,
style: { color: "#ff00d4", weight: 8, opacity: 1, fillColor: "#00fff0", fillOpacity: 0.16 },
pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
pane: "managerHighlightPane",
radius: 12,
color: "#ff00d4",
weight: 5,
fillColor: "#00fff0",
fillOpacity: 0.9,
interactive: false
})
}).addTo(lmeState.managerHighlight);
}
fitLayer(record.layer);
}
function clearManagerHighlight() {
lmeState.managerHighlight?.clearLayers();
lmeState.managerHighlightedObjectId = null;
lmeState.managerHighlightedCategoryKey = null;
}
function layerManagerMoveButtons(label) {
return ``;
}
function bindLayerManagerDrag(container) {
container.querySelectorAll(".layer-drag-handle").forEach(handle => {
handle.addEventListener("pointerdown", event => {
event.preventDefault();
event.stopPropagation();
const row = handle.closest("[data-drag-type]");
const list = row.parentElement;
row.classList.add("layer-manager-dragging");
handle.setPointerCapture?.(event.pointerId);
const move = moveEvent => {
moveEvent.preventDefault();
const target = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY)?.closest(`[data-drag-type="${row.dataset.dragType}"]`);
if (!target || target === row || target.parentElement !== list) {
return;
}
const targetRect = target.getBoundingClientRect();
list.insertBefore(row, moveEvent.clientY < targetRect.top + targetRect.height / 2 ? target : target.nextSibling);
};
const end = () => {
row.classList.remove("layer-manager-dragging");
handle.releasePointerCapture?.(event.pointerId);
document.removeEventListener("pointermove", move);
document.removeEventListener("pointerup", end);
document.removeEventListener("pointercancel", end);
applyLayerManagerDomOrder(container);
};
document.addEventListener("pointermove", move, { passive: false });
document.addEventListener("pointerup", end);
document.addEventListener("pointercancel", end);
});
});
}
function bindLayerManagerMoveButtons(container) {
container.querySelectorAll(".layer-move-up, .layer-move-down").forEach(button => {
button.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
const row = button.closest("[data-drag-type]");
const sibling = button.classList.contains("layer-move-up")
? row.previousElementSibling
: row.nextElementSibling;
if (!sibling || sibling.dataset.dragType !== row.dataset.dragType) {
return;
}
if (button.classList.contains("layer-move-up")) {
row.parentElement.insertBefore(row, sibling);
} else {
row.parentElement.insertBefore(sibling, row);
}
applyLayerManagerDomOrder(container);
});
});
}
function applyLayerManagerDomOrder(container) {
const categoryKeys = [...container.children].map(row => row.dataset.categoryKey).filter(Boolean);
setLayerOrder(categoryKeys);
container.querySelectorAll(":scope > .layer-manager-group").forEach(group => {
const objectIds = [...group.querySelectorAll(".layer-manager-object")].map(row => row.dataset.objectId).filter(Boolean);
lmeState.managerObjectOrder[group.dataset.categoryKey] = objectIds;
});
applyManagedVisualOrder(container);
saveLayerManagerState();
}
function applyManagedVisualOrder(container) {
container.querySelectorAll(":scope > .layer-manager-group").forEach(group => {
const objectIds = [...group.querySelectorAll(".layer-manager-object")].map(row => row.dataset.objectId).filter(Boolean);
[...objectIds].reverse().forEach(id => lmeState.managerRecords.get(id)?.layer?.bringToFront?.());
objectIds.forEach((id, index) => lmeState.managerRecords.get(id)?.layer?.setZIndexOffset?.(1000 - index));
});
}
function setImportedSheetLayerVisible(enabled) {
migrateMisclassifiedImportedSheets();
lmeState.importedSheetsVisible = Boolean(enabled);
const generated = lmeState.importedSheetsVisible && ensureImportedSheetOutline();
applyImportedSheetVisibility();
const count = lmeState.importedSheets?.toGeoJSON()?.features?.length || 0;
setInfo(lmeState.importedSheetsVisible
? (count
? `${generated ? "Cornice generata dai dati importati" : "Layer del foglio importato visibile"} (${count} elementi).`
: "Nessun dato importato da cui ricavare la cornice del foglio.")
: "Layer del foglio importato nascosto; le particelle restano visibili.");
}
function ensureImportedSheetOutline() {
if (!lmeState.imported || !lmeState.importedSheets) {
return false;
}
const currentSheets = lmeState.importedSheets.toGeoJSON();
const hasAreaOutline = (currentSheets.features || []).some(feature =>
["Polygon", "MultiPolygon"].includes(feature?.geometry?.type));
if (hasAreaOutline) {
return false;
}
const imported = lmeState.imported.toGeoJSON();
const points = collectGeoJsonCoordinates(imported);
if (!points.length) {
return false;
}
const bounds = points.reduce((result, point) => ({
west: Math.min(result.west, point[0]),
south: Math.min(result.south, point[1]),
east: Math.max(result.east, point[0]),
north: Math.max(result.north, point[1])
}), { west: Infinity, south: Infinity, east: -Infinity, north: -Infinity });
if (!Number.isFinite(bounds.west)
|| !Number.isFinite(bounds.south)
|| bounds.east <= bounds.west
|| bounds.north <= bounds.south) {
return false;
}
const longitudePadding = Math.max((bounds.east - bounds.west) * 0.01, 0.000001);
const latitudePadding = Math.max((bounds.north - bounds.south) * 0.01, 0.000001);
const west = bounds.west - longitudePadding;
const south = bounds.south - latitudePadding;
const east = bounds.east + longitudePadding;
const north = bounds.north + latitudePadding;
addImportedSheetFeatures({
type: "FeatureCollection",
features: [{
type: "Feature",
properties: {
name: "Cornice del foglio importato",
_lmeGeneratedSheetOutline: true
},
geometry: {
type: "Polygon",
coordinates: [[
[west, south],
[east, south],
[east, north],
[west, north],
[west, south]
]]
}
}]
});
return true;
}
function migrateMisclassifiedImportedSheets() {
if (!lmeState.imported || !lmeState.importedSheets) {
return 0;
}
const current = lmeState.imported.toGeoJSON();
const misplaced = (current.features || []).filter(isSheetOrOverlayFeature);
if (!misplaced.length) {
return 0;
}
const retained = (current.features || []).filter(feature => !isSheetOrOverlayFeature(feature));
lmeState.imported.clearLayers();
addImportedFeatures({ type: "FeatureCollection", features: retained });
addImportedSheetFeatures({ type: "FeatureCollection", features: misplaced });
refreshImportedMeasurements();
return misplaced.length;
}
function addImportedFeatures(geojson) {
const layer = L.geoJSON(geojson || emptyFeatureCollection(), {
pane: "importedPane",
style: { color: "#22c55e", weight: 3, opacity: 0.95, fillOpacity: 0.18 },
pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
pane: "importedPane",
radius: 7,
color: "#22c55e",
fillColor: "#ffffff",
fillOpacity: 0.9
}),
onEachFeature: bindStoredFeaturePopup
}).addTo(lmeState.imported);
layer.lmeManagerLabel = managedGeoJsonLabel(geojson, "Dati importati");
return layer;
}
function addImportedSheetFeatures(geojson) {
const layer = L.geoJSON(geojson || emptyFeatureCollection(), {
pane: "importedSheetPane",
style: { color: "#f59e0b", weight: 4, opacity: 1, fillOpacity: 0.03, dashArray: "10 6" },
pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
pane: "importedSheetPane",
radius: 7,
color: "#f59e0b",
fillColor: "#ffffff",
fillOpacity: 0.85
}),
onEachFeature: bindStoredFeaturePopup
}).addTo(lmeState.importedSheets);
layer.lmeManagerLabel = managedGeoJsonLabel(geojson, "Cornice o foglio");
return layer;
}
function applyImportedSheetVisibility() {
if (!lmeState.map || !lmeState.importedSheets) {
return;
}
if (lmeState.importedSheetsVisible) {
if (!lmeState.map.hasLayer(lmeState.importedSheets)) {
lmeState.importedSheets.addTo(lmeState.map);
}
lmeState.importedSheets.bringToFront();
return;
}
if (lmeState.map.hasLayer(lmeState.importedSheets)) {
lmeState.map.removeLayer(lmeState.importedSheets);
}
}
function setImportedMeasurementsVisible(enabled) {
lmeState.showImportedMeasurements = Boolean(enabled);
refreshImportedMeasurements();
setInfo(lmeState.showImportedMeasurements
? "Misure dei file GIS visibili."
: "Misure dei file GIS nascoste.");
}
function setDrawingMeasurementsVisible(enabled) {
lmeState.showDrawingMeasurements = Boolean(enabled);
refreshDrawingMeasurements();
setInfo(lmeState.showDrawingMeasurements
? "Misure dei disegni visibili."
: "Misure dei disegni nascoste.");
}
function fitImported() {
const layers = [lmeState.imported];
if (lmeState.importedSheetsVisible) {
layers.push(lmeState.importedSheets);
}
fitLayer(L.featureGroup(layers));
}
function clearImported() {
clearManagedCategoryState("imported");
clearManagedCategoryState("importedSheets");
lmeState.imported.clearLayers();
lmeState.importedSheets.clearLayers();
lmeState.importedSheetsVisible = false;
applyImportedSheetVisibility();
lmeState.importedMeasurements.clearLayers();
setInfo("File GIS rimossi.");
updateGpsInsideState();
refreshLayerManager();
}
function addProjectMarker(name) {
const center = lmeState.map.getCenter();
const label = (name || "Nuovo POI").trim();
const marker = L.marker(center, { pane: "drawingsPane", draggable: true }).addTo(lmeState.drawings);
marker.lmeManagerLabel = `POI: ${label}`;
marker.feature = {
type: "Feature",
properties: { name: label }
};
marker.bindPopup(`${escapeHtml(label)}
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}`).openPopup();
refreshDrawingMeasurements();
setInfo(`POI aggiunto: ${label}`);
refreshLayerManager();
}
function configureDrawLabels() {
L.drawLocal.draw.toolbar.buttons.polygon = "Misura area";
L.drawLocal.draw.toolbar.buttons.polyline = "Misura distanza";
L.drawLocal.draw.toolbar.buttons.circle = "Misura cerchio";
L.drawLocal.draw.toolbar.buttons.rectangle = "Disegna rettangolo";
L.drawLocal.draw.toolbar.buttons.marker = "Aggiungi POI";
L.drawLocal.edit.toolbar.buttons.edit = "Modifica disegni";
L.drawLocal.edit.toolbar.buttons.remove = "Elimina disegni";
L.drawLocal.draw.handlers.polygon.tooltip.start = "Tocca il primo vertice dell'area.";
L.drawLocal.draw.handlers.polygon.tooltip.cont = "Tocca per aggiungere un vertice.";
L.drawLocal.draw.handlers.polygon.tooltip.end = "Tocca il primo punto per chiudere l'area.";
L.drawLocal.draw.handlers.polyline.tooltip.start = "Tocca il primo punto della distanza.";
L.drawLocal.draw.handlers.polyline.tooltip.cont = "Tocca per aggiungere un tratto.";
L.drawLocal.draw.handlers.polyline.tooltip.end = "Tocca l'ultimo punto per terminare.";
L.drawLocal.draw.handlers.circle.tooltip.start = "Tocca il centro e trascina per il raggio.";
}
function bindDrawingEvents() {
if (!lmeState.map || lmeState.drawEventsBound) {
return;
}
lmeState.map.on(L.Draw.Event.CREATED, event => {
annotateDrawingLayer(event.layer, event.layerType);
lmeState.drawings.addLayer(event.layer);
refreshDrawingMeasurements();
updateGpsInsideState();
const completedLabels = {
polygon: "Area completata: superficie, perimetro e lati aggiornati.",
rectangle: "Rettangolo completato: superficie, perimetro e lati aggiornati.",
polyline: "Distanza completata: lunghezza totale e tratti aggiornati.",
circle: "Cerchio completato: superficie, raggio e circonferenza aggiornati.",
marker: "Punto aggiunto alla mappa."
};
setInfo(completedLabels[event.layerType] || "Disegno completato.");
refreshLayerManager();
});
lmeState.map.on(L.Draw.Event.EDITED, event => {
event.layers.eachLayer(layer => annotateDrawingLayer(layer));
refreshDrawingMeasurements();
updateGpsInsideState();
refreshLayerManager();
});
lmeState.map.on(L.Draw.Event.DELETED, () => {
refreshDrawingMeasurements();
updateGpsInsideState();
refreshLayerManager();
});
lmeState.map.on(L.Draw.Event.DRAWSTOP, () => {
lmeState.activeDrawHandler = null;
});
lmeState.drawEventsBound = true;
}
function enableDrawTools() {
if (lmeState.drawControl) {
setInfo("Strumenti di disegno gia attivi.");
return;
}
configureDrawLabels();
const orangeArea = { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 };
lmeState.drawControl = new L.Control.Draw({
position: "topright",
edit: { featureGroup: lmeState.drawings },
draw: {
circle: { metric: true, feet: false, showRadius: true, shapeOptions: orangeArea },
circlemarker: false,
marker: { zIndexOffset: 0 },
polygon: {
metric: true,
feet: false,
showArea: true,
allowIntersection: false,
shapeOptions: orangeArea
},
polyline: {
metric: true,
feet: false,
showLength: true,
shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, opacity: 0.95 }
},
rectangle: { metric: true, shapeOptions: orangeArea }
}
});
lmeState.map.addControl(lmeState.drawControl);
bindDrawingEvents();
setInfo("Barra strumenti attiva: misure, disegni, POI, modifica e cancellazione.");
}
function startMeasurementDrawing(mode) {
if (!lmeState.map) {
return;
}
bindDrawingEvents();
if (lmeState.activeDrawHandler) {
lmeState.activeDrawHandler.disable();
}
const handlers = {
area: () => new L.Draw.Polygon(lmeState.map, {
allowIntersection: false,
showArea: true,
metric: true,
feet: false,
shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 }
}),
distance: () => new L.Draw.Polyline(lmeState.map, {
metric: true,
feet: false,
showLength: true,
shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, opacity: 0.95 }
}),
circle: () => new L.Draw.Circle(lmeState.map, {
metric: true,
feet: false,
showRadius: true,
shapeOptions: { pane: "drawingsPane", color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 }
})
};
const createHandler = handlers[mode];
if (!createHandler) {
setInfo("Modalita di misura non riconosciuta.");
return;
}
lmeState.activeDrawHandler = createHandler();
lmeState.activeDrawHandler.enable();
const labels = {
area: "Tocca i vertici dell'area; chiudi toccando il primo punto.",
distance: "Tocca i punti del percorso; termina sull'ultimo punto.",
circle: "Tocca il centro e trascina fino al raggio desiderato."
};
setInfo(labels[mode]);
}
function annotateDrawingLayer(layer, layerType) {
layer.options.pane = "drawingsPane";
layer.feature = layer.feature || { type: "Feature", properties: {} };
layer.feature.properties = layer.feature.properties || {};
if (layer instanceof L.Circle) {
layer.feature.properties._lmeShape = "circle";
layer.feature.properties._lmeRadius = layer.getRadius();
} else if (layerType) {
layer.feature.properties._lmeShape = layerType;
}
layer.lmeManagerLabel = drawingLayerLabel(layer, layerType);
}
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 = managedGroupGeoJson(lmeState.drawings, "drawings").features.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();
clearManagedCategoryState("drawings");
lmeState.drawingMeasurements.clearLayers();
updateGpsInsideState();
setInfo("Disegni e POI eliminati dalla mappa.");
refreshLayerManager();
}
function scheduleMeasurementRefresh() {
window.clearTimeout(lmeState.measurementRefreshTimer);
lmeState.measurementRefreshTimer = window.setTimeout(() => {
refreshImportedMeasurements();
refreshDrawingMeasurements();
}, 80);
}
function refreshImportedMeasurements() {
renderMeasurements(
lmeState.imported,
lmeState.importedMeasurements,
lmeState.showImportedMeasurements,
"imported");
}
function refreshDrawingMeasurements() {
renderMeasurements(
lmeState.drawings,
lmeState.drawingMeasurements,
lmeState.showDrawingMeasurements,
"drawing");
}
function renderMeasurements(sourceGroup, targetGroup, visible, sourceType) {
if (!sourceGroup || !targetGroup) {
return;
}
targetGroup.clearLayers();
if (!visible || !lmeState.map.hasLayer(sourceGroup)) {
return;
}
const context = {
count: 0,
max: 600,
showSides: lmeState.map.getZoom() >= 17,
bounds: lmeState.map.getBounds(),
sourceType
};
visitMeasurableLayers(sourceGroup, layer => {
if (context.count >= context.max || !layerIsInView(layer, context.bounds)) {
return;
}
if (layer instanceof L.Circle) {
renderCircleMeasurement(layer, targetGroup, context);
} else if (layer instanceof L.Polygon) {
renderPolygonMeasurement(layer, targetGroup, context);
} else if (layer instanceof L.Polyline) {
renderPolylineMeasurement(layer, targetGroup, context);
}
});
}
function visitMeasurableLayers(layer, visitor) {
if (layer instanceof L.Circle || layer instanceof L.Polygon || layer instanceof L.Polyline) {
visitor(layer);
return;
}
if (typeof layer.eachLayer === "function") {
layer.eachLayer(child => visitMeasurableLayers(child, visitor));
}
}
function layerIsInView(layer, mapBounds) {
if (typeof layer.getBounds === "function") {
const bounds = layer.getBounds();
return !bounds.isValid || !bounds.isValid() || bounds.intersects(mapBounds);
}
if (typeof layer.getLatLng === "function") {
return mapBounds.contains(layer.getLatLng());
}
return true;
}
function renderCircleMeasurement(layer, targetGroup, context) {
const radius = layer.getRadius();
const area = Math.PI * radius * radius;
const circumference = 2 * Math.PI * radius;
addMeasurementLabel(
targetGroup,
layer.getLatLng(),
`S: ${formatArea(area)}
R: ${formatDistance(radius)} · C: ${formatDistance(circumference)}`,
"summary",
context);
}
function renderPolygonMeasurement(layer, targetGroup, context) {
const polygons = polygonLatLngGroups(layer.getLatLngs());
let area = 0;
let perimeter = 0;
polygons.forEach(rings => {
rings.forEach((ring, index) => {
const ringArea = Math.abs(L.GeometryUtil.geodesicArea(ring));
area += index === 0 ? ringArea : -ringArea;
perimeter += latLngPathLength(ring, true);
if (context.showSides) {
addSegmentLabels(ring, true, targetGroup, context);
}
});
});
const center = typeof layer.getCenter === "function"
? layer.getCenter()
: layer.getBounds().getCenter();
addMeasurementLabel(
targetGroup,
center,
`S: ${formatArea(Math.max(0, area))}
P: ${formatDistance(perimeter)}`,
"summary",
context);
}
function renderPolylineMeasurement(layer, targetGroup, context) {
const lines = lineLatLngGroups(layer.getLatLngs());
const length = lines.reduce((total, line) => total + latLngPathLength(line, false), 0);
if (context.showSides) {
lines.forEach(line => addSegmentLabels(line, false, targetGroup, context));
}
const longest = lines.reduce((current, line) => line.length > current.length ? line : current, []);
if (longest.length) {
addMeasurementLabel(
targetGroup,
longest[Math.floor(longest.length / 2)],
`L: ${formatDistance(length)}`,
"summary",
context);
}
}
function polygonLatLngGroups(latLngs) {
if (!Array.isArray(latLngs) || !latLngs.length) {
return [];
}
if (isLatLng(latLngs[0])) {
return [[latLngs]];
}
if (Array.isArray(latLngs[0]) && isLatLng(latLngs[0][0])) {
return [latLngs];
}
return latLngs.flatMap(polygonLatLngGroups);
}
function lineLatLngGroups(latLngs) {
if (!Array.isArray(latLngs) || !latLngs.length) {
return [];
}
if (isLatLng(latLngs[0])) {
return [latLngs];
}
return latLngs.flatMap(lineLatLngGroups);
}
function isLatLng(value) {
return value && Number.isFinite(value.lat) && Number.isFinite(value.lng);
}
function latLngPathLength(points, closed) {
if (points.length < 2) {
return 0;
}
let total = 0;
for (let index = 1; index < points.length; index += 1) {
total += lmeState.map.distance(points[index - 1], points[index]);
}
if (closed && !points[0].equals(points[points.length - 1])) {
total += lmeState.map.distance(points[points.length - 1], points[0]);
}
return total;
}
function addSegmentLabels(points, closed, targetGroup, context) {
if (points.length < 2) {
return;
}
const segmentCount = closed ? points.length : points.length - 1;
for (let index = 0; index < segmentCount && context.count < context.max; index += 1) {
const start = points[index];
const end = points[(index + 1) % points.length];
if (start.equals(end)) {
continue;
}
addMeasurementLabel(
targetGroup,
L.latLng((start.lat + end.lat) / 2, (start.lng + end.lng) / 2),
formatDistance(lmeState.map.distance(start, end)),
"side",
context);
}
}
function addMeasurementLabel(targetGroup, latLng, html, labelType, context) {
if (context.count >= context.max) {
return;
}
L.marker(latLng, {
pane: context.sourceType === "drawing" ? "drawingsPane" : "importedPane",
interactive: false,
keyboard: false,
icon: L.divIcon({
className: `lme-measure-label ${context.sourceType} ${labelType}`,
html: `${html}`,
iconSize: null
})
}).addTo(targetGroup).lmeManagerLabel = html.replace(/<[^>]+>/g, "");
context.count += 1;
}
function formatDistance(meters) {
if (meters >= 1000) {
return `${formatNumber(meters / 1000, 3)} km`;
}
return `${formatNumber(meters, meters < 100 ? 1 : 0)} m`;
}
function formatArea(squareMeters) {
if (squareMeters >= 10000) {
return `${formatNumber(squareMeters / 10000, 4)} ha`;
}
return `${formatNumber(squareMeters, squareMeters < 100 ? 1 : 0)} m²`;
}
function formatNumber(value, digits) {
return new Intl.NumberFormat("it-IT", {
minimumFractionDigits: 0,
maximumFractionDigits: digits
}).format(value);
}
function createMapExportGeoJson(options) {
migrateMisclassifiedImportedSheets();
const requested = options || {};
const features = [];
if (requested.includeImported !== false) {
appendExportFeatures(features, managedGroupGeoJson(lmeState.imported, "imported"), "imported");
}
if (requested.includeImportedSheets !== false) {
appendExportFeatures(features, managedGroupGeoJson(lmeState.importedSheets, "importedSheets"), "imported-sheet");
}
if (requested.includeCadastral !== false) {
appendExportFeatures(features, managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels"), "cadastral");
appendExportFeatures(features, managedGroupGeoJson(lmeState.gpsParcelLayer, "gpsParcels"), "cadastral-gps");
}
const drawings = managedGroupGeoJson(lmeState.drawings, "drawings");
if (requested.includeDrawings !== false) {
appendExportFeatures(features, drawings, "drawing", feature => !isPoiFeature(feature));
}
if (requested.includePoi !== false) {
appendExportFeatures(features, drawings, "poi", isPoiFeature);
}
if (requested.includeTrail !== false && lmeState.gpsTrailPoints.length >= 2) {
features.push({
type: "Feature",
properties: { name: "Traccia GPS", _lmeCategory: "gps-trail" },
geometry: {
type: "LineString",
coordinates: lmeState.gpsTrailPoints.map(point => [
Number(point.lng ?? point[1]),
Number(point.lat ?? point[0])
])
}
});
}
return JSON.stringify({ type: "FeatureCollection", features });
}
function appendExportFeatures(target, collection, category, predicate = () => true) {
for (const sourceFeature of collection?.features || []) {
if (!sourceFeature?.geometry || !predicate(sourceFeature)) {
continue;
}
const feature = JSON.parse(JSON.stringify(sourceFeature));
feature.properties = { ...(feature.properties || {}), _lmeCategory: category };
if (feature.properties._lmeShape === "circle"
&& feature.geometry.type === "Point"
&& Number(feature.properties._lmeRadius) > 0) {
feature.geometry = circleToPolygon(feature.geometry.coordinates, Number(feature.properties._lmeRadius));
}
target.push(feature);
}
}
function isPoiFeature(feature) {
return feature?.geometry?.type === "Point"
&& feature?.properties?._lmeShape !== "circle";
}
function circleToPolygon(center, radiusMeters) {
const longitude = Number(center?.[0]);
const latitude = Number(center?.[1]);
const earthRadius = 6378137;
const latitudeRadians = latitude * Math.PI / 180;
const ring = [];
for (let index = 0; index <= 64; index += 1) {
const angle = index / 64 * Math.PI * 2;
const north = Math.sin(angle) * radiusMeters;
const east = Math.cos(angle) * radiusMeters;
ring.push([
longitude + east / (earthRadius * Math.cos(latitudeRadians)) * 180 / Math.PI,
latitude + north / earthRadius * 180 / Math.PI
]);
}
return { type: "Polygon", coordinates: [ring] };
}
function createProjectSnapshot(includeCadastralParcels) {
const center = lmeState.map.getCenter();
return JSON.stringify({
schemaVersion: 3,
view: {
latitude: center.lat,
longitude: center.lng,
zoom: lmeState.map.getZoom()
},
drawings: managedGroupGeoJson(lmeState.drawings, "drawings"),
imported: managedGroupGeoJson(lmeState.imported, "imported"),
importedSheets: managedGroupGeoJson(lmeState.importedSheets, "importedSheets"),
importedSheetsVisible: lmeState.importedSheetsVisible,
displayOptions: captureDisplayOptions(),
selectedParcels: includeCadastralParcels
? managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels")
: { type: "FeatureCollection", features: [] },
trail: lmeState.gpsTrailPoints.map(point => [
Number(point.lat ?? point[0]),
Number(point.lng ?? point[1])
])
});
}
function getLegacyProjectSnapshot() {
return localStorage.getItem("lmeMapWorkspace");
}
function clearLegacyProjectSnapshot() {
localStorage.removeItem("lmeMapWorkspace");
}
function loadProjectSnapshot(raw, projectName) {
try {
const data = typeof raw === "string" ? JSON.parse(raw) : raw;
if (!data || typeof data !== "object") {
throw new Error("contenuto non valido");
}
resetLayerManagerProjectState();
lmeState.drawings.clearLayers();
lmeState.imported.clearLayers();
lmeState.importedSheets.clearLayers();
lmeState.drawingMeasurements.clearLayers();
lmeState.importedMeasurements.clearLayers();
lmeState.selectedParcelsLayer.clearLayers();
lmeState.gpsParcelLayer.clearLayers();
const restoredDrawings = L.geoJSON(data.drawings || emptyFeatureCollection(), {
pane: "drawingsPane",
style: { color: "#f97316", weight: 3, fillOpacity: 0.16 },
pointToLayer: (feature, latlng) => {
const properties = feature?.properties || {};
if (properties._lmeShape === "circle" && Number(properties._lmeRadius) > 0) {
return L.circle(latlng, {
pane: "drawingsPane",
radius: Number(properties._lmeRadius),
color: "#f97316",
weight: 3,
fillColor: "#fb923c",
fillOpacity: 0.2
});
}
return L.marker(latlng, { pane: "drawingsPane", draggable: true });
},
onEachFeature: (feature, layer) => {
bindStoredFeaturePopup(feature, layer);
annotateDrawingLayer(layer, feature?.properties?._lmeShape);
}
});
restoredDrawings.eachLayer(layer => lmeState.drawings.addLayer(layer));
const importedParts = partitionStoredImportedFeatures(
data.imported || emptyFeatureCollection(),
data.importedSheets || emptyFeatureCollection());
addImportedFeatures(importedParts.parcels);
addImportedSheetFeatures(importedParts.sheets);
const projectDisplayOptions = data.schemaVersion >= 3 && data.displayOptions
? data.displayOptions
: null;
lmeState.importedSheetsVisible = projectDisplayOptions
? Boolean(projectDisplayOptions.importedSheetsVisible)
: data.schemaVersion >= 2 && Boolean(data.importedSheetsVisible);
if (lmeState.importedSheetsVisible) {
ensureImportedSheetOutline();
}
applyImportedSheetVisibility();
L.geoJSON(data.selectedParcels || emptyFeatureCollection(), {
pane: "selectedParcelsPane",
style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 },
onEachFeature: (feature, layer) => {
const properties = feature?.properties || {};
const reference = properties.NATIONALCADASTRALREFERENCE
|| properties.INSPIREID_LOCALID
|| properties.LABEL
|| "Particella catastale";
layer.lmeParcelReference = reference;
layer.bindPopup(`${escapeHtml(reference)}`);
}
}).addTo(lmeState.selectedParcelsLayer);
lmeState.gpsTrailPoints = Array.isArray(data.trail) ? data.trail : [];
lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
if (projectDisplayOptions) {
applyProjectDisplayOptions(projectDisplayOptions);
}
refreshImportedMeasurements();
refreshDrawingMeasurements();
const projectLayerList = [
lmeState.drawings,
lmeState.imported,
lmeState.selectedParcelsLayer,
lmeState.gpsTrail
];
if (lmeState.importedSheetsVisible) {
projectLayerList.push(lmeState.importedSheets);
}
const projectLayers = L.featureGroup(projectLayerList);
if (projectLayers.getBounds().isValid()) {
fitLayer(projectLayers);
} else if (Number.isFinite(data.view?.latitude)
&& Number.isFinite(data.view?.longitude)
&& Number.isFinite(data.view?.zoom)) {
lmeState.map.setView([data.view.latitude, data.view.longitude], data.view.zoom);
}
const parcelCount = lmeState.selectedParcelsLayer.toGeoJSON().features?.length || 0;
setText("parcelState", parcelCount
? `${parcelCount} particelle del progetto`
: "Nessuna particella selezionata");
updateGpsInsideState();
const hiddenSheets = importedParts.sheets.features.length && !lmeState.importedSheetsVisible
? ` ${importedParts.sheets.features.length} elementi del foglio sono nascosti.`
: "";
setInfo(`Progetto caricato: ${projectName || "senza nome"}.${hiddenSheets}`);
refreshLayerManager();
return captureDisplayOptions();
} catch (error) {
setInfo(`Progetto non leggibile: ${error.message}`);
throw error;
}
}
function captureDisplayOptions() {
return {
baseLayer: lmeState.currentBaseName,
cadastreOpacity: lmeState.cadastreOpacity,
cadastreLayers: [...lmeState.cadastreLayers],
includeKmlSheetLayer: lmeState.includeKmlSheetLayer,
importedVisible: lmeState.importedVisible,
importedSheetsVisible: lmeState.importedSheetsVisible,
showImportedMeasurements: lmeState.showImportedMeasurements,
showDrawingMeasurements: lmeState.showDrawingMeasurements,
drawToolsVisible: Boolean(lmeState.drawControl),
gpsPositionVisible: lmeState.gpsPositionVisible,
selectedParcelsVisible: lmeState.selectedParcelsVisible,
layerOrder: [...lmeState.layerOrder],
layerObjectOrder: { ...lmeState.managerObjectOrder },
hiddenLayerObjects: [...lmeState.managerHiddenObjectIds],
hiddenLayerCategories: [...lmeState.managerHiddenCategories],
trailEnabled: lmeState.gpsTrailEnabled
};
}
function applyProjectDisplayOptions(options) {
const supportedCadastreLayers = new Set([
"CP.CadastralParcel",
"codice_plla",
"CP.CadastralZoning",
"fabbricati",
"strade",
"acque"
]);
if (Array.isArray(options.cadastreLayers)) {
lmeState.cadastreLayers = options.cadastreLayers.filter(layer => supportedCadastreLayers.has(layer));
}
const opacity = Number(options.cadastreOpacity);
if (Number.isFinite(opacity)) {
lmeState.cadastreOpacity = Math.max(0, Math.min(1, opacity));
}
lmeState.includeKmlSheetLayer = Boolean(options.includeKmlSheetLayer);
lmeState.importedVisible = options.importedVisible !== false;
lmeState.importedSheetsVisible = Boolean(options.importedSheetsVisible);
lmeState.showImportedMeasurements = Boolean(options.showImportedMeasurements);
lmeState.showDrawingMeasurements = options.showDrawingMeasurements !== false;
if (Array.isArray(options.layerOrder)) {
setLayerOrder(options.layerOrder);
}
if (options.layerObjectOrder && typeof options.layerObjectOrder === "object") {
lmeState.managerObjectOrder = options.layerObjectOrder;
}
if (Array.isArray(options.hiddenLayerObjects)) {
lmeState.managerHiddenObjectIds = new Set(options.hiddenLayerObjects);
}
if (Array.isArray(options.hiddenLayerCategories)) {
lmeState.managerHiddenCategories = new Set(options.hiddenLayerCategories);
}
applyImportedSheetVisibility();
setImportedLayerVisible(lmeState.importedVisible);
refreshCadastreLayer();
if (typeof options.baseLayer === "string") {
setBaseLayer(options.baseLayer);
}
setGpsPositionVisible(options.gpsPositionVisible !== false);
setSelectedParcelsVisible(options.selectedParcelsVisible !== false);
toggleTrail(Boolean(options.trailEnabled));
setDrawToolsVisible(Boolean(options.drawToolsVisible));
applyStoredLayerManagerVisibility();
saveLayerManagerState();
}
function partitionStoredImportedFeatures(imported, storedSheets) {
const parcelFeatures = [];
const sheetFeatures = [...(storedSheets?.features || [])];
const sourceGroups = new Map();
(imported?.features || []).forEach(feature => {
if (isSheetOrOverlayFeature(feature)) {
sheetFeatures.push(feature);
return;
}
const source = String(feature?.properties?._lmeSource || "");
if (!/\.(kml|kmz)$/i.test(source)) {
parcelFeatures.push(feature);
return;
}
const group = sourceGroups.get(source) || [];
group.push(feature);
sourceGroups.set(source, group);
});
sourceGroups.forEach(features => {
const polygonParts = splitKmlPolygonFeatures(features);
const parcels = filterKmlParcelFeatures(polygonParts);
const parcelSet = new Set(parcels);
parcelFeatures.push(...parcels);
sheetFeatures.push(...polygonParts.filter(feature => !parcelSet.has(feature)));
sheetFeatures.push(...features.filter(feature => geometryPolygons(feature.geometry).length === 0));
});
return {
parcels: { type: "FeatureCollection", features: parcelFeatures },
sheets: { type: "FeatureCollection", features: sheetFeatures }
};
}
function emptyFeatureCollection() {
return { type: "FeatureCollection", features: [] };
}
function managedGroupGeoJson(group, categoryKey) {
const collection = group?.toGeoJSON?.() || emptyFeatureCollection();
const features = [...(collection.features || [])];
const existingIds = new Set(features.map(feature => feature?.properties?._lmeManagerId).filter(Boolean));
lmeState.managerHiddenObjects.forEach(record => {
if (record.categoryKey !== categoryKey) {
return;
}
const hidden = record.layer?.toGeoJSON?.();
const hiddenFeatures = hidden?.type === "FeatureCollection" ? hidden.features : hidden ? [hidden] : [];
hiddenFeatures.forEach(feature => {
const id = feature?.properties?._lmeManagerId;
if (!id || !existingIds.has(id)) {
features.push(feature);
if (id) {
existingIds.add(id);
}
}
});
});
return { type: "FeatureCollection", features };
}
function resetLayerManagerProjectState() {
lmeState.managerHiddenObjects.clear();
lmeState.managerHiddenObjectIds.clear();
lmeState.managerHiddenCategories.clear();
lmeState.managerObjectOrder = {};
clearManagerHighlight();
}
function clearManagedCategoryState(categoryKey) {
[...lmeState.managerHiddenObjects.entries()].forEach(([id, record]) => {
if (record.categoryKey === categoryKey) {
lmeState.managerHiddenObjects.delete(id);
lmeState.managerHiddenObjectIds.delete(id);
}
});
lmeState.managerHiddenCategories.delete(categoryKey);
delete lmeState.managerObjectOrder[categoryKey];
}
function bindStoredFeaturePopup(feature, layer) {
const properties = feature?.properties || {};
const label = properties.name || properties.Name || properties.label || properties.LABEL;
if (label) {
layer.bindPopup(`${escapeHtml(label)}`);
}
}
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) {
const opacity = Number(value);
if (!Number.isFinite(opacity)) {
return;
}
lmeState.cadastreOpacity = Math.max(0, Math.min(1, opacity));
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(), {
pane: "cadastrePane",
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 = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length;
lmeState.selectedParcelsLayer.clearLayers();
clearManagedCategoryState("selectedParcels");
updateGpsInsideState();
setInfo(count ? `${count} particelle selezionate rimosse.` : "Non ci sono particelle selezionate da rimuovere.");
refreshLayerManager();
}
function setSelectedParcelsVisible(enabled) {
lmeState.selectedParcelsVisible = Boolean(enabled);
if (!lmeState.map || !lmeState.selectedParcelsLayer) {
return;
}
if (lmeState.selectedParcelsVisible) {
if (!lmeState.map.hasLayer(lmeState.selectedParcelsLayer)) {
lmeState.selectedParcelsLayer.addTo(lmeState.map);
}
lmeState.selectedParcelsLayer.bringToFront();
const count = lmeState.selectedParcelsLayer.toGeoJSON().features?.length || 0;
setInfo(count
? `${count} particelle selezionate visibili.`
: "Nessuna particella selezionata da mostrare.");
return;
}
if (lmeState.map.hasLayer(lmeState.selectedParcelsLayer)) {
lmeState.map.removeLayer(lmeState.selectedParcelsLayer);
}
setInfo("Particelle selezionate nascoste; i dati restano nel progetto.");
}
function clearMapElements() {
const count = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length
+ managedGroupGeoJson(lmeState.gpsParcelLayer, "gpsParcels").features.length
+ managedGroupGeoJson(lmeState.imported, "imported").features.length
+ managedGroupGeoJson(lmeState.importedSheets, "importedSheets").features.length
+ managedGroupGeoJson(lmeState.drawings, "drawings").features.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;
}
resetLayerManagerProjectState();
lmeState.selectedParcelsLayer.clearLayers();
lmeState.gpsParcelLayer.clearLayers();
lmeState.imported.clearLayers();
lmeState.importedSheets.clearLayers();
lmeState.drawings.clearLayers();
lmeState.importedMeasurements.clearLayers();
lmeState.drawingMeasurements.clearLayers();
lmeState.gpsTrailPoints = [];
lmeState.gpsTrail.setLatLngs([]);
updateGpsInsideState();
setInfo("Elementi della mappa rimossi. Sfondo e layer catastali restano attivi.");
refreshLayerManager();
}
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 setGpsPositionVisible(enabled) {
lmeState.gpsPositionVisible = Boolean(enabled);
if (!lmeState.gpsPositionVisible) {
if (lmeState.gpsMarker && lmeState.map.hasLayer(lmeState.gpsMarker)) {
lmeState.map.removeLayer(lmeState.gpsMarker);
}
if (lmeState.gpsAccuracy && lmeState.map.hasLayer(lmeState.gpsAccuracy)) {
lmeState.map.removeLayer(lmeState.gpsAccuracy);
}
setInfo("Punto e cerchio GPS nascosti. Il rilevamento continua.");
return;
}
if (lmeState.hasLiveGpsFix && lmeState.lastPosition) {
updateGpsPositionLayers(lmeState.lastPosition, lmeState.lastAccuracy);
}
setInfo("Punto e cerchio GPS visibili.");
}
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;
lmeState.lastAccuracy = Math.max(0, Number(accuracy) || 0);
const firstLiveFix = !lmeState.hasLiveGpsFix;
lmeState.hasLiveGpsFix = true;
updateGpsPositionLayers(latlng, lmeState.lastAccuracy);
if (firstLiveFix) {
lmeState.map.setView(latlng, 19);
}
if (lmeState.gpsTrailEnabled) {
lmeState.gpsTrailPoints.push(latlng);
lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
}
setText("gpsState", `${latitude.toFixed(6)}, ${longitude.toFixed(6)} - precisione ${Math.round(lmeState.lastAccuracy)} m`);
updateGpsInsideState();
}
function updateGpsPositionLayers(latlng, accuracy) {
let created = false;
if (!lmeState.gpsMarker) {
lmeState.gpsMarker = L.marker(latlng, { pane: "gpsPositionPane", title: "La tua posizione" });
lmeState.gpsMarker.lmeManagerId = "gps-position-marker";
lmeState.gpsMarker.lmeManagerLabel = "Posizione GPS";
lmeState.gpsAccuracy = L.circle(latlng, {
pane: "gpsPositionPane",
radius: accuracy,
color: "#2563eb",
weight: 2,
fillColor: "#60a5fa",
fillOpacity: 0.16
});
lmeState.gpsAccuracy.lmeManagerId = "gps-accuracy-circle";
lmeState.gpsAccuracy.lmeManagerLabel = "Cerchio precisione GPS";
created = true;
} else {
lmeState.gpsMarker.setLatLng(latlng);
lmeState.gpsAccuracy.setLatLng(latlng).setRadius(accuracy);
}
const categoryVisible = lmeState.gpsPositionVisible
&& !lmeState.managerHiddenCategories.has("gpsPosition");
if (categoryVisible
&& !lmeState.managerHiddenObjectIds.has("gps-position-marker")
&& !lmeState.map.hasLayer(lmeState.gpsMarker)) {
lmeState.gpsMarker.addTo(lmeState.map);
} else if ((!categoryVisible || lmeState.managerHiddenObjectIds.has("gps-position-marker"))
&& lmeState.map.hasLayer(lmeState.gpsMarker)) {
lmeState.map.removeLayer(lmeState.gpsMarker);
}
if (categoryVisible
&& !lmeState.managerHiddenObjectIds.has("gps-accuracy-circle")
&& !lmeState.map.hasLayer(lmeState.gpsAccuracy)) {
lmeState.gpsAccuracy.addTo(lmeState.map);
} else if ((!categoryVisible || lmeState.managerHiddenObjectIds.has("gps-accuracy-circle"))
&& lmeState.map.hasLayer(lmeState.gpsAccuracy)) {
lmeState.map.removeLayer(lmeState.gpsAccuracy);
}
if (created) {
refreshLayerManager();
}
}
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("'", "'");
}