diff --git a/Components/MenuHelp.razor b/Components/MenuHelp.razor index d2e7618..19c58fe 100644 --- a/Components/MenuHelp.razor +++ b/Components/MenuHelp.razor @@ -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.", diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index 2162b94..47cda2c 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -29,8 +29,8 @@ it.liviomerola.lmemappecatastali.pro - 1.0.23 - 123 + 1.0.24 + 124 $(ApplicationDisplayVersion) diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index fb3fca3..9e89906 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -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; } diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index 434fbd8..d627b49 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -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(`${escapeHtml(ref)}`); + 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?.(`${escapeHtml(reference)}`); + 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 = ` -
${selectedCount} selezionati
+
${selectedCount} selezionati
@@ -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 = `Azioni gruppo${escapeHtml(managedGroupTotals(records))}
${managedColorEditorMarkup("color", "Colore linea", lineColor)} ${managedColorEditorMarkup("fillColor", "Colore riempimento", fillColor)} + ${managedColorEditorMarkup("selectionColor", "Colore selezione", selectionColor)}
-
`; +
`; 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 = ` + const selectionColor = normalizedColor(properties._lmeSelectionColor, "#d500f9"); + tools.innerHTML = `
${managedColorEditorMarkup("color", "Colore linea", lineColor)} ${managedColorEditorMarkup("fillColor", "Colore riempimento", fillColor)} + ${managedColorEditorMarkup("selectionColor", "Colore selezione", selectionColor)} @@ -2672,15 +2705,25 @@ function renderManagedObjectTools(record) { ${properties._lmePhoto ? `Foto allegata` : ""}
`; 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 `${escapeHtml(managedObjectLabel(record, index))}${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}${escapeHtml(properties.foglio || properties.FOGLIO || "")}${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}${escapeHtml(details.slice(0, 2).join(" | "))}`; + return `${escapeHtml(managedObjectLabel(record, index))}${escapeHtml(managedCategoryName(layerManagerCategories().find(item => item.key === record.categoryKey) || { key: record.categoryKey, label: record.categoryKey }, [record]))}${escapeHtml(properties.foglio || properties.FOGLIO || "")}${escapeHtml(properties.particella || properties.PARTICELLA || properties.LABEL || "")}${escapeHtml(details.slice(0, 2).join(" | "))}`; }).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.
${body || ""}
Selez.BloccaMappaMetadatiNoteNomeGruppoFoglioParticellaMisure
Nessun oggetto
`); + const dialog = showAppDialog("Elenco oggetti e particelle", `
Modalita: ${escapeHtml(selectionModeLabel())}. La selezione e condivisa con mappa e pannello Layer.Il lucchetto impedisce modifiche e rimozioni; la matita modifica i metadati.Note apre l'elenco completo; le modifiche richiedono LME Pro.
${body || ""}
Selez.BloccaMappaMetadatiNoteNomeGruppoFoglioParticellaMisure
Nessun oggetto
`); 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 = `✓ Selezione singola + ☑ Selezione multipla + × Selezione disattivata + ⌫ Cancella selezione`; + 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() {
▱
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.
+
✓
Modalita di selezioneScegli Singola, Multipla o Disattivata. Mappa, pannello Layer e tabella oggetti condividono la stessa selezione. Disattivata e Cancella selezione deselezionano tutto senza eliminare alcun oggetto.
⇄
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.
@@ -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); }