Unify cadastral object selection

This commit is contained in:
livio committed 2026-10-06 00:15:05 +02:00
1 parent a0e24fc925
commit 74527638c1
4 files changed
+313 -128

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@
],
"tools" =>
[
"Tabella oggetti: apre una vista ordinata di particelle, file importati e disegni. Usa i flag per selezionare piu righe, evidenziate sulla mappa con contorno magenta e riempimento ciano; il lucchetto le protegge e la matita modifica i metadati.",
"Tabella oggetti: apre una vista ordinata di particelle catastali, file importati e disegni. La selezione e bidirezionale con mappa e pannello Layer: Singola sostituisce la scelta, Multipla aggiunge o rimuove oggetti, Disattivata deseleziona tutto e blocca nuove selezioni. Cancella selezione toglie soltanto lo stato selezionato e non elimina geometrie.",
"Colonna Note: il pulsante Note e sempre attivo e apre tutte le note dell'oggetto. La consultazione resta disponibile anche senza Pro; aggiunta, modifica ed eliminazione richiedono abbonamento o licenza manuale.",
"Copia metadati: nella maschera Metadati oggetto scegli Copia da e l'oggetto sorgente. Attiva Importa anche i valori per copiare nomi e contenuti; disattivalo per creare soltanto gli stessi campi, lasciandoli vuoti o mantenendo i valori gia presenti.",
"Coordinate WGS84 e UTM: scegli un punto sulla mappa per leggere latitudine e longitudine oppure le coordinate metriche UTM, utili per rilievi e confronti tecnici.",
+2 -2
View File
@@ -29,8 +29,8 @@
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0.23</ApplicationDisplayVersion>
<ApplicationVersion>123</ApplicationVersion>
<ApplicationDisplayVersion>1.0.24</ApplicationDisplayVersion>
<ApplicationVersion>124</ApplicationVersion>
<Version>$(ApplicationDisplayVersion)</Version>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
+31 -5
View File
@@ -913,6 +913,11 @@ input[type="checkbox"] {
white-space: nowrap;
}
.object-table-wrap tr.object-table-selected td {
background: #fce7f3;
box-shadow: inset 0 1px 0 #f0abfc, inset 0 -1px 0 #f0abfc;
}
.managed-notes-command {
display: grid;
gap: 5px;
@@ -1224,6 +1229,12 @@ input[type="checkbox"] {
font-size: 11px;
}
.object-table-legend button {
min-height: 28px;
padding: 4px 9px;
font-size: 11px;
}
.object-table-wrap input[type="checkbox"] {
width: 18px;
height: 18px;
@@ -1271,17 +1282,23 @@ input[type="checkbox"] {
opacity: 0.72;
}
.lme-geometry-toolbar a.lme-multi-selection-button.is-active {
.lme-geometry-toolbar a.lme-selection-mode-button.is-active {
background: #0f766e;
color: #fff;
box-shadow: inset 0 0 0 2px #99f6e4;
}
.lme-geometry-toolbar a.lme-selection-mode-button.is-multiple {
background: #a21caf;
box-shadow: inset 0 0 0 2px #f5d0fe;
}
.lme-geometry-toolbar {
position: relative;
}
.lme-comparison-actions {
.lme-comparison-actions,
.lme-selection-actions {
position: absolute;
right: calc(100% + 6px);
z-index: 1000;
@@ -1294,7 +1311,12 @@ input[type="checkbox"] {
box-shadow: 0 2px 8px rgb(0 0 0 / 22%);
}
.lme-geometry-toolbar .lme-comparison-actions a {
.lme-selection-actions {
min-width: 205px;
}
.lme-geometry-toolbar .lme-comparison-actions a,
.lme-geometry-toolbar .lme-selection-actions a {
display: flex !important;
width: auto !important;
height: 36px !important;
@@ -1309,12 +1331,16 @@ input[type="checkbox"] {
white-space: nowrap;
}
.lme-geometry-toolbar .lme-comparison-actions a:last-child {
.lme-geometry-toolbar .lme-comparison-actions a:last-child,
.lme-geometry-toolbar .lme-selection-actions a:last-child {
border-bottom: 0;
}
.lme-geometry-toolbar .lme-comparison-actions a:hover,
.lme-geometry-toolbar .lme-comparison-actions a:focus-visible {
.lme-geometry-toolbar .lme-comparison-actions a:focus-visible,
.lme-geometry-toolbar .lme-selection-actions a:hover,
.lme-geometry-toolbar .lme-selection-actions a:focus-visible,
.lme-geometry-toolbar .lme-selection-actions a.is-current {
background: #e8f4f1;
color: #075e54;
}
+279 -120
View File
@@ -38,8 +38,9 @@ const lmeState = {
drawingScopeMenu: null,
geometryToolsControl: null,
comparisonMenu: null,
multiSelectionEnabled: false,
multiSelectionButton: null,
selectionMenu: null,
selectionMode: "single",
selectionModeButton: null,
fiducialDownloadButton: null,
ignoreMapClickUntil: 0,
activeDrawHandler: null,
@@ -402,7 +403,7 @@ function init(dotNetReference) {
return;
}
hideDrawingScopeMenu();
if (!multiSelectionHasObjects()) clearManagerHighlight();
if (!selectionHasObjects()) clearManagerHighlight();
const info = await queryFeatureInfo(event.latlng);
if (info) {
L.popup().setLatLng(event.latlng).setContent(info).openOn(lmeState.map);
@@ -448,6 +449,10 @@ function init(dotNetReference) {
&& !event.target?.closest?.(".lme-comparison-actions, .lme-comparison-button")) {
hideComparisonMenu();
}
if (lmeState.selectionMenu
&& !event.target?.closest?.(".lme-selection-actions, .lme-selection-mode-button")) {
hideSelectionMenu();
}
if (!lmeState.managerHighlightedObjectId && !lmeState.managerHighlightedParentKey) {
return;
}
@@ -460,7 +465,7 @@ function init(dotNetReference) {
|| selectedParent?.dataset.highlightKey === lmeState.managerHighlightedParentKey) {
return;
}
if (multiSelectionHasObjects()) {
if (selectionHasObjects()) {
renderSelectedManagerHighlight();
return;
}
@@ -791,6 +796,7 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
targetGroup.bringToFront();
}
if (!normalizedParcel) {
clearManagedCategorySelection("gpsParcels");
targetGroup.clearLayers();
}
@@ -831,11 +837,11 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
feature.properties = {
...(feature.properties || {}),
_lmeParcelReference: String(ref || label || normalizedParcel),
_lmeColor: feature.properties?._lmeColor || "#ef4444",
_lmeFillColor: feature.properties?._lmeFillColor || "#fbbf24",
_lmeColor: feature.properties?._lmeColor || (normalizedParcel ? "#ef4444" : "#1d4ed8"),
_lmeFillColor: feature.properties?._lmeFillColor || (normalizedParcel ? "#fbbf24" : "#38bdf8"),
_lmeWeight: Number(feature.properties?._lmeWeight) || 4,
_lmeOpacity: Number.isFinite(Number(feature.properties?._lmeOpacity)) ? Number(feature.properties._lmeOpacity) : 1,
_lmeFillOpacity: Number.isFinite(Number(feature.properties?._lmeFillOpacity)) ? Number(feature.properties._lmeFillOpacity) : 0.26
_lmeFillOpacity: Number.isFinite(Number(feature.properties?._lmeFillOpacity)) ? Number(feature.properties._lmeFillOpacity) : (normalizedParcel ? 0.26 : 0.2)
};
const layer = L.geoJSON(feature, {
pane: normalizedParcel ? "selectedParcelsPane" : "gpsParcelsPane",
@@ -845,6 +851,7 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
}).addTo(targetGroup);
layer.lmeParcelReference = ref;
layer.bindPopup(`<strong>${escapeHtml(ref)}</strong>`);
bindParcelMapSelection(layer, normalizedParcel ? "selectedParcels" : "gpsParcels", ref);
if (normalizedParcel) {
targetGroup.bringToFront();
}
@@ -890,6 +897,16 @@ function findParcelLayerByReference(group, reference) {
return match;
}
function bindParcelMapSelection(geoJsonLayer, categoryKey, reference) {
if (!geoJsonLayer || typeof geoJsonLayer.eachLayer !== "function") return;
geoJsonLayer.eachLayer(layer => {
layer.lmeParcelReference = reference;
layer.bindPopup?.(`<strong>${escapeHtml(reference)}</strong>`);
applyStyleFromProperties(layer);
bindManagedMapSelection(layer, categoryKey, geoJsonLayer);
});
}
function restoreHiddenParcelLayerByReference(reference) {
const expected = normalizeParcelReference(reference);
const entry = [...lmeState.managerHiddenObjects.entries()].find(([, record]) =>
@@ -1979,6 +1996,7 @@ function applyLayerManagerPalette(row, key) {
}
function clearManagedObjectState(id) {
lmeState.managerSelectedObjectIds.delete(id);
lmeState.managerHiddenObjects.delete(id);
lmeState.managerHiddenObjectIds.delete(id);
lmeState.managerHiddenMeasurementSummaryIds.delete(id);
@@ -2255,7 +2273,7 @@ function refreshLayerManager() {
scrollHost.scrollTop = scrollTop;
});
}
if (multiSelectionHasObjects()) renderSelectedManagerHighlight();
if (selectionHasObjects()) renderSelectedManagerHighlight();
}
function renderLayerManagerCategory(category, records) {
@@ -2389,7 +2407,7 @@ function renderLayerManagerBlock(category, source, records) {
function renderLayerManagerObject(record, index) {
const object = document.createElement("details");
object.className = "layer-manager-object";
if (record.id === lmeState.managerHighlightedObjectId) {
if (record.id === lmeState.managerHighlightedObjectId || lmeState.managerSelectedObjectIds.has(record.id)) {
object.classList.add("layer-manager-object-selected");
}
object.dataset.dragType = "object";
@@ -2471,7 +2489,7 @@ function renderLayerSelectionToolbar() {
const toolbar = document.createElement("section");
toolbar.className = "layer-selection-toolbar";
toolbar.innerHTML = `<label>Cerca oggetti<input type="search" value="${escapeHtml(lmeState.managerSearchText)}" placeholder="Nome o gruppo"></label>
<div class="layer-selection-summary"><strong>${selectedCount} selezionati</strong><button type="button" data-action="clear">Deseleziona</button></div>
<div class="layer-selection-summary"><strong>${selectedCount} selezionati</strong><button type="button" data-action="clear" title="Toglie la selezione senza eliminare oggetti">Cancella selezione</button></div>
<div class="layer-batch-actions">
<button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="move">Sposta geometria</button>
<button type="button" data-action="buffer">Buffer</button><button type="button" data-action="union">Unisci</button>
@@ -2493,8 +2511,7 @@ function renderLayerSelectionToolbar() {
const records = [...lmeState.managerSelectedObjectIds]
.map(id => lmeState.managerRecords?.get(id)).filter(Boolean);
if (button.dataset.action === "clear") {
lmeState.managerSelectedObjectIds.clear();
refreshLayerManager();
clearManagedSelection();
} else if (!records.length) {
setInfo("Seleziona almeno un oggetto dalle sue opzioni.");
} else if (button.dataset.action === "duplicate") duplicateManagedRecords(records);
@@ -2514,16 +2531,19 @@ function renderManagedGroupTools(records) {
const properties = firstLayer?.feature?.properties || {};
const lineColor = normalizedColor(properties._lmeColor || firstLayer?.options?.color, "#16806a");
const fillColor = normalizedColor(properties._lmeFillColor || firstLayer?.options?.fillColor, lineColor);
const selectionColor = normalizedColor(properties._lmeSelectionColor, "#d500f9");
tools.innerHTML = `<strong>Azioni gruppo</strong><span class="layer-group-totals">${escapeHtml(managedGroupTotals(records))}</span><div class="layer-style-controls">
${managedColorEditorMarkup("color", "Colore linea", lineColor)}
${managedColorEditorMarkup("fillColor", "Colore riempimento", fillColor)}
${managedColorEditorMarkup("selectionColor", "Colore selezione", selectionColor)}
<label>Spessore<input type="range" data-field="weight" min="1" max="10" step="1" value="${Number(properties._lmeWeight) || Number(firstLayer?.options?.weight) || 3}"></label>
<label>Opacita<input type="range" data-field="opacity" min="0.1" max="1" step="0.1" value="${Number.isFinite(Number(properties._lmeOpacity)) ? Number(properties._lmeOpacity) : Number(firstLayer?.options?.opacity) || 1}"></label></div>
<div class="layer-compact-actions"><button type="button" data-group-action="select">Seleziona tutti</button><button type="button" data-group-action="lock">Blocca/sblocca</button><button type="button" data-group-action="labels">Etichette</button></div>`;
<div class="layer-compact-actions"><button type="button" data-group-action="select" ${lmeState.selectionMode === "off" ? "disabled" : ""}>Seleziona tutti</button><button type="button" data-group-action="lock">Blocca/sblocca</button><button type="button" data-group-action="labels">Etichette</button></div>`;
const applyCurrentStyle = () => {
applyManagedStyle(records, {
color: tools.querySelector("[data-color-picker='color']").value,
fillColor: tools.querySelector("[data-color-picker='fillColor']").value,
selectionColor: tools.querySelector("[data-color-picker='selectionColor']").value,
weight: Number(tools.querySelector("[data-field='weight']").value),
opacity: Number(tools.querySelector("[data-field='opacity']").value)
});
@@ -2531,7 +2551,18 @@ function renderManagedGroupTools(records) {
bindManagedColorEditors(tools, applyCurrentStyle);
tools.querySelectorAll("input[type='range']").forEach(input => input.addEventListener("change", applyCurrentStyle));
tools.querySelector("[data-group-action='select']").addEventListener("click", () => {
records.forEach(record => lmeState.managerSelectedObjectIds.add(record.id)); refreshLayerManager();
if (lmeState.selectionMode === "off") {
setInfo("Selezione disattivata. Scegli Singola o Multipla dalla barra mappa.");
return;
}
if (lmeState.selectionMode === "single") {
selectManagedRecord(records[0]);
return;
}
records.forEach(record => lmeState.managerSelectedObjectIds.add(record.id));
refreshLayerManager();
refreshMultipleMapSelection(records.at(-1));
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
});
tools.querySelector("[data-group-action='lock']").addEventListener("click", () => {
const lock = !records.every(record => Boolean(record.layer?.feature?.properties?._lmeLocked));
@@ -2658,11 +2689,13 @@ function renderManagedObjectTools(record) {
tools.className = "layer-object-tools";
const lineColor = normalizedColor(properties._lmeColor || record.layer?.options?.color, "#f97316");
const fillColor = normalizedColor(properties._lmeFillColor || record.layer?.options?.fillColor, lineColor);
tools.innerHTML = `<label class="layer-manager-suboption"><input type="checkbox" data-field="selected" ${selected ? "checked" : ""}><span>Seleziona per azioni multiple</span></label>
const selectionColor = normalizedColor(properties._lmeSelectionColor, "#d500f9");
tools.innerHTML = `<label class="layer-manager-suboption"><input type="checkbox" data-field="selected" ${selected ? "checked" : ""} ${lmeState.selectionMode === "off" ? "disabled" : ""}><span>Selezionato (${escapeHtml(selectionModeLabel().toLocaleLowerCase("it"))})</span></label>
<label class="layer-manager-suboption"><input type="checkbox" data-field="locked" ${locked ? "checked" : ""}><span>Blocca modifiche e rimozione</span></label>
<label class="layer-manager-suboption"><input type="checkbox" data-field="label" ${properties._lmeLabelVisible ? "checked" : ""}><span>Etichetta permanente sulla mappa</span></label>
<div class="layer-style-controls">${managedColorEditorMarkup("color", "Colore linea", lineColor)}
${managedColorEditorMarkup("fillColor", "Colore riempimento", fillColor)}
${managedColorEditorMarkup("selectionColor", "Colore selezione", selectionColor)}
<label>Spessore<input type="range" data-field="weight" min="1" max="10" step="1" value="${Number(properties._lmeWeight) || 3}"></label>
<label>Opacita<input type="range" data-field="opacity" min="0.1" max="1" step="0.1" value="${Number(properties._lmeOpacity) || 1}"></label>
<label>Riempimento %<input type="range" data-field="fillOpacity" min="0" max="0.8" step="0.05" value="${Number.isFinite(Number(properties._lmeFillOpacity)) ? Number(properties._lmeFillOpacity) : 0.2}"></label>
@@ -2672,15 +2705,25 @@ function renderManagedObjectTools(record) {
${properties._lmePhoto ? `<img class="layer-photo-preview" src="${properties._lmePhoto}" alt="Foto allegata"><button type="button" data-action="remove-photo">Rimuovi foto</button>` : ""}
<div class="layer-compact-actions"><button type="button" data-action="attributes">Attributi</button><button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="numeric">Coordinate</button><button type="button" data-action="inspect">WGS84 / UTM</button><button type="button" data-action="move">Sposta geometria</button><button type="button" data-action="buffer">Buffer</button></div>`;
tools.querySelector("[data-field='selected']").addEventListener("change", event => {
if (event.target.checked) lmeState.managerSelectedObjectIds.add(record.id);
else lmeState.managerSelectedObjectIds.delete(record.id);
refreshLayerManager();
if (lmeState.selectionMode === "off") {
event.target.checked = lmeState.managerSelectedObjectIds.has(record.id);
setInfo("Selezione disattivata. Scegli Singola o Multipla dalla barra mappa.");
return;
}
if (event.target.checked) {
selectManagedRecord(record);
} else {
lmeState.managerSelectedObjectIds.delete(record.id);
refreshLayerManager();
refreshMultipleMapSelection();
}
});
tools.querySelector("[data-field='locked']").addEventListener("change", event => updateManagedProperties([record], value => { value._lmeLocked = event.target.checked; }));
tools.querySelector("[data-field='label']").addEventListener("change", event => updateManagedProperties([record], value => { value._lmeLabelVisible = event.target.checked; }));
const applyCurrentStyle = () => applyManagedStyle([record], {
color: tools.querySelector("[data-color-picker='color']").value,
fillColor: tools.querySelector("[data-color-picker='fillColor']").value,
selectionColor: tools.querySelector("[data-color-picker='selectionColor']").value,
weight: Number(tools.querySelector("[data-field='weight']").value),
opacity: Number(tools.querySelector("[data-field='opacity']").value),
fillOpacity: Number(tools.querySelector("[data-field='fillOpacity']").value),
@@ -3005,24 +3048,35 @@ function showObjectTable() {
const details = managedObjectDetails(record.layer);
const selected = lmeState.managerSelectedObjectIds.has(record.id);
const locked = Boolean(properties._lmeLocked);
return `<tr data-object-id="${escapeHtml(record.id)}"><td><input type="checkbox" data-select="${escapeHtml(record.id)}" ${selected ? "checked" : ""} aria-label="Seleziona ${escapeHtml(managedObjectLabel(record, index))}"></td><td><input type="checkbox" data-lock="${escapeHtml(record.id)}" ${locked ? "checked" : ""} ${lmeState.readOnly ? "disabled" : ""} aria-label="Blocca ${escapeHtml(managedObjectLabel(record, index))}"></td><td><button type="button" data-locate="${escapeHtml(record.id)}" title="Mostra sulla mappa" aria-label="Mostra sulla mappa">⌖</button></td><td><button type="button" data-attributes="${escapeHtml(record.id)}" title="Modifica metadati" aria-label="Modifica metadati di ${escapeHtml(managedObjectLabel(record, index))}" ${lmeState.readOnly ? "disabled" : ""}>✎</button></td><td><button type="button" class="object-notes-button" data-notes="${escapeHtml(record.id)}" title="Visualizza note">Note (${managedNotes(properties).length})</button></td><td>${escapeHtml(managedObjectLabel(record, index))}</td><td>${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}</td><td>${escapeHtml(properties.foglio || properties.FOGLIO || "")}</td><td>${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}</td><td>${escapeHtml(details.slice(0, 2).join(" | "))}</td></tr>`;
return `<tr class="${selected ? "object-table-selected" : ""}" data-object-id="${escapeHtml(record.id)}"><td><input type="checkbox" data-select="${escapeHtml(record.id)}" ${selected ? "checked" : ""} ${lmeState.selectionMode === "off" ? "disabled" : ""} aria-label="Seleziona ${escapeHtml(managedObjectLabel(record, index))}"></td><td><input type="checkbox" data-lock="${escapeHtml(record.id)}" ${locked ? "checked" : ""} ${lmeState.readOnly ? "disabled" : ""} aria-label="Blocca ${escapeHtml(managedObjectLabel(record, index))}"></td><td><button type="button" data-locate="${escapeHtml(record.id)}" title="Mostra sulla mappa" aria-label="Mostra sulla mappa">⌖</button></td><td><button type="button" data-attributes="${escapeHtml(record.id)}" title="Modifica metadati" aria-label="Modifica metadati di ${escapeHtml(managedObjectLabel(record, index))}" ${lmeState.readOnly ? "disabled" : ""}>✎</button></td><td><button type="button" class="object-notes-button" data-notes="${escapeHtml(record.id)}" title="Visualizza note">Note (${managedNotes(properties).length})</button></td><td>${escapeHtml(managedObjectLabel(record, index))}</td><td>${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}</td><td>${escapeHtml(properties.foglio || properties.FOGLIO || "")}</td><td>${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}</td><td>${escapeHtml(details.slice(0, 2).join(" | "))}</td></tr>`;
}).join("");
const dialog = showAppDialog("Elenco oggetti e particelle", `<label class="object-table-search">Filtra<input type="search" placeholder="Nome, foglio, particella o gruppo"></label><div class="object-table-legend"><span>Seleziona più oggetti con i flag nella colonna Selez.</span><span>Il lucchetto impedisce modifiche e rimozioni; la matita modifica i metadati.</span><span>Note apre l'elenco completo; le modifiche richiedono LME Pro.</span></div><div class="object-table-wrap"><table><thead><tr><th>Selez.</th><th>Blocca</th><th>Mappa</th><th>Metadati</th><th>Note</th><th>Nome</th><th>Gruppo</th><th>Foglio</th><th>Particella</th><th>Misure</th></tr></thead><tbody>${body || "<tr><td colspan='10'>Nessun oggetto</td></tr>"}</tbody></table></div>`);
const dialog = showAppDialog("Elenco oggetti e particelle", `<label class="object-table-search">Filtra<input type="search" placeholder="Nome, foglio, particella o gruppo"></label><div class="object-table-legend"><span>Modalita: <strong>${escapeHtml(selectionModeLabel())}</strong>. La selezione e condivisa con mappa e pannello Layer.</span><button type="button" data-table-clear title="Toglie la selezione senza eliminare oggetti">Cancella selezione</button><span>Il lucchetto impedisce modifiche e rimozioni; la matita modifica i metadati.</span><span>Note apre l'elenco completo; le modifiche richiedono LME Pro.</span></div><div class="object-table-wrap"><table><thead><tr><th>Selez.</th><th>Blocca</th><th>Mappa</th><th>Metadati</th><th>Note</th><th>Nome</th><th>Gruppo</th><th>Foglio</th><th>Particella</th><th>Misure</th></tr></thead><tbody>${body || "<tr><td colspan='10'>Nessun oggetto</td></tr>"}</tbody></table></div>`);
const byId = new Map(records.map(record => [record.id, record]));
dialog.querySelector("input[type='search']").addEventListener("input", event => {
const query = event.target.value.toLocaleLowerCase("it");
dialog.querySelectorAll("tbody tr").forEach(row => { row.hidden = query && !row.textContent.toLocaleLowerCase("it").includes(query); });
});
dialog.querySelector("[data-table-clear]").addEventListener("click", () => {
clearManagedSelection();
dialog.querySelectorAll("[data-select]").forEach(candidate => {
candidate.checked = false;
candidate.closest("tr")?.classList.remove("object-table-selected");
});
});
dialog.querySelectorAll("[data-select]").forEach(input => input.addEventListener("change", () => {
if (input.checked) lmeState.managerSelectedObjectIds.add(input.dataset.select);
else lmeState.managerSelectedObjectIds.delete(input.dataset.select);
const record = byId.get(input.dataset.select);
if (record && input.checked) {
lmeState.managerHighlightedObjectId = record.id;
lmeState.managerHighlightedCategoryKey = record.categoryKey;
renderManagerHighlight([record]);
if (!record) return;
if (input.checked) {
selectManagedRecord(record);
} else {
lmeState.managerSelectedObjectIds.delete(record.id);
refreshLayerManager();
refreshMultipleMapSelection();
}
refreshLayerManager();
dialog.querySelectorAll("[data-select]").forEach(candidate => {
candidate.checked = lmeState.managerSelectedObjectIds.has(candidate.dataset.select);
candidate.closest("tr")?.classList.toggle("object-table-selected", candidate.checked);
});
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
}));
dialog.querySelectorAll("[data-lock]").forEach(input => input.addEventListener("change", () => {
@@ -3033,7 +3087,9 @@ function showObjectTable() {
}));
dialog.querySelectorAll("[data-locate]").forEach(button => button.addEventListener("click", () => {
const record = byId.get(button.dataset.locate); if (!record) return;
dialog.remove(); highlightManagedObject(record);
dialog.remove();
if (lmeState.selectionMode !== "off") selectManagedRecord(record);
fitLayer(record.layer);
}));
dialog.querySelectorAll("[data-attributes]").forEach(button => button.addEventListener("click", () => {
const record = byId.get(button.dataset.attributes); if (!record) return;
@@ -3054,6 +3110,7 @@ function showAuditLog() {
function updateManagedProperties(records, change) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
restoreDirectManagerHighlights();
rememberMapChange();
records.forEach(record => {
record.layer.feature ||= { type: "Feature", properties: {} };
@@ -3073,6 +3130,7 @@ function applyManagedStyle(records, style) {
properties._lmeFillColor = style.fillColor || style.color;
properties._lmeWeight = style.weight;
properties._lmeOpacity = style.opacity;
if (style.selectionColor !== undefined) properties._lmeSelectionColor = style.selectionColor;
if (style.fillOpacity !== undefined) properties._lmeFillOpacity = style.fillOpacity;
if (style.dash !== undefined) properties._lmeDash = style.dash;
});
@@ -3081,6 +3139,7 @@ function applyManagedStyle(records, style) {
function applyStyleFromProperties(layer) {
const properties = layer?.feature?.properties || {};
if (typeof layer?.setStyle === "function") {
lmeState.managerDirectHighlightStyles.delete(layer);
layer.setStyle({
color: properties._lmeColor || layer.options.color,
fillColor: properties._lmeFillColor || layer.options.fillColor,
@@ -3419,18 +3478,8 @@ function hideComparisonMenu() {
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 selectionHasObjects() {
return lmeState.managerSelectedObjectIds.size > 0;
}
function refreshMultipleMapSelection(preferredRecord) {
@@ -3440,9 +3489,119 @@ function refreshMultipleMapSelection(preferredRecord) {
const current = selectedRecords.find(record => record.id === preferredRecord?.id) || selectedRecords.at(-1);
lmeState.managerHighlightedObjectId = current?.id || null;
lmeState.managerHighlightedCategoryKey = current?.categoryKey || null;
setSelectedDrawing(selectedRecords.length === 1 && current?.categoryKey === "drawings" ? current.layer : null);
renderManagerHighlight(selectedRecords);
}
function selectionModeLabel(mode = lmeState.selectionMode) {
return mode === "multiple" ? "Multipla" : mode === "off" ? "Disattivata" : "Singola";
}
function selectionModeIcon(mode = lmeState.selectionMode) {
return mode === "multiple" ? "☑" : mode === "off" ? "×" : "✓";
}
function updateSelectionModeButton() {
const button = lmeState.selectionModeButton;
if (!button) return;
const label = `Selezione: ${selectionModeLabel()}`;
button.textContent = selectionModeIcon();
button.title = `${label}. Apri le opzioni`;
button.setAttribute("aria-label", button.title);
button.setAttribute("aria-pressed", String(lmeState.selectionMode !== "off"));
button.classList.toggle("is-active", lmeState.selectionMode !== "off");
button.classList.toggle("is-multiple", lmeState.selectionMode === "multiple");
}
function setSelectionMode(mode) {
const normalized = ["single", "multiple", "off"].includes(mode) ? mode : "single";
lmeState.selectionMode = normalized;
if (normalized === "off") {
lmeState.managerSelectedObjectIds.clear();
clearManagerHighlight();
} else if (normalized === "single" && lmeState.managerSelectedObjectIds.size > 1) {
const selectedIds = [...lmeState.managerSelectedObjectIds];
const retained = lmeState.managerSelectedObjectIds.has(lmeState.managerHighlightedObjectId)
? lmeState.managerHighlightedObjectId
: selectedIds.at(-1);
lmeState.managerSelectedObjectIds.clear();
if (retained) lmeState.managerSelectedObjectIds.add(retained);
}
updateSelectionModeButton();
refreshLayerManager();
renderSelectedManagerHighlight();
setInfo(normalized === "single"
? "Selezione singola attiva: tocca un oggetto per sostituire la selezione corrente."
: normalized === "multiple"
? "Selezione multipla attiva: tocca gli oggetti per aggiungerli o rimuoverli."
: "Selezione disattivata e selezione corrente cancellata. Nessun oggetto e' stato rimosso.");
return normalized;
}
function clearManagedSelection(announce = true) {
lmeState.managerSelectedObjectIds.clear();
clearManagerHighlight();
refreshLayerManager();
if (announce) setInfo("Selezione cancellata. Nessun oggetto e' stato rimosso.");
}
function selectManagedRecord(record, event, announce = true) {
if (!record) return false;
if (lmeState.selectionMode === "off") {
if (announce) setInfo("Selezione disattivata. Attivala dal pulsante Selezione nella barra mappa.");
return false;
}
const multiple = lmeState.selectionMode === "multiple"
|| event?.ctrlKey || event?.metaKey || event?.shiftKey;
if (multiple) {
if (lmeState.managerSelectedObjectIds.has(record.id)) lmeState.managerSelectedObjectIds.delete(record.id);
else lmeState.managerSelectedObjectIds.add(record.id);
} else {
lmeState.managerSelectedObjectIds.clear();
lmeState.managerSelectedObjectIds.add(record.id);
}
refreshLayerManager();
refreshMultipleMapSelection(record);
if (lmeState.managerSelectedObjectIds.has(record.id)) {
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(record.id));
}
if (announce) setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati (${selectionModeLabel().toLocaleLowerCase("it")}).`);
return true;
}
function showSelectionMenu(button) {
if (lmeState.selectionMenu) {
hideSelectionMenu();
return;
}
const menu = L.DomUtil.create("div", "lme-selection-actions", button.parentElement);
menu.style.top = `${button.offsetTop}px`;
menu.innerHTML = `<a href="#" data-selection-mode="single">✓ Selezione singola</a>
<a href="#" data-selection-mode="multiple">☑ Selezione multipla</a>
<a href="#" data-selection-mode="off">× Selezione disattivata</a>
<a href="#" data-selection-clear="true">⌫ Cancella selezione</a>`;
lmeState.selectionMenu = menu;
menu.querySelectorAll("a").forEach(link => {
const active = link.dataset.selectionMode === lmeState.selectionMode;
link.classList.toggle("is-current", active);
link.setAttribute("aria-current", active ? "true" : "false");
L.DomEvent.on(link, "click", L.DomEvent.stop)
.on(link, "mousedown", L.DomEvent.stopPropagation)
.on(link, "touchstart", L.DomEvent.stopPropagation)
.on(link, "click", () => {
const mode = link.dataset.selectionMode;
hideSelectionMenu();
if (mode) setSelectionMode(mode);
else clearManagedSelection();
});
});
}
function hideSelectionMenu() {
lmeState.selectionMenu?.remove();
lmeState.selectionMenu = null;
}
function showGeometryResult(title, message, tone = "success") {
setInfo(`${title}: ${message}`);
const icons = { success: "✓", warning: "!", error: "×" };
@@ -3464,7 +3623,7 @@ function showLeafletToolbarHelp() {
<div><dt>▱</dt><dd><strong>Elimina disegni</strong>Premi il cestino e scegli Solo selezionato oppure Tutti. L'oggetto rimosso può essere recuperato con Annulla o dal cestino negli strumenti avanzati.</dd></div>
<div><dt>✓</dt><dd><strong>Controlla e ripara</strong>Verifica 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.</dd></div>
<div><dt>◉</dt><dd><strong>Crea foro</strong>Seleziona 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.</dd></div>
<div><dt>☑</dt><dd><strong>Selezione multipla</strong>Attiva 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.</dd></div>
<div><dt>✓</dt><dd><strong>Modalita di selezione</strong>Scegli Singola, Multipla o Disattivata. Mappa, pannello Layer e tabella oggetti condividono la stessa selezione. Disattivata e Cancella selezione deselezionano tutto senza eliminare alcun oggetto.</dd></div>
<div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Apri 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.</dd></div>
<div><dt>PF</dt><dd><strong>Scarica Punti Fiduciali</strong>Seleziona 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.</dd></div>
<div><dt>?</dt><dd><strong>Guida</strong>Riapre 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.</dd></div>
@@ -3481,7 +3640,7 @@ function addGeometryToolsControl() {
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: selectionModeIcon(), title: "Modalita e comandi di selezione", selection: 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 }
@@ -3498,14 +3657,15 @@ function addGeometryToolsControl() {
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;
if (action.selection) {
button.classList.add("lme-selection-mode-button");
lmeState.selectionModeButton = button;
updateSelectionModeButton();
}
L.DomEvent.on(button, "click", event => {
L.DomEvent.stop(event);
if (action.comparison) showComparisonMenu(button);
else if (action.selection) showSelectionMenu(button);
else action.run();
});
});
@@ -4000,22 +4160,10 @@ function managedCategoryEnabled(categoryKey) {
}
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 (!selectManagedRecord(record)) return;
if (lmeState.autoFitSelectedObject) {
fitLayer(record.layer);
}
revealManagedRecordInLayerManager(record.id);
}
function revealManagedRecordInLayerManager(recordId) {
@@ -4060,7 +4208,7 @@ function setSelectedDrawing(layer) {
}
function selectDrawingLayer(layer) {
if (drawingEditActive()) {
if (drawingEditActive() || lmeState.selectionMode === "off") {
return;
}
const record = {
@@ -4070,14 +4218,10 @@ function selectDrawingLayer(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]);
lmeState.managerSelectedObjectIds.clear();
lmeState.managerSelectedObjectIds.add(record.id);
refreshLayerManager();
refreshMultipleMapSelection(record);
setInfo(`Selezione: ${managedObjectLabel(record, 0)}. Usa la matita per modificare o il cestino per eliminare.`);
}
@@ -4111,6 +4255,10 @@ function bindManagedParentHighlight(row, key, records) {
}
function highlightManagedParent(row, key, records) {
if (lmeState.selectionMode === "off") {
setInfo("Selezione disattivata. Scegli Singola o Multipla dalla barra mappa.");
return;
}
if (lmeState.managerHighlightedParentKey === key) {
clearManagerHighlight();
return;
@@ -4125,7 +4273,7 @@ function highlightManagedParent(row, key, records) {
}
function renderSelectedManagerHighlight() {
if (multiSelectionHasObjects()) {
if (selectionHasObjects()) {
const selected = [...lmeState.managerSelectedObjectIds]
.map(id => lmeState.managerRecords.get(id))
.filter(Boolean);
@@ -4143,13 +4291,18 @@ function renderSelectedManagerHighlight() {
function renderManagerHighlight(records) {
restoreDirectManagerHighlights();
lmeState.managerHighlight?.clearLayers();
records.forEach(record => applyDirectManagerHighlight(record.layer));
records.forEach(record => applyDirectManagerHighlight(record));
const features = records.flatMap(record => {
const geojson = record.layer?.toGeoJSON?.();
if (!geojson) {
return [];
}
return geojson.type === "FeatureCollection" ? geojson.features : [geojson];
const selectionColor = managedSelectionColor(record.layer);
const items = geojson.type === "FeatureCollection" ? geojson.features : [geojson];
return items.map(feature => ({
...feature,
properties: { ...(feature.properties || {}), _lmeSelectionColorResolved: selectionColor }
}));
});
if (!features.length) {
return;
@@ -4157,22 +4310,34 @@ function renderManagerHighlight(records) {
L.geoJSON({ type: "FeatureCollection", features }, {
pane: "managerHighlightPane",
interactive: false,
style: { color: "#ff00d4", weight: 8, opacity: 1, fillColor: "#00fff0", fillOpacity: 0.16 },
style: feature => ({
color: managedSelectionColor(null, feature?.properties?._lmeSelectionColorResolved),
weight: 8,
opacity: 1,
fillColor: managedSelectionColor(null, feature?.properties?._lmeSelectionColorResolved),
fillOpacity: 0.22
}),
pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
pane: "managerHighlightPane",
radius: 12,
color: "#ff00d4",
color: managedSelectionColor(null, feature?.properties?._lmeSelectionColorResolved),
weight: 5,
fillColor: "#00fff0",
fillColor: managedSelectionColor(null, feature?.properties?._lmeSelectionColorResolved),
fillOpacity: 0.9,
interactive: false
})
}).addTo(lmeState.managerHighlight);
}
function applyDirectManagerHighlight(layer) {
function managedSelectionColor(layer, explicitColor) {
return normalizedColor(explicitColor || layer?.feature?.properties?._lmeSelectionColor, "#d500f9");
}
function applyDirectManagerHighlight(record) {
const layer = record?.layer;
if (!layer || typeof layer.setStyle !== "function" || lmeState.managerDirectHighlightStyles.has(layer)) return;
const options = layer.options || {};
const selectionColor = managedSelectionColor(layer);
lmeState.managerDirectHighlightStyles.set(layer, {
color: options.color,
weight: options.weight,
@@ -4182,10 +4347,10 @@ function applyDirectManagerHighlight(layer) {
dashArray: options.dashArray ?? null
});
layer.setStyle({
color: "#ff00d4",
color: selectionColor,
weight: Math.max(7, (Number(options.weight) || 2) + 4),
opacity: 1,
fillColor: "#00fff0",
fillColor: selectionColor,
fillOpacity: 0.34,
dashArray: null
});
@@ -4933,7 +5098,7 @@ function bindDrawingLayerSelection(layer) {
}
layer.lmeDrawingSelectionBound = true;
layer.on("click", event => {
if (drawingEditActive()) {
if (drawingEditActive() || lmeState.selectionMode === "off") {
return;
}
lmeState.ignoreMapClickUntil = Date.now() + 1000;
@@ -4941,28 +5106,14 @@ function bindDrawingLayerSelection(layer) {
event.originalEvent._lmeDrawingSelected = true;
L.DomEvent.stopPropagation(event.originalEvent);
}
const id = managedLayerId(layer, "drawings");
const multiple = lmeState.multiSelectionEnabled
|| event.originalEvent?.ctrlKey
|| event.originalEvent?.metaKey
|| event.originalEvent?.shiftKey;
if (multiple) {
if (lmeState.managerSelectedObjectIds.has(id)) lmeState.managerSelectedObjectIds.delete(id);
else lmeState.managerSelectedObjectIds.add(id);
} else {
lmeState.managerSelectedObjectIds.clear();
lmeState.managerSelectedObjectIds.add(id);
}
refreshLayerManager();
if (multiple) {
refreshMultipleMapSelection({ id, categoryKey: "drawings", layer, parent: lmeState.drawings });
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
} else {
selectDrawingLayer(layer);
}
if (lmeState.managerSelectedObjectIds.has(id)) {
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(id));
}
const record = {
id: managedLayerId(layer, "drawings"),
categoryKey: "drawings",
layer,
parent: lmeState.drawings,
source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente")
};
selectManagedRecord(record, event.originalEvent);
});
}
@@ -4970,31 +5121,14 @@ function bindManagedMapSelection(layer, categoryKey, parent) {
if (layer.lmeManagedMapSelectionBound) return;
layer.lmeManagedMapSelectionBound = true;
layer.on("click", event => {
if (Date.now() < lmeState.ignoreMapClickUntil) return;
if (Date.now() < lmeState.ignoreMapClickUntil || lmeState.selectionMode === "off") return;
lmeState.ignoreMapClickUntil = Date.now() + 600;
if (event.originalEvent) {
event.originalEvent._lmeDrawingSelected = true;
L.DomEvent.stopPropagation(event.originalEvent);
}
const record = { id: managedLayerId(layer, categoryKey), categoryKey, layer, parent, source: String(layer?.feature?.properties?._lmeSource || "Progetto corrente") };
const multiple = lmeState.multiSelectionEnabled
|| event.originalEvent?.ctrlKey
|| event.originalEvent?.metaKey
|| event.originalEvent?.shiftKey;
if (!multiple) lmeState.managerSelectedObjectIds.clear();
if (multiple && lmeState.managerSelectedObjectIds.has(record.id)) lmeState.managerSelectedObjectIds.delete(record.id);
else lmeState.managerSelectedObjectIds.add(record.id);
refreshLayerManager();
if (multiple) {
refreshMultipleMapSelection(record);
setInfo(`${lmeState.managerSelectedObjectIds.size} oggetti selezionati.`);
} else {
highlightManagedObject(record);
setInfo(`Selezionato dalla mappa: ${managedObjectEditableName(record) || "oggetto"}.`);
}
if (lmeState.managerSelectedObjectIds.has(record.id)) {
window.requestAnimationFrame(() => revealManagedRecordInLayerManager(record.id));
}
selectManagedRecord(record, event.originalEvent);
});
}
@@ -5523,7 +5657,7 @@ function loadProjectSnapshot(raw, projectName) {
? Boolean(projectDisplayOptions.importedSheetsVisible)
: data.schemaVersion >= 2 && Boolean(data.importedSheetsVisible);
applyImportedSheetVisibility();
L.geoJSON(data.selectedParcels || emptyFeatureCollection(), {
const restoredParcels = L.geoJSON(data.selectedParcels || emptyFeatureCollection(), {
pane: "selectedParcelsPane",
style: feature => ({
color: feature?.properties?._lmeColor || "#ef4444",
@@ -5544,6 +5678,15 @@ function loadProjectSnapshot(raw, projectName) {
applyStyleFromProperties(layer);
}
}).addTo(lmeState.selectedParcelsLayer);
restoredParcels.eachLayer(layer => {
const properties = layer?.feature?.properties || {};
const reference = properties.NATIONALCADASTRALREFERENCE
|| properties.INSPIREID_LOCALID
|| properties.LABEL
|| "Particella catastale";
layer.lmeParcelReference = reference;
bindManagedMapSelection(layer, "selectedParcels", restoredParcels);
});
const restoredFiducials = L.geoJSON(data.fiducials || emptyFeatureCollection(), {
pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng),
onEachFeature: (feature, layer) => {
@@ -5790,6 +5933,7 @@ function resetLayerManagerProjectState() {
}
function clearManagedCategoryState(categoryKey) {
clearManagedCategorySelection(categoryKey);
const category = layerManagerCategories().find(item => item.key === categoryKey);
if (category) {
managedRecordsForCategory(category).forEach(record => {
@@ -5807,6 +5951,21 @@ function clearManagedCategoryState(categoryKey) {
delete lmeState.managerObjectOrder[categoryKey];
}
function clearManagedCategorySelection(categoryKey) {
const ids = new Set();
lmeState.managerRecords?.forEach?.(record => {
if (record.categoryKey === categoryKey) ids.add(record.id);
});
const category = layerManagerCategories().find(item => item.key === categoryKey);
if (category) managedRecordsForCategory(category).forEach(record => ids.add(record.id));
ids.forEach(id => lmeState.managerSelectedObjectIds.delete(id));
if (lmeState.managerHighlightedCategoryKey === categoryKey) {
lmeState.managerHighlightedObjectId = null;
lmeState.managerHighlightedCategoryKey = null;
}
restoreDirectManagerHighlights();
}
function bindStoredFeaturePopup(feature, layer) {
updateStoredFeaturePopup(layer, feature?.properties);
}