${lmeState.readOnly ? "Il progetto e' in sola lettura: le note possono essere consultate ma non modificate." : "La consultazione e' disponibile; per aggiungere, modificare o eliminare note serve LME Pro con abbonamento o licenza manuale."}
`;
}).join("");
const dialog = showAppDialog("Elenco oggetti e particelle", `
Seleziona piรน oggetti con i flag nella colonna Selez.Il lucchetto impedisce modifiche e rimozioni; la matita modifica i metadati.Note apre l'elenco completo; le modifiche richiedono LME Pro.
`);
return lmeState.auditLog.length;
}
function updateManagedProperties(records, change) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
rememberMapChange();
records.forEach(record => {
record.layer.feature ||= { type: "Feature", properties: {} };
record.layer.feature.properties ||= {};
change(record.layer.feature.properties);
applyStyleFromProperties(record.layer);
updateStoredFeaturePopup(record.layer);
});
refreshPermanentLabels();
refreshLayerManager();
finishMapChange();
}
function applyManagedStyle(records, style) {
updateManagedProperties(records, properties => {
properties._lmeColor = style.color;
properties._lmeFillColor = style.fillColor || style.color;
properties._lmeWeight = style.weight;
properties._lmeOpacity = style.opacity;
if (style.fillOpacity !== undefined) properties._lmeFillOpacity = style.fillOpacity;
if (style.dash !== undefined) properties._lmeDash = style.dash;
});
}
function applyStyleFromProperties(layer) {
const properties = layer?.feature?.properties || {};
if (typeof layer?.setStyle === "function") {
layer.setStyle({
color: properties._lmeColor || layer.options.color,
fillColor: properties._lmeFillColor || layer.options.fillColor,
weight: Number(properties._lmeWeight) || layer.options.weight,
opacity: Number.isFinite(Number(properties._lmeOpacity)) ? Number(properties._lmeOpacity) : layer.options.opacity,
fillOpacity: Number.isFinite(Number(properties._lmeFillOpacity)) ? Number(properties._lmeFillOpacity) : layer.options.fillOpacity,
dashArray: properties._lmeDash || null
});
}
}
function refreshPermanentLabels() {
lmeState.permanentLabels?.clearLayers();
layerManagerCategories().forEach(category => managedRecordsForCategory(category).forEach((record, index) => {
const properties = record.layer?.feature?.properties || {};
if (!properties._lmeLabelVisible || lmeState.managerHiddenObjectIds.has(record.id)) return;
const center = layerLabelPosition(record.layer); if (!center) return;
L.marker(center, { pane: "tooltipPane", interactive: false, icon: L.divIcon({ className: "lme-permanent-label", html: `${escapeHtml(managedObjectLabel(record, index))}`, iconSize: null }) }).addTo(lmeState.permanentLabels);
}));
}
function layerLabelPosition(layer) {
if (typeof layer?.getBounds === "function" && layer.getBounds().isValid()) return layer.getBounds().getCenter();
if (typeof layer?.getLatLng === "function") return layer.getLatLng();
return null;
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); });
}
function addDrawingFeature(feature, select = false) {
let added = null;
L.geoJSON(feature, {
pane: "drawingsPane",
style: storedFeatureStyle,
pointToLayer: (item, latlng) => item?.properties?._lmeShape === "circle" && Number(item.properties._lmeRadius) > 0
? L.circle(latlng, { pane: "drawingsPane", renderer: lmeState.drawingsRenderer, radius: Number(item.properties._lmeRadius) })
: L.marker(latlng, { pane: "drawingsMarkerPane", draggable: false }),
onEachFeature: (item, layer) => {
annotateDrawingLayer(layer, item?.properties?._lmeShape);
applyStyleFromProperties(layer);
updateStoredFeaturePopup(layer);
lmeState.drawings.addLayer(layer);
added = layer;
}
});
if (select && added) selectDrawingLayer(added);
return added;
}
function storedFeatureStyle(feature) {
const properties = feature?.properties || {};
return {
...(properties._lmeComparisonResult ? { pane: "comparisonPane" } : { renderer: lmeState.drawingsRenderer }),
color: properties._lmeColor || "#f97316",
fillColor: properties._lmeFillColor || "#fb923c",
weight: Number(properties._lmeWeight) || 3,
opacity: Number.isFinite(Number(properties._lmeOpacity)) ? Number(properties._lmeOpacity) : 1,
fillOpacity: Number.isFinite(Number(properties._lmeFillOpacity)) ? Number(properties._lmeFillOpacity) : 0.2,
dashArray: properties._lmeDash || null
};
}
function duplicateManagedRecords(records) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
if (records.some(record => record.layer?.feature?.properties?._lmeLocked)) { setInfo("Sblocca gli oggetti da duplicare."); return; }
if (!canAddDrawingObjects(records.length)) return;
rememberMapChange();
records.forEach((record, index) => {
const feature = record.layer.toGeoJSON();
feature.properties = { ...(feature.properties || {}), name: `${managedObjectLabel(record, index)} - copia`, _lmeManagerId: undefined };
if (record.layer instanceof L.Circle) {
const point = turf.transformTranslate(feature, 1.5, 45, { units: "meters" });
point.properties._lmeShape = "circle";
point.properties._lmeRadius = record.layer.getRadius();
addDrawingFeature(point);
} else {
addDrawingFeature(turf.transformTranslate(feature, 1.5, 45, { units: "meters" }));
}
});
refreshDrawingMeasurements(); refreshLayerManager(); finishMapChange();
setInfo(`${records.length} ${records.length === 1 ? "oggetto duplicato" : "oggetti duplicati"}.`);
}
async function moveManagedRecords(records) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
if (records.some(record => record.layer?.feature?.properties?._lmeLocked)) { setInfo("Sblocca gli oggetti da spostare."); return; }
const result = await showFormDialog("Sposta oggetti", [
{ name: "north", label: "Metri verso nord (negativo verso sud)", type: "number", value: "0", step: "0.01" },
{ name: "east", label: "Metri verso est (negativo verso ovest)", type: "number", value: "0", step: "0.01" }
], "Sposta");
if (!result) return;
const north = Number(result.north);
if (!Number.isFinite(north)) return;
const east = Number(result.east);
if (!Number.isFinite(east) || (!north && !east)) return;
const distance = Math.hypot(north, east);
const bearing = Math.atan2(east, north) * 180 / Math.PI;
rememberMapChange();
records.forEach(record => {
const moved = turf.transformTranslate(record.layer.toGeoJSON(), distance, bearing, { units: "meters" });
replaceManagedLayer(record, moved);
});
finishManagedGeometryChange(`Spostati ${records.length} oggetti.`);
}
async function bufferManagedRecords(records) {
if (!requireProFeature("advancedGeometry")) return;
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
const result = await showFormDialog("Crea buffer", [{ name: "meters", label: "Distanza in metri", type: "number", value: "5", step: "0.01" }], "Crea buffer");
if (!result) return;
const meters = Number(result.meters);
if (!Number.isFinite(meters) || meters <= 0) return;
rememberMapChange();
let count = 0;
records.forEach((record, index) => {
const buffered = turf.buffer(record.layer.toGeoJSON(), meters, { units: "meters" });
if (!buffered) return;
buffered.properties = { ...(record.layer.feature?.properties || {}), name: `Buffer ${meters} m - ${managedObjectLabel(record, index)}`, _lmeShape: "polygon", _lmeManagerId: undefined };
addDrawingFeature(buffered); count++;
});
finishManagedGeometryChange(`Creati ${count} buffer da ${formatDistance(meters)}.`);
}
function unionManagedRecords(records) {
if (!requireProFeature("advancedGeometry")) return;
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
const polygons = records.filter(record => record.layer instanceof L.Polygon).map(record => record.layer.toGeoJSON());
if (polygons.length < 2) { setInfo("Per unire seleziona almeno due aree o poligoni."); return; }
try {
const merged = turf.union(turf.featureCollection(polygons));
if (!merged) throw new Error("le aree non producono un risultato");
rememberMapChange();
merged.properties = { ...(polygons[0].properties || {}), name: "Unione aree", _lmeShape: "polygon", _lmeManagerId: undefined };
addDrawingFeature(merged, true);
polygons.forEach((_feature, index) => removeRecordWithoutPrompt(records.filter(record => record.layer instanceof L.Polygon)[index]));
lmeState.managerSelectedObjectIds.clear();
finishManagedGeometryChange("Aree unite in un nuovo oggetto.");
} catch (error) { setInfo(`Unione non riuscita: ${error.message}`); }
}
function splitManagedRecords(records) {
if (!requireProFeature("advancedGeometry")) return;
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
const polygonRecord = records.find(record => record.layer instanceof L.Polygon);
const lineRecord = records.find(record => record.layer instanceof L.Polyline && !(record.layer instanceof L.Polygon));
if (!polygonRecord || !lineRecord) { setInfo("Per dividere seleziona un poligono e una linea di taglio."); return; }
try {
const cutter = turf.buffer(lineRecord.layer.toGeoJSON(), 0.02, { units: "meters" });
const split = turf.difference(turf.featureCollection([polygonRecord.layer.toGeoJSON(), cutter]));
const parts = split ? turf.flatten(split).features : [];
if (parts.length < 2) throw new Error("la linea deve attraversare completamente il poligono");
rememberMapChange();
parts.forEach((part, index) => {
part.properties = { ...(polygonRecord.layer.feature?.properties || {}), name: `${managedObjectLabel(polygonRecord, 0)} - parte ${index + 1}`, _lmeManagerId: undefined };
addDrawingFeature(part);
});
removeRecordWithoutPrompt(polygonRecord);
lmeState.managerSelectedObjectIds.clear();
finishManagedGeometryChange(`Poligono diviso in ${parts.length} parti.`);
} catch (error) { setInfo(`Divisione non riuscita: ${error.message}`); }
}
function selectedManagedRecords() {
const current = new Map(layerManagerCategories()
.flatMap(category => managedRecordsForCategory(category))
.map(record => [record.id, record]));
lmeState.managerRecords = current;
return [...lmeState.managerSelectedObjectIds].map(id => current.get(id)).filter(Boolean);
}
function createHoleFromSelected() {
if (!requireProFeature("advancedGeometry")) return false;
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const polygons = selectedManagedRecords().filter(record => record.layer instanceof L.Polygon);
if (polygons.length !== 2) { showGeometryResult("Creazione foro", "Seleziona esattamente due poligoni: l'area esterna e l'area interna da escludere.", "warning"); return false; }
try {
const ordered = [...polygons].sort((left, right) => turf.area(right.layer.toGeoJSON()) - turf.area(left.layer.toGeoJSON()));
const outer = ordered[0].layer.toGeoJSON();
const inner = ordered[1].layer.toGeoJSON();
if (!turf.booleanWithin(inner, outer)) throw new Error("il poligono interno deve essere completamente contenuto nell'area esterna");
const result = turf.difference(turf.featureCollection([outer, inner]));
if (!result) throw new Error("nessun risultato geometrico");
rememberMapChange();
result.properties = { ...(outer.properties || {}), name: `${managedObjectLabel(ordered[0], 0)} con area esclusa`, _lmeManagerId: undefined };
addDrawingFeature(result, true);
ordered.forEach(removeRecordWithoutPrompt);
lmeState.managerSelectedObjectIds.clear();
finishManagedGeometryChange("Poligono con foro creato correttamente.");
showGeometryResult("Foro creato", "Il nuovo poligono contiene l'area interna esclusa. I due oggetti originali sono stati sostituiti.", "success");
return true;
} catch (error) { showGeometryResult("Creazione foro non riuscita", error.message, "error"); return false; }
}
function validateAndRepairSelected() {
if (!requireProFeature("advancedGeometry")) return "LME Pro richiesto";
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return "Progetto in sola lettura"; }
const records = selectedManagedRecords();
if (!records.length) { showGeometryResult("Controlla e ripara", "Seleziona uno o piรน oggetti dalla mappa, dal pannello Layer o dalla tabella oggetti.", "warning"); return "Nessuna selezione"; }
let valid = 0; let repaired = 0; let rejected = 0;
rememberMapChange();
records.forEach((record, index) => {
try {
let feature = turf.cleanCoords(record.layer.toGeoJSON(), { mutate: false });
if (feature.geometry?.type === "Polygon" && typeof turf.booleanValid === "function" && !turf.booleanValid(feature)) {
const parts = turf.unkinkPolygon(feature).features;
if (!parts.length) throw new Error("geometria non riparabile");
removeRecordWithoutPrompt(record);
parts.forEach((part, partIndex) => {
part.properties = { ...(feature.properties || {}), name: `${managedObjectLabel(record, index)} - riparato ${partIndex + 1}`, _lmeManagerId: undefined };
addDrawingFeature(part);
});
repaired++;
} else {
if (JSON.stringify(feature.geometry) !== JSON.stringify(record.layer.toGeoJSON().geometry)) {
replaceManagedLayer(record, feature); repaired++;
} else valid++;
}
} catch { rejected++; }
});
lmeState.managerSelectedObjectIds.clear();
const message = `Controllati ${records.length} oggetti: ${valid} validi, ${repaired} riparati, ${rejected} non riparabili.`;
finishManagedGeometryChange(message);
showGeometryResult("Controllo geometrie completato", message, rejected ? "warning" : "success");
return `${valid} valide, ${repaired} riparate, ${rejected} non riparabili`;
}
function compareSelectedGeometries() {
if (!requireProFeature("advancedGeometry")) return false;
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const polygons = selectedManagedRecords().filter(record => record.layer instanceof L.Polygon);
if (polygons.length !== 2) { showGeometryResult("Confronta due confini", "Seleziona esattamente due poligoni dalla mappa, dal pannello Layer o dalla tabella oggetti.", "warning"); return false; }
try {
const first = polygons[0].layer.toGeoJSON(); const second = polygons[1].layer.toGeoJSON();
const onlyFirst = turf.difference(turf.featureCollection([first, second]));
const onlySecond = turf.difference(turf.featureCollection([second, first]));
const overlap = turf.intersect(turf.featureCollection([first, second]));
rememberMapChange();
const resultLayers = [];
const addResult = (feature, name, color, role) => {
if (!feature) return;
const overlapResult = role === "overlap";
feature.properties = {
name,
_lmeSource: "Confronto confini",
_lmeColor: color,
_lmeFillColor: color,
_lmeWeight: overlapResult ? 5 : 3,
_lmeOpacity: 1,
_lmeFillOpacity: overlapResult ? 0.72 : 0.48,
_lmeComparisonResult: true,
_lmeComparisonRole: role,
_lmeManagerId: undefined
};
const layer = addDrawingFeature(feature);
if (layer) resultLayers.push(layer);
};
addResult(onlyFirst, `Solo ${managedObjectLabel(polygons[0], 0)}`, "#ef4444", "only-first");
addResult(onlySecond, `Solo ${managedObjectLabel(polygons[1], 1)}`, "#2563eb", "only-second");
addResult(overlap, "Sovrapposizione", "#16a34a", "overlap");
const comparisonIds = [...resultLayers].reverse().map(layer => managedLayerId(layer, "drawings"));
const previousOrder = lmeState.managerObjectOrder.drawings || [];
lmeState.managerObjectOrder.drawings = [
...comparisonIds,
...previousOrder.filter(id => !comparisonIds.includes(id))
];
saveLayerManagerState();
const firstArea = onlyFirst ? turf.area(onlyFirst) : 0; const secondArea = onlySecond ? turf.area(onlySecond) : 0; const overlapArea = overlap ? turf.area(overlap) : 0;
const message = `Confronto creato. Solo primo confine: ${formatArea(firstArea)}. Solo secondo confine: ${formatArea(secondArea)}. Sovrapposizione: ${formatArea(overlapArea)}.`;
lmeState.managerSelectedObjectIds.clear();
clearManagerHighlight();
finishManagedGeometryChange(message);
resultLayers.forEach(layer => layer.bringToFront?.());
showGeometryResult("Confronto completato", `${message} Sulla mappa: rosso solo primo, blu solo secondo, verde sovrapposizione.`, "success");
return true;
} catch (error) { showGeometryResult("Confronto non riuscito", error.message, "error"); return false; }
}
function isComparisonResultLayer(layer) {
const properties = layer?.feature?.properties || {};
if (properties._lmeComparisonResult || properties._lmeComparisonRole) return true;
const name = String(properties.name || "");
const color = String(properties._lmeFillColor || properties._lmeColor || "").toLowerCase();
return (name.startsWith("Solo ") && ["#ef4444", "#3b82f6", "#2563eb"].includes(color))
|| (name === "Sovrapposizione" && color === "#16a34a");
}
function clearComparisonResults() {
hideComparisonMenu();
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const layers = lmeState.drawings?.getLayers?.().filter(isComparisonResultLayer) || [];
if (!layers.length) {
setInfo("Non ci sono risultati di confronto da rimuovere.");
return false;
}
rememberMapChange();
layers.forEach(layer => {
const id = layer.feature?.properties?._lmeManagerId;
if (id) {
lmeState.managerSelectedObjectIds.delete(id);
clearManagedObjectState(id);
}
lmeState.drawings.removeLayer(layer);
});
clearManagerHighlight();
const message = `Rimossi ${layers.length} layer del confronto.`;
finishManagedGeometryChange(message);
return true;
}
function showComparisonMenu(button) {
if (lmeState.comparisonMenu) {
hideComparisonMenu();
return;
}
const menu = L.DomUtil.create("div", "lme-comparison-actions", button.parentElement);
menu.style.top = `${button.offsetTop}px`;
menu.innerHTML = `ConfrontaPulisci confronto`;
lmeState.comparisonMenu = menu;
menu.querySelectorAll("a").forEach(link => {
L.DomEvent.on(link, "click", L.DomEvent.stop)
.on(link, "mousedown", L.DomEvent.stopPropagation)
.on(link, "touchstart", L.DomEvent.stopPropagation)
.on(link, "click", () => {
const action = link.dataset.comparisonAction;
hideComparisonMenu();
if (action === "compare") compareSelectedGeometries();
else clearComparisonResults();
});
});
}
function hideComparisonMenu() {
lmeState.comparisonMenu?.remove();
lmeState.comparisonMenu = null;
}
function toggleMultiSelection() {
lmeState.multiSelectionEnabled = !lmeState.multiSelectionEnabled;
lmeState.multiSelectionButton?.classList.toggle("is-active", lmeState.multiSelectionEnabled);
lmeState.multiSelectionButton?.setAttribute("aria-pressed", String(lmeState.multiSelectionEnabled));
setInfo(lmeState.multiSelectionEnabled
? "Selezione multipla attiva: tocca gli oggetti per aggiungerli o rimuoverli dalla selezione."
: "Selezione multipla disattivata.");
return lmeState.multiSelectionEnabled;
}
function multiSelectionHasObjects() {
return lmeState.multiSelectionEnabled && lmeState.managerSelectedObjectIds.size > 0;
}
function refreshMultipleMapSelection(preferredRecord) {
clearManagerHighlight();
const selectedRecords = selectedManagedRecords();
if (!selectedRecords.length) return;
const current = selectedRecords.find(record => record.id === preferredRecord?.id) || selectedRecords.at(-1);
lmeState.managerHighlightedObjectId = current?.id || null;
lmeState.managerHighlightedCategoryKey = current?.categoryKey || null;
renderManagerHighlight(selectedRecords);
}
function showGeometryResult(title, message, tone = "success") {
setInfo(`${title}: ${message}`);
const icons = { success: "โ", warning: "!", error: "ร" };
showAppDialog(title, `
${icons[tone] || "i"}
${escapeHtml(message)}
`);
}
function showLeafletToolbarHelp() {
showAppDialog("Guida strumenti mappa", `
Gli strumenti operano direttamente sulla mappa. Per le azioni su oggetti esistenti, seleziona prima le righe nel pannello Layer, nella tabella oggetti oppure tocca un oggetto sulla mappa.
+
IngrandisciAumenta lo zoom mantenendo il centro attuale. Esempio: avvicinati a un vertice per posizionarlo con maggiore precisione.
โ
RiduciAllarga l'area visibile. Esempio: usalo per controllare insieme particelle appartenenti a fogli vicini.
โ
Punto di interesseTocca la posizione da memorizzare. Esempio: segna un cancello, un cippo o il punto di accesso al fondo.
๏ผ
LineaTocca i vertici del percorso e termina con doppio tocco o sul primo comando della barra. Durante il disegno viene mostrata la distanza; utile per un fronte strada o una recinzione.
โฌ
PoligonoTocca almeno tre vertici e chiudi la figura. Esempio: delimita un'area di intervento; l'app calcola area e perimetro.
โก
RettangoloTrascina tra due angoli opposti. Esempio: crea rapidamente l'ingombro rettangolare di un fabbricato.
โ
CerchioTocca il centro e trascina fino al raggio desiderato. Esempio: rappresenta una fascia di rispetto attorno a un pozzo.
โ
Modifica disegniPremi la matita e scegli Solo selezionato oppure Tutti. Sposta i vertici e conferma con Salva; gli oggetti bloccati devono prima essere sbloccati.
โฑ
Elimina disegniPremi il cestino e scegli Solo selezionato oppure Tutti. L'oggetto rimosso puรฒ essere recuperato con Annulla o dal cestino negli strumenti avanzati.
โ
Controlla e riparaVerifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.
โ
Crea foroSeleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.
โ
Selezione multiplaAttiva il comando e tocca piรน oggetti sulla mappa per aggiungerli o rimuoverli dalla selezione. Gli oggetti restano evidenziati anche aprendo e chiudendo i pannelli. Disattivalo per tornare alla selezione singola.
โ
Confronta due confiniApri il comando e scegli Confronta dopo aver selezionato esattamente due poligoni. Il gruppo Confronto confini viene collocato sopra gli altri layer: rosso indica solo il primo, blu solo il secondo e verde la sovrapposizione. Usa Pulisci confronto per rimuovere soltanto questi risultati.
PF
Scarica Punti FiducialiSeleziona una particella o un foglio catastale; in alternativa usa la particella rilevata dal GPS. I PF vengono aggiunti come sottogruppo del relativo foglio e richiedono LME Pro.
?
GuidaRiapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra; per uscire dalla visualizzazione rosso, blu e verde usa Pulisci confronto.
`);
}
function addGeometryToolsControl() {
if (lmeState.geometryToolsControl || !lmeState.map) return;
const GeometryTools = L.Control.extend({
options: { position: "topright" },
onAdd() {
const container = L.DomUtil.create("div", "leaflet-bar lme-geometry-toolbar");
const actions = [
{ icon: "โ", title: "Controlla e ripara geometrie", run: validateAndRepairSelected },
{ icon: "โ", title: "Crea foro da due poligoni", run: createHoleFromSelected },
{ icon: "โ", title: "Attiva o disattiva selezione multipla", run: toggleMultiSelection, multiSelection: true },
{ icon: "โ", title: "Confronta o pulisci confronto", comparison: true },
{ icon: "PF", title: "Scarica PF del foglio o della particella", run: downloadFiducialsForMapContext, fiducials: true },
{ icon: "?", title: "Guida strumenti mappa", run: showLeafletToolbarHelp }
];
actions.forEach(action => {
const button = L.DomUtil.create("a", "", container);
button.href = "#";
button.title = action.title;
button.setAttribute("role", "button");
button.setAttribute("aria-label", action.title);
button.textContent = action.icon;
if (action.fiducials) {
button.classList.add("lme-fiducial-download");
lmeState.fiducialDownloadButton = button;
}
if (action.comparison) button.classList.add("lme-comparison-button");
if (action.multiSelection) {
button.classList.add("lme-multi-selection-button");
button.setAttribute("aria-pressed", String(lmeState.multiSelectionEnabled));
lmeState.multiSelectionButton = button;
}
L.DomEvent.on(button, "click", event => {
L.DomEvent.stop(event);
if (action.comparison) showComparisonMenu(button);
else action.run();
});
});
L.DomEvent.disableClickPropagation(container);
return container;
}
});
lmeState.geometryToolsControl = new GeometryTools().addTo(lmeState.map);
updateFiducialDownloadButton();
}
function downloadFiducialsForMapContext() {
if (!requireProFeature("fiducialPoints")) return false;
const context = currentFiducialMapContext();
if (!context) {
setInfo("Seleziona una particella o un foglio catastale. Se il GPS รจ attivo puoi usare anche la particella corrente.");
return false;
}
if (!lmeState.dotNetReference) {
setInfo("Servizio PF non ancora pronto. Riprova tra poco.");
return false;
}
void lmeState.dotNetReference.invokeMethodAsync(
"DownloadFiducialsForMapSelection",
context.municipalityCode,
context.sheet,
context.label);
return true;
}
function currentFiducialMapContext() {
const selected = selectedManagedRecords();
if (!selected.length && lmeState.managerHighlightedObjectId) {
const highlighted = lmeState.managerRecords?.get?.(lmeState.managerHighlightedObjectId);
if (highlighted) selected.push(highlighted);
}
const preferred = selected.filter(record => ["selectedParcels", "gpsParcels", "importedSheets", "imported"].includes(record.categoryKey));
for (const record of preferred) {
const context = fiducialContextFromLayer(record.layer);
if (context) return context;
}
const gpsLayers = managedRecordsForCategory(layerManagerCategories().find(category => category.key === "gpsParcels"));
for (const record of gpsLayers) {
const context = fiducialContextFromLayer(record.layer);
if (context) return context;
}
return null;
}
function fiducialContextFromLayer(layer) {
const properties = layer?.feature?.properties || {};
const municipalityCode = String(properties._lmeMunicipalityCode || "").trim().toUpperCase();
const sheet = normalizeFiducialSheet(properties._lmeSheet || properties.foglio || properties.FOGLIO || properties.sheet || properties.SHEET);
if (/^[A-Z][0-9A-Z]{3}$/.test(municipalityCode) && sheet) {
return { municipalityCode, sheet, label: `Comune ${municipalityCode}, foglio ${sheet}` };
}
const references = [
properties.NATIONALCADASTRALREFERENCE,
properties.NATIONALCADASTRALZONINGREFERENCE,
properties.INSPIREID_LOCALID,
properties._lmeParcelReference,
properties.LABEL
].filter(Boolean).map(value => String(value).toUpperCase());
for (const reference of references) {
const match = reference.match(/(?:^|\.)([A-Z][0-9A-Z]{3})[_-](\d{4})(?:\d{2})?(?:[._-]|$)/);
if (match) {
const parsedSheet = normalizeFiducialSheet(match[2]);
return { municipalityCode: match[1], sheet: parsedSheet, label: `Comune ${match[1]}, foglio ${parsedSheet}` };
}
}
return null;
}
function normalizeFiducialSheet(value) {
const digits = String(value ?? "").match(/\d+/)?.[0] || "";
if (!digits) return "";
return String(Number.parseInt(digits, 10));
}
function updateFiducialDownloadButton() {
const button = lmeState.fiducialDownloadButton;
if (!button) return;
const enabled = Boolean(lmeState.entitlements?.fiducialPoints && currentFiducialMapContext());
button.classList.toggle("is-disabled", !enabled);
button.setAttribute("aria-disabled", String(!enabled));
button.title = enabled
? "Scarica PF del foglio o della particella"
: "Seleziona una particella o un foglio per scaricare i PF";
}
async function numericEditManagedRecord(record) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
if (record.layer?.feature?.properties?._lmeLocked) { setInfo("Sblocca prima l'oggetto."); return; }
if (record.layer instanceof L.Circle) {
const center = record.layer.getLatLng();
const result = await showFormDialog("Modifica cerchio", [{ name: "coordinates", label: "Latitudine, longitudine, raggio in metri", type: "textarea", rows: 3, value: `${center.lat.toFixed(7)}, ${center.lng.toFixed(7)}, ${record.layer.getRadius().toFixed(2)}` }], "Applica");
if (!result) return;
const value = result.coordinates;
const parts = value.split(/[;, ]+/).map(Number).filter(Number.isFinite);
if (parts.length < 3) { setInfo("Inserisci latitudine, longitudine e raggio validi."); return; }
rememberMapChange(); record.layer.setLatLng([parts[0], parts[1]]); record.layer.setRadius(parts[2]); annotateDrawingLayer(record.layer, "circle");
} else if (typeof record.layer?.getLatLng === "function") {
const point = record.layer.getLatLng();
const result = await showFormDialog("Modifica punto", [{ name: "coordinates", label: "Latitudine, longitudine", type: "textarea", rows: 3, value: `${point.lat.toFixed(7)}, ${point.lng.toFixed(7)}` }], "Applica");
if (!result) return;
const value = result.coordinates;
const parts = value.split(/[;, ]+/).map(Number).filter(Number.isFinite);
if (parts.length < 2) { setInfo("Inserisci coordinate valide."); return; }
rememberMapChange(); record.layer.setLatLng([parts[0], parts[1]]);
} else {
const lines = lineLatLngGroups(record.layer.getLatLngs());
const current = lines[0]?.map(point => `${point.lat.toFixed(7)},${point.lng.toFixed(7)}`).join("; ") || "";
const result = await showFormDialog("Modifica vertici", [{ name: "coordinates", label: "Coppie latitudine,longitudine separate da punto e virgola", type: "textarea", rows: 8, value: current }], "Applica");
if (!result) return;
const value = result.coordinates;
const points = value.split(";").map(pair => pair.split(",").map(Number)).filter(pair => pair.length === 2 && pair.every(Number.isFinite));
if (points.length < 2) { setInfo("Servono almeno due vertici validi."); return; }
rememberMapChange(); record.layer.setLatLngs(points.map(point => L.latLng(point[0], point[1])));
}
finishManagedGeometryChange("Coordinate aggiornate.");
}
function replaceManagedLayer(record, feature) {
const parent = record.parent?.hasLayer?.(record.layer) ? record.parent : lmeState.drawings;
parent.removeLayer?.(record.layer);
clearManagedObjectState(record.id);
feature.properties = { ...(record.layer.feature?.properties || {}), ...(feature.properties || {}), _lmeManagerId: undefined };
addDrawingFeature(feature);
}
function removeRecordWithoutPrompt(record) {
record.parent?.removeLayer?.(record.layer); clearManagedObjectState(record.id);
}
function finishManagedGeometryChange(message) {
refreshImportedMeasurements(); refreshDrawingMeasurements(); refreshPermanentLabels(); updateGpsInsideState(); refreshLayerManager(); finishMapChange(); setInfo(message);
}
function rememberMapChange() {
if (lmeState.historyBusy || !lmeState.map) return;
const snapshot = createProjectSnapshot(true);
if (lmeState.undoStack.at(-1) !== snapshot) lmeState.undoStack.push(snapshot);
if (lmeState.undoStack.length > 30) lmeState.undoStack.shift();
lmeState.redoStack = [];
}
function finishMapChange() {
if (lmeState.historyBusy || !lmeState.map) return;
const action = arguments[0] || "Modifica mappa";
lmeState.isDirty = true;
lmeState.auditLog.push({ at: new Date().toISOString(), action });
if (lmeState.auditLog.length > 250) lmeState.auditLog.shift();
if (lmeState.autoSaveEnabled) {
try { localStorage.setItem("lmeAutosaveV3", createProjectSnapshot(true)); } catch { }
}
window.clearTimeout(lmeState.changeNotifyTimer);
lmeState.changeNotifyTimer = window.setTimeout(() => {
void lmeState.dotNetReference?.invokeMethodAsync("OnMapChanged", action);
}, 450);
}
function getHistoryState() {
return { canUndo: lmeState.undoStack.length > 0, canRedo: lmeState.redoStack.length > 0, dirty: lmeState.isDirty };
}
function markProjectSaved() {
lmeState.isDirty = false;
void lmeState.dotNetReference?.invokeMethodAsync("OnProjectSaved");
}
function confirmDiscardChanges(actionLabel) {
return !lmeState.isDirty || window.confirm(`Ci sono modifiche non salvate. Continuare e ${actionLabel || "abbandonarle"}?`);
}
function setProjectReadOnly(enabled) {
lmeState.readOnly = Boolean(enabled);
document.querySelector(".map-app")?.classList.toggle("project-readonly", lmeState.readOnly);
if (lmeState.readOnly) setDrawToolsVisible(false);
refreshLayerManager();
setInfo(lmeState.readOnly ? "Progetto aperto in sola lettura. Duplicalo per modificarlo." : "Progetto modificabile.");
}
function undoMapChange() {
if (!lmeState.undoStack.length) { setInfo("Nessuna operazione da annullare."); return; }
lmeState.redoStack.push(createProjectSnapshot(true));
restoreHistorySnapshot(lmeState.undoStack.pop(), "Operazione annullata.");
lmeState.isDirty = true;
}
function redoMapChange() {
if (!lmeState.redoStack.length) { setInfo("Nessuna operazione da ripristinare."); return; }
lmeState.undoStack.push(createProjectSnapshot(true));
restoreHistorySnapshot(lmeState.redoStack.pop(), "Operazione ripristinata.");
lmeState.isDirty = true;
}
function restoreLastTrashItem() {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const entry = lmeState.trash.pop();
if (!entry) { setInfo("Il cestino e' vuoto."); return false; }
const drawingsToRestore = entry.items.filter(item =>
!["imported", "importedSheets", "fiducials"].includes(item.categoryKey)).length;
if (!canAddDrawingObjects(drawingsToRestore)) {
lmeState.trash.push(entry);
return false;
}
rememberMapChange();
entry.items.forEach(item => {
if (item.categoryKey === "imported") addImportedFeatures({ type: "FeatureCollection", features: [item.feature] });
else if (item.categoryKey === "importedSheets") addImportedSheetFeatures({ type: "FeatureCollection", features: [item.feature] });
else if (item.categoryKey === "fiducials") {
L.geoJSON(item.feature, {
pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng),
onEachFeature: (feature, layer) => bindFiducialPopup(feature, layer)
}).eachLayer(layer => lmeState.fiducials.addLayer(layer));
}
else addDrawingFeature(item.feature);
});
refreshLayerManager();
finishManagedGeometryChange(`Ripristinati ${entry.items.length} oggetti dal cestino.`);
return true;
}
function emptyTrash() {
if (!lmeState.trash.length) { setInfo("Il cestino e' gia vuoto."); return 0; }
if (!window.confirm(`Eliminare definitivamente ${lmeState.trash.reduce((sum, entry) => sum + entry.items.length, 0)} oggetti dal cestino?`)) return lmeState.trash.length;
lmeState.trash = [];
setInfo("Cestino svuotato.");
return 0;
}
function restoreHistorySnapshot(snapshot, message) {
lmeState.historyBusy = true;
try { loadProjectSnapshot(snapshot, "cronologia"); setInfo(message); } finally { lmeState.historyBusy = false; }
}
function restoreAutosave() {
const snapshot = localStorage.getItem("lmeAutosaveV3");
if (!snapshot) { setInfo("Nessun recupero automatico disponibile."); return false; }
rememberMapChange(); loadProjectSnapshot(snapshot, "recupero automatico"); return true;
}
function setAdvancedOptions(options) {
const value = options || {};
lmeState.snapEnabled = value.snapEnabled !== false;
lmeState.snapDistancePixels = Math.max(4, Math.min(40, Number(value.snapDistancePixels) || 14));
lmeState.autoSaveEnabled = value.autoSaveEnabled !== false;
lmeState.autoFitSelectedObject = Boolean(value.autoFitSelectedObject);
lmeState.gpsProximityEnabled = value.gpsProximityEnabled !== false;
lmeState.gpsBoundaryAlertEnabled = Boolean(value.gpsBoundaryAlertEnabled);
}
function compareProjectSnapshots(currentRaw, previousRaw) {
const current = typeof currentRaw === "string" ? JSON.parse(currentRaw) : currentRaw;
const previous = typeof previousRaw === "string" ? JSON.parse(previousRaw) : previousRaw;
const collect = snapshot => ["drawings", "imported", "importedSheets", "selectedParcels"]
.flatMap(key => snapshot?.[key]?.features || []);
const keyFor = (feature, index) => feature?.properties?._lmeManagerId
|| `${feature?.properties?.name || feature?.properties?.Name || "object"}:${index}`;
const currentMap = new Map(collect(current).map((feature, index) => [keyFor(feature, index), JSON.stringify(feature)]));
const previousMap = new Map(collect(previous).map((feature, index) => [keyFor(feature, index), JSON.stringify(feature)]));
let added = 0; let removed = 0; let changed = 0;
currentMap.forEach((value, key) => { if (!previousMap.has(key)) added++; else if (previousMap.get(key) !== value) changed++; });
previousMap.forEach((_value, key) => { if (!currentMap.has(key)) removed++; });
const message = `Confronto revisione: ${added} aggiunti, ${removed} rimossi, ${changed} modificati.`;
setInfo(message);
return message;
}
function renderLayerManagerVisibilityOption(label, checked, onChange) {
const option = document.createElement("label");
option.className = "layer-manager-suboption";
option.innerHTML = `${escapeHtml(label)}`;
const checkbox = option.querySelector("input");
checkbox.addEventListener("click", event => event.stopPropagation());
checkbox.addEventListener("change", () => onChange(checkbox.checked));
return option;
}
function geometryVisibilityLabel(layer) {
if (layer instanceof L.Polygon || layer instanceof L.Circle) {
return "Area/geometria sulla mappa";
}
if (layer instanceof L.Polyline) {
return "Linea sulla mappa";
}
return "Punto sulla mappa";
}
function supportsManagedMeasurements(record) {
return ["imported", "drawings"].includes(record.categoryKey)
&& (record.layer instanceof L.Circle
|| record.layer instanceof L.Polygon
|| record.layer instanceof L.Polyline);
}
function managedMeasurementsGloballyVisible(categoryKey) {
return categoryKey === "imported"
? lmeState.showImportedMeasurements
: lmeState.showDrawingMeasurements;
}
function managedMeasurementKinds(record) {
if (record.layer instanceof L.Polygon) {
return [
{ key: "summary", label: "Superficie e perimetro sulla mappa" },
{ key: "sides", label: "Misure dei lati sulla mappa" }
];
}
if (record.layer instanceof L.Circle) {
return [{ key: "summary", label: "Superficie, raggio e circonferenza sulla mappa" }];
}
return [
{ key: "summary", label: "Lunghezza totale sulla mappa" },
{ key: "sides", label: "Misure dei tratti sulla mappa" }
];
}
function managedMeasurementHiddenSet(kind) {
return kind === "sides"
? lmeState.managerHiddenMeasurementSideIds
: lmeState.managerHiddenMeasurementSummaryIds;
}
function setManagedMeasurementKindVisible(record, kind, visible) {
const hiddenSet = managedMeasurementHiddenSet(kind);
if (visible) {
if (!managedMeasurementsGloballyVisible(record.categoryKey)) {
lmeState.managerRecords.forEach(candidate => {
if (candidate.categoryKey === record.categoryKey
&& supportsManagedMeasurements(candidate)) {
lmeState.managerHiddenMeasurementSummaryIds.add(candidate.id);
lmeState.managerHiddenMeasurementSideIds.add(candidate.id);
}
});
if (record.categoryKey === "imported") {
lmeState.showImportedMeasurements = true;
} else {
lmeState.showDrawingMeasurements = true;
}
void lmeState.dotNetReference?.invokeMethodAsync(
"SetMeasurementsFromLayerManager",
record.categoryKey,
true);
}
hiddenSet.delete(record.id);
} else {
hiddenSet.add(record.id);
}
refreshImportedMeasurements();
refreshDrawingMeasurements();
refreshLayerManager();
}
function captureLayerManagerExpansion(container) {
container.querySelectorAll("details[data-expansion-key]").forEach(row => {
if (row.open) {
lmeState.managerExpandedRows.add(row.dataset.expansionKey);
} else {
lmeState.managerExpandedRows.delete(row.dataset.expansionKey);
}
});
}
function bindLayerManagerExpansion(row, key) {
row.dataset.expansionKey = key;
row.open = lmeState.managerExpandedRows.has(key);
row.addEventListener("toggle", () => {
if (row.open) {
lmeState.managerExpandedRows.add(key);
} else {
lmeState.managerExpandedRows.delete(key);
}
const button = row.querySelector(":scope > summary > .layer-expand-button");
if (button) {
button.textContent = row.open ? "โพ" : "โธ";
button.setAttribute("aria-expanded", String(row.open));
}
});
}
function layerExpandButton(expanded, label) {
return ``;
}
function bindLayerManagerToggleButton(row) {
const summary = row.querySelector(":scope > summary");
const button = summary?.querySelector(".layer-expand-button");
summary?.addEventListener("click", event => event.preventDefault());
button?.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
row.open = !row.open;
});
}
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 === "fiducials") {
lmeState.fiducialsVisible = visible;
} else if (category.key === "gpsPosition") {
lmeState.gpsPositionVisible = visible;
} else if (category.key === "compass") {
lmeState.compassVisible = visible;
void lmeState.dotNetReference?.invokeMethodAsync("SetCompassFromLayerManager", visible);
if (!visible) {
lmeState.map.setHeading?.(null);
lmeState.map.setBearing?.(0);
}
}
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();
refreshPermanentLabels();
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;
}
if (categoryKey === "fiducials") {
return lmeState.fiducialsVisible;
}
return true;
}
function highlightManagedObject(record) {
if (lmeState.managerHighlightedObjectId === record.id) {
clearManagerHighlight();
return;
}
clearManagerHighlight();
lmeState.managerHighlightedObjectId = record.id;
lmeState.managerHighlightedCategoryKey = record.categoryKey;
document.querySelectorAll(".layer-manager-object").forEach(row => {
row.classList.toggle("layer-manager-object-selected", row.dataset.objectId === record.id);
});
setSelectedDrawing(record.categoryKey === "drawings" ? record.layer : null);
renderManagerHighlight([record]);
if (lmeState.autoFitSelectedObject) {
fitLayer(record.layer);
}
revealManagedRecordInLayerManager(record.id);
}
function revealManagedRecordInLayerManager(recordId) {
const row = document.querySelector(`.layer-manager-object[data-object-id="${CSS.escape(recordId)}"]`);
if (!row) return;
const block = row.closest(".layer-manager-block");
const group = row.closest(".layer-manager-group");
if (group) group.classList.add("layer-manager-selection-path");
if (block) block.classList.add("layer-manager-selection-path");
row.classList.add("layer-manager-object-selected");
row.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
function drawingEditToolbar() {
return lmeState.drawControl?._toolbars?.[L.EditToolbar.TYPE] || null;
}
function drawingEditActive() {
return Boolean(drawingEditToolbar()?.enabled?.());
}
function updateDrawingSelectionActions() {
const hasSelection = Boolean(lmeState.drawingSelection?.getLayers?.().length);
const hasDrawings = Boolean(lmeState.drawings?.getLayers?.().length);
lmeState.drawingDeleteButton?.classList.toggle("leaflet-disabled", !hasDrawings);
if (lmeState.drawingDeleteButton) {
lmeState.drawingDeleteButton.setAttribute("aria-disabled", String(!hasDrawings));
}
lmeState.drawingScopeMenu?.querySelector("[data-drawing-scope='selected']")
?.setAttribute("aria-disabled", String(!hasSelection));
}
function setSelectedDrawing(layer) {
if (!lmeState.drawingSelection || drawingEditActive()) {
return;
}
lmeState.drawingSelection.clearLayers();
if (layer) {
lmeState.drawingSelection.addLayer(layer);
}
updateDrawingSelectionActions();
}
function selectDrawingLayer(layer) {
if (drawingEditActive()) {
return;
}
const record = {
id: managedLayerId(layer, "drawings"),
categoryKey: "drawings",
layer,
parent: lmeState.drawings,
source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente")
};
clearManagerHighlight();
lmeState.managerHighlightedObjectId = record.id;
lmeState.managerHighlightedCategoryKey = "drawings";
document.querySelectorAll(".layer-manager-object").forEach(row => {
row.classList.toggle("layer-manager-object-selected", row.dataset.objectId === record.id);
});
setSelectedDrawing(layer);
renderManagerHighlight([record]);
setInfo(`Selezione: ${managedObjectLabel(record, 0)}. Usa la matita per modificare o il cestino per eliminare.`);
}
function bindManagedParentHighlight(row, key, records) {
const summary = row.querySelector(":scope > summary");
row.dataset.highlightKey = key;
row.classList.toggle("layer-manager-parent-selected", lmeState.managerHighlightedParentKey === key);
if (lmeState.managerHighlightedParentKey === key) {
lmeState.managerHighlightedParentRecords = records;
}
row.querySelector(":scope > summary > .layer-manager-title").addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
highlightManagedParent(row, key, records);
});
summary.addEventListener("pointerenter", () => {
lmeState.managerHoveredParentKey = key;
lmeState.managerHoveredParentRecords = records;
row.classList.add("layer-manager-parent-highlighted");
renderManagerHighlight(records);
});
summary.addEventListener("pointerleave", () => {
if (lmeState.managerHoveredParentKey !== key) {
return;
}
lmeState.managerHoveredParentKey = null;
lmeState.managerHoveredParentRecords = null;
row.classList.remove("layer-manager-parent-highlighted");
renderSelectedManagerHighlight();
});
}
function highlightManagedParent(row, key, records) {
if (lmeState.managerHighlightedParentKey === key) {
clearManagerHighlight();
return;
}
clearManagerHighlight();
lmeState.managerHighlightedParentKey = key;
lmeState.managerHighlightedParentRecords = records;
lmeState.managerHighlightedCategoryKey = records[0]?.categoryKey || null;
setSelectedDrawing(null);
row.classList.add("layer-manager-parent-selected");
renderManagerHighlight(records);
}
function renderSelectedManagerHighlight() {
if (multiSelectionHasObjects()) {
const selected = [...lmeState.managerSelectedObjectIds]
.map(id => lmeState.managerRecords.get(id))
.filter(Boolean);
renderManagerHighlight(selected);
return;
}
if (lmeState.managerHighlightedParentRecords?.length) {
renderManagerHighlight(lmeState.managerHighlightedParentRecords);
return;
}
const selected = lmeState.managerRecords.get(lmeState.managerHighlightedObjectId);
renderManagerHighlight(selected ? [selected] : []);
}
function renderManagerHighlight(records) {
restoreDirectManagerHighlights();
lmeState.managerHighlight?.clearLayers();
records.forEach(record => applyDirectManagerHighlight(record.layer));
const features = records.flatMap(record => {
const geojson = record.layer?.toGeoJSON?.();
if (!geojson) {
return [];
}
return geojson.type === "FeatureCollection" ? geojson.features : [geojson];
});
if (!features.length) {
return;
}
L.geoJSON({ type: "FeatureCollection", features }, {
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);
}
function applyDirectManagerHighlight(layer) {
if (!layer || typeof layer.setStyle !== "function" || lmeState.managerDirectHighlightStyles.has(layer)) return;
const options = layer.options || {};
lmeState.managerDirectHighlightStyles.set(layer, {
color: options.color,
weight: options.weight,
opacity: options.opacity,
fillColor: options.fillColor,
fillOpacity: options.fillOpacity,
dashArray: options.dashArray ?? null
});
layer.setStyle({
color: "#ff00d4",
weight: Math.max(7, (Number(options.weight) || 2) + 4),
opacity: 1,
fillColor: "#00fff0",
fillOpacity: 0.34,
dashArray: null
});
}
function restoreDirectManagerHighlights() {
lmeState.managerDirectHighlightStyles.forEach((style, layer) => {
if (typeof layer?.setStyle === "function") layer.setStyle(style);
});
lmeState.managerDirectHighlightStyles.clear();
}
function clearManagerHighlight() {
restoreDirectManagerHighlights();
lmeState.managerHighlight?.clearLayers();
lmeState.managerHighlightedObjectId = null;
lmeState.managerHighlightedCategoryKey = null;
lmeState.managerHighlightedParentKey = null;
lmeState.managerHighlightedParentRecords = null;
lmeState.managerHoveredParentKey = null;
lmeState.managerHoveredParentRecords = null;
setSelectedDrawing(null);
document.querySelectorAll(".layer-manager-object-selected").forEach(row => {
row.classList.remove("layer-manager-object-selected");
});
document.querySelectorAll(".layer-manager-parent-highlighted").forEach(row => {
row.classList.remove("layer-manager-parent-highlighted");
});
document.querySelectorAll(".layer-manager-parent-selected").forEach(row => {
row.classList.remove("layer-manager-parent-selected");
});
}
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));
});
}
async function setImportedSheetLayerVisible(enabled) {
migrateMisclassifiedImportedSheets();
lmeState.importedSheetsVisible = Boolean(enabled);
let sheetResult = null;
if (lmeState.importedSheetsVisible) {
setInfo("Carico i fogli catastali delle particelle...");
sheetResult = await ensureImportedSheetOutlines();
}
applyImportedSheetVisibility();
const count = lmeState.importedSheets?.toGeoJSON()?.features?.length || 0;
setInfo(lmeState.importedSheetsVisible
? (count
? `${sheetResult?.added ? "Fogli catastali ufficiali caricati" : "Layer dei fogli catastali visibile"} (${count} cornici).`
: (sheetResult?.failed
? "Il servizio ufficiale dei fogli catastali non e' al momento disponibile. Riprova mostrando nuovamente il layer."
: "Nessuna particella con cui individuare il foglio catastale."))
: "Layer del foglio importato nascosto; le particelle restano visibili.");
}
function removeLegacyGeneratedSheetOutlines() {
const category = layerManagerCategories().find(item => item.key === "importedSheets");
if (!category) return 0;
const generated = managedRecordsForCategory(category).filter(record =>
Boolean(record.layer?.feature?.properties?._lmeGeneratedSheetOutline));
generated.forEach(record => {
record.parent?.removeLayer?.(record.layer);
clearManagedObjectState(record.id);
});
return generated.length;
}
function cadastralSheetIdentity(feature, index) {
const properties = feature?.properties || {};
const explicit = properties.foglio ?? properties.FOGLIO ?? properties.sheet ?? properties.SHEET;
const administrativeUnit = String(properties.ADMINISTRATIVEUNIT || "").trim().toUpperCase();
if (String(explicit ?? "").trim()) {
return `${administrativeUnit}|${String(explicit).trim().replace(/^0+/, "") || "0"}`;
}
const context = [
properties.NATIONALCADASTRALREFERENCE,
properties.INSPIREID_LOCALID,
properties._lmeParcelReference,
properties._lmeSource,
properties._lmeFolder,
properties.name,
properties.Name
].filter(Boolean).join(" ").toUpperCase();
const namedSheet = context.match(/\bFOGLIO\s*[_ .-]*0*(\d+[A-Z]?)\b/i);
const codedSheet = context.match(/\b([A-Z]\d{3})[._-]([0-9]{4}[A-Z0-9]{2})(?=[._/-]|\b)/i);
if (namedSheet) return `${administrativeUnit}|${namedSheet[1]}`;
if (codedSheet) return `${codedSheet[1]}|${codedSheet[2]}`;
return `geometria-${index}`;
}
function sheetRepresentativePoint(feature) {
try {
return turf.pointOnFeature(feature).geometry.coordinates;
} catch {
const bounds = geoJsonBounds(feature?.geometry);
return bounds ? boundsCenter(bounds) : null;
}
}
function cadastralSheetFeatureKey(feature) {
const properties = feature?.properties || {};
return String(properties.NATIONALCADASTRALZONINGREFERENCE
|| properties.INSPIREID_LOCALID
|| properties.LABEL
|| JSON.stringify(feature?.geometry || {})).trim().toUpperCase();
}
async function ensureImportedSheetOutlines() {
if (!lmeState.imported || !lmeState.importedSheets) {
return { added: 0, failed: false };
}
removeLegacyGeneratedSheetOutlines();
const existingAreaSheets = layerManagerCategories()
.flatMap(category => managedRecordsForCategory(category))
.map(record => ({ categoryKey: record.categoryKey, feature: record.layer?.toGeoJSON?.() }))
.filter(item => ["Polygon", "MultiPolygon"].includes(item.feature?.geometry?.type)
&& !item.feature?.properties?._lmeGeneratedSheetOutline
&& (item.categoryKey === "importedSheets"
|| item.feature?.properties?._lmeOfficialSheet
|| item.feature?.properties?.NATIONALCADASTRALZONINGREFERENCE
|| isSheetOrOverlayFeature(item.feature)))
.map(item => item.feature);
const parcelFeatures = [
...(lmeState.imported.toGeoJSON()?.features || []),
...(lmeState.selectedParcelsLayer?.toGeoJSON?.()?.features || [])
].filter(feature => ["Polygon", "MultiPolygon"].includes(feature?.geometry?.type));
const representatives = new Map();
parcelFeatures.forEach((feature, index) => {
const identity = cadastralSheetIdentity(feature, index);
if (!representatives.has(identity)) representatives.set(identity, feature);
});
if (!representatives.size || !lmeState.dotNetReference) {
return { added: 0, failed: false };
}
const existingKeys = new Set(existingAreaSheets.map(cadastralSheetFeatureKey));
const officialSheets = new Map();
let failures = 0;
for (const [identity, parcelFeature] of representatives) {
const point = sheetRepresentativePoint(parcelFeature);
if (!point) continue;
const existingSheet = findContainingPolygon(point, {
type: "FeatureCollection",
features: [...existingAreaSheets, ...officialSheets.values()]
});
if (existingSheet) continue;
try {
const xml = await lmeState.dotNetReference.invokeMethodAsync(
"GetOfficialCadastralSheetsNear",
point[1],
point[0]);
const candidates = gmlParcelsToGeoJson(xml).features || [];
const containing = findContainingPolygon(point, { type: "FeatureCollection", features: candidates });
const sheet = containing || candidates[0];
if (!sheet) continue;
const key = cadastralSheetFeatureKey(sheet);
if (existingKeys.has(key) || officialSheets.has(key)) continue;
const reference = String(sheet.properties?.NATIONALCADASTRALZONINGREFERENCE
|| sheet.properties?.LABEL
|| identity.split("|").pop()
|| "").trim();
sheet.properties = {
...(sheet.properties || {}),
name: reference ? `Foglio catastale ${reference}` : "Foglio catastale",
foglio: reference,
_lmeSheetIdentity: identity,
_lmeOfficialSheet: true,
_lmeSource: reference ? `Foglio catastale ${reference}` : "Fogli catastali AdE",
_lmeGroupName: reference ? `Foglio ${reference}` : "Fogli catastali AdE"
};
officialSheets.set(key, sheet);
} catch {
failures++;
}
}
if (officialSheets.size) {
addImportedSheetFeatures({ type: "FeatureCollection", features: [...officialSheets.values()] });
applyImportedSheetVisibility();
refreshLayerManager();
finishMapChange(`${officialSheets.size} fogli catastali ufficiali caricati.`);
}
return { added: officialSheets.size, failed: failures > 0 && !officialSheets.size };
}
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: (feature, layer) => { bindStoredFeaturePopup(feature, layer); applyStyleFromProperties(layer); bindManagedMapSelection(layer, "imported", lmeState.imported); }
}).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: (feature, layer) => { bindStoredFeaturePopup(feature, layer); applyStyleFromProperties(layer); bindManagedMapSelection(layer, "importedSheets", lmeState.importedSheets); }
}).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();
refreshLayerManager();
setInfo(lmeState.showImportedMeasurements
? "Misure dei file GIS visibili."
: "Misure dei file GIS nascoste.");
}
function setDrawingMeasurementsVisible(enabled) {
lmeState.showDrawingMeasurements = Boolean(enabled);
refreshDrawingMeasurements();
refreshLayerManager();
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() {
rememberMapChange();
clearManagedCategoryState("imported");
clearManagedCategoryState("importedSheets");
lmeState.imported.clearLayers();
lmeState.importedSheets.clearLayers();
lmeState.importedSheetsVisible = false;
applyImportedSheetVisibility();
lmeState.importedMeasurements.clearLayers();
setInfo("File GIS rimossi.");
updateGpsInsideState();
refreshLayerManager();
finishMapChange();
}
function addProjectMarker(name) {
if (!canAddDrawingObjects()) return false;
rememberMapChange();
const center = lmeState.map.getCenter();
const label = (name || "Nuovo POI").trim();
const marker = L.marker(center, { pane: "drawingsMarkerPane", draggable: false }).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();
bindDrawingLayerSelection(marker);
refreshDrawingMeasurements();
refreshLayerManager();
selectDrawingLayer(marker);
finishMapChange();
return true;
}
function configureDrawLabels() {
L.drawLocal.draw.toolbar.buttons.polygon = lmeState.labels.measureArea;
L.drawLocal.draw.toolbar.buttons.polyline = lmeState.labels.measureDistance;
L.drawLocal.draw.toolbar.buttons.circle = lmeState.labels.measureCircle;
L.drawLocal.draw.toolbar.buttons.rectangle = lmeState.labels.drawRectangle;
L.drawLocal.draw.toolbar.buttons.marker = lmeState.labels.addPoi;
L.drawLocal.edit.toolbar.buttons.edit = "Scegli quali disegni modificare";
L.drawLocal.edit.toolbar.buttons.remove = "Scegli quali disegni eliminare";
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 => {
if (lmeState.readOnly) {
setInfo("Il progetto e' in sola lettura.");
return;
}
if (!canAddDrawingObjects()) return;
snapDrawingLayer(event.layer);
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();
selectDrawingLayer(event.layer);
finishMapChange();
});
lmeState.map.on(L.Draw.Event.DRAWSTART, rememberMapChange);
lmeState.map.on(L.Draw.Event.EDITSTART, rememberMapChange);
lmeState.map.on(L.Draw.Event.EDITED, event => {
event.layers.eachLayer(layer => { snapDrawingLayer(layer); annotateDrawingLayer(layer); });
refreshDrawingMeasurements();
updateGpsInsideState();
refreshLayerManager();
finishMapChange();
});
lmeState.map.on(L.Draw.Event.DELETED, event => {
event.layers?.eachLayer(layer => lmeState.drawings.removeLayer(layer));
clearManagerHighlight();
refreshDrawingMeasurements();
updateGpsInsideState();
refreshLayerManager();
finishMapChange();
});
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",
renderer: lmeState.drawingsRenderer,
color: "#f97316",
weight: 4,
fillColor: "#fb923c",
fillOpacity: 0.2
};
lmeState.drawControl = new L.Control.Draw({
position: "topright",
edit: {
featureGroup: lmeState.drawings,
remove: false,
edit: {
selectedPathOptions: {
color: "#ff00d4",
fillColor: "#00fff0",
fillOpacity: 0.16,
maintainColor: false
}
}
},
draw: {
circle: { metric: usesMetricMeasurements(), feet: usesImperialMeasurements(), showRadius: true, shapeOptions: orangeArea },
circlemarker: false,
marker: { zIndexOffset: 0 },
polygon: {
metric: usesMetricMeasurements(),
feet: usesImperialMeasurements(),
showArea: true,
allowIntersection: false,
shapeOptions: orangeArea
},
polyline: {
metric: usesMetricMeasurements(),
feet: usesImperialMeasurements(),
showLength: true,
shapeOptions: {
pane: "drawingsPane",
renderer: lmeState.drawingsRenderer,
color: "#f97316",
weight: 4,
opacity: 0.95
}
},
rectangle: { metric: usesMetricMeasurements(), feet: usesImperialMeasurements(), shapeOptions: orangeArea }
}
});
lmeState.map.addControl(lmeState.drawControl);
installDrawingScopeControls();
bindDrawingEvents();
setInfo("Barra strumenti attiva: seleziona un disegno sulla mappa, poi usa matita o cestino.");
}
function installDrawingScopeControls() {
const toolbar = drawingEditToolbar();
const container = toolbar?._toolbarContainer;
if (!container) {
return;
}
const editButton = toolbar._modes?.[L.EditToolbar.Edit.TYPE]?.button;
editButton?.addEventListener("click", event => {
event.preventDefault();
event.stopImmediatePropagation();
showDrawingScopeMenu(editButton, "edit");
}, true);
const button = L.DomUtil.create("a", "leaflet-draw-edit-remove", container);
button.href = "#";
button.title = L.drawLocal.edit.toolbar.buttons.remove;
button.setAttribute("role", "button");
button.setAttribute("aria-label", L.drawLocal.edit.toolbar.buttons.remove);
const accessibleLabel = L.DomUtil.create("span", "sr-only", button);
accessibleLabel.textContent = L.drawLocal.edit.toolbar.buttons.remove;
L.DomEvent.on(button, "click", L.DomEvent.stop)
.on(button, "mousedown", L.DomEvent.stopPropagation)
.on(button, "touchstart", L.DomEvent.stopPropagation)
.on(button, "click", () => showDrawingScopeMenu(button, "delete"));
lmeState.drawingDeleteButton = button;
updateDrawingSelectionActions();
}
function showDrawingScopeMenu(button, action) {
if (drawingEditActive()) {
setInfo("Salva o annulla la modifica corrente prima di scegliere un altro comando.");
return;
}
if (!lmeState.drawings?.getLayers?.().length) {
setInfo("Non ci sono disegni o POI su cui operare.");
return;
}
if (lmeState.drawingScopeMenu?.dataset.action === action) {
hideDrawingScopeMenu();
return;
}
hideDrawingScopeMenu();
const menu = document.createElement("ul");
menu.className = "leaflet-draw-actions lme-drawing-scope-actions";
menu.dataset.action = action;
menu.style.display = "block";
menu.style.top = `${button.offsetTop - 1}px`;
menu.innerHTML = `