diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index a7a3dfe..e65523d 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -28,8 +28,8 @@ com.companyname.lmemappecatastaliapp - 0.4.1 - 36 + 0.4.2 + 37 $(ApplicationDisplayVersion) diff --git a/README.md b/README.md index 621c3da..3b5f82f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata per consultare mappe catastali e lavorare sul campo con GPS. +## Versione 0.4.2 + +- Nuovo comando `Sposta nel gruppo` visibile accanto a Rinomina e Rimuovi per padri, sottogruppi e singoli oggetti. +- Scelta della categoria e del sottogruppo di destinazione, con possibilita di creare un nuovo gruppo. +- I disegni possono essere organizzati in sottogruppi come i dati importati. +- `Sposta geometria` resta separato e continua a traslare gli oggetti in metri verso nord, sud, est o ovest. + ## Versione 0.4.1 - Pannelli operativi allineati a destra, con ricerca lasciata nella posizione precedente. diff --git a/Services/MapExportService.cs b/Services/MapExportService.cs index 8411c1e..f18cfc9 100644 --- a/Services/MapExportService.cs +++ b/Services/MapExportService.cs @@ -85,7 +85,7 @@ public sealed class MapExportService { var root = new XElement("gpx", new XAttribute("version", "1.1"), - new XAttribute("creator", "LME Mappe Catastali 0.4.1"), + new XAttribute("creator", "LME Mappe Catastali 0.4.2"), new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"), new XElement("metadata", new XElement("name", name))); foreach (var feature in features.EnumerateArray()) diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index dc2e4e4..c9975ea 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -573,6 +573,7 @@ input[type="checkbox"] { } .layer-rename-button, +.layer-membership-button, .layer-remove-button { width: 100%; min-height: 28px; @@ -592,6 +593,16 @@ input[type="checkbox"] { background: #d9eee7; } +.layer-membership-button { + border-color: #bfdbfe; + background: #eff6ff; + color: #1d4ed8; +} + +.layer-membership-button:active { + background: #dbeafe; +} + .layer-remove-button { border-color: #fecaca; background: #fff1f2; @@ -827,6 +838,7 @@ input[type="checkbox"] { } .layer-manager-object > summary .layer-rename-button, +.layer-manager-object > summary .layer-membership-button, .layer-manager-object > summary .layer-remove-button { min-height: 24px; font-size: 9px; diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index 16b2633..eac5242 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -1184,7 +1184,7 @@ function drawingLayerLabel(layer, layerType) { function layerManagerCategories() { return [ { key: "gpsPosition", label: "Posizione GPS", roots: [lmeState.gpsMarker, lmeState.gpsAccuracy].filter(Boolean) }, - { key: "drawings", label: "Disegni e POI", roots: [lmeState.drawings].filter(Boolean) }, + { key: "drawings", label: "Disegni e POI", roots: [lmeState.drawings].filter(Boolean), blocks: true }, { key: "gpsTrail", label: "Traccia GPS", roots: [lmeState.gpsTrail].filter(Boolean) }, { key: "gpsParcels", label: "Particella rilevata dal GPS", roots: [lmeState.gpsParcelLayer].filter(Boolean) }, { key: "selectedParcels", label: "Particelle catastali evidenziate", roots: [lmeState.selectedParcelsLayer].filter(Boolean) }, @@ -1382,11 +1382,11 @@ function managedObjectCanRemove(record) { return ["drawings", "imported", "importedSheets", "selectedParcels", "gpsParcels"].includes(record.categoryKey); } -function layerManagerRowActions(renameButton, removeButton) { - if (!renameButton && !removeButton) { +function layerManagerRowActions(renameButton, moveButton, removeButton) { + if (!renameButton && !moveButton && !removeButton) { return ""; } - return `${renameButton}${removeButton}`; + return `${renameButton}${moveButton}${removeButton}`; } const layerManagerPalettes = [ @@ -1472,7 +1472,87 @@ function removeManagedRecords(records, subject) { setInfo(`${records.length === 1 ? "Oggetto rimosso" : `${records.length} oggetti rimossi`} dal progetto.`); } -function moveManagedRecordsToCategory(records, targetCategory) { +function managedMoveDestinations() { + return layerManagerCategories() + .filter(category => ["drawings", "imported"].includes(category.key)) + .map(category => { + const records = managedRecordsForCategory(category); + const groups = new Map(); + records.forEach(record => { + if (!groups.has(record.source)) { + groups.set(record.source, managedBlockName(record.source, [record])); + } + }); + if (!groups.size) { + groups.set("Progetto corrente", "Gruppo principale"); + } + return { + key: category.key, + name: managedCategoryName(category, records), + groups: [...groups].map(([source, name]) => ({ source, name })) + }; + }); +} + +function showMoveManagedRecordsDialog(records, subject) { + if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; } + if (!records.length || records.some(record => !["drawings", "imported"].includes(record.categoryKey))) { + setInfo("Questo oggetto o gruppo non puo essere spostato."); + return; + } + if (records.some(record => record.layer?.feature?.properties?._lmeLocked)) { + setInfo("Sblocca prima tutti gli oggetti da spostare."); + return; + } + const destinations = managedMoveDestinations(); + const categoryOptions = destinations.map(destination => + ``).join(""); + const dialog = showAppDialog("Sposta nel gruppo", `

Cambia l'appartenenza di ${escapeHtml(subject)} senza modificarne la posizione sulla mappa.

`); + const form = dialog.querySelector("form"); + const categorySelect = form.elements.category; + const groupSelect = form.elements.group; + const newGroupInput = form.elements.newGroup; + const currentCategory = records.every(record => record.categoryKey === records[0].categoryKey) + ? records[0].categoryKey + : "drawings"; + categorySelect.value = currentCategory; + const refreshGroups = () => { + const destination = destinations.find(item => item.key === categorySelect.value); + groupSelect.innerHTML = (destination?.groups || []).map(group => + ``).join(""); + const currentSource = records.every(record => record.source === records[0].source) + ? records[0].source + : ""; + if (destination?.key === currentCategory && currentSource + && destination.groups.some(group => group.source === currentSource)) { + groupSelect.value = currentSource; + } + }; + categorySelect.addEventListener("change", refreshGroups); + refreshGroups(); + dialog.querySelector("[data-cancel]").addEventListener("click", () => dialog.remove()); + form.addEventListener("submit", event => { + event.preventDefault(); + const destination = destinations.find(item => item.key === categorySelect.value); + if (!destination) return; + const requestedName = newGroupInput.value.trim().slice(0, 120); + const existingByName = requestedName + ? destination.groups.find(group => group.name.toLocaleLowerCase("it") === requestedName.toLocaleLowerCase("it")) + : null; + const selectedGroup = destination.groups.find(group => group.source === groupSelect.value); + const target = existingByName || (requestedName + ? { source: `Gruppo ${requestedName}`, name: requestedName } + : selectedGroup); + if (!target) { + setInfo("Scegli un gruppo di destinazione o inserisci il nome di un nuovo gruppo."); + return; + } + dialog.remove(); + moveManagedRecordsToGroup(records, destination.key, target.source, target.name); + }); +} + +function moveManagedRecordsToGroup(records, targetCategory, targetSource, targetGroupName) { if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; } const movable = records.filter(record => ["drawings", "imported"].includes(record.categoryKey)); if (!movable.length || !["drawings", "imported"].includes(targetCategory)) { @@ -1488,8 +1568,10 @@ function moveManagedRecordsToCategory(records, targetCategory) { const feature = record.layer.toGeoJSON(); feature.properties = { ...(feature.properties || {}), - _lmeSource: feature.properties?._lmeSource || "Progetto corrente" + _lmeSource: targetSource, + _lmeGroupName: targetGroupName }; + delete feature.properties._lmeManagerId; return feature; }); movable.forEach(record => { @@ -1505,8 +1587,9 @@ function moveManagedRecordsToCategory(records, targetCategory) { refreshImportedMeasurements(); refreshDrawingMeasurements(); refreshLayerManager(); - finishMapChange(`Spostati ${features.length} oggetti in ${targetCategory === "drawings" ? "Disegni e POI" : "Progetti e dati importati"}.`); - setInfo(`${features.length} oggetti spostati in ${targetCategory === "drawings" ? "Disegni e POI" : "Progetti e dati importati"}.`); + const categoryName = targetCategory === "drawings" ? "Disegni e POI" : "Progetti e dati importati"; + finishMapChange(`Spostati ${features.length} oggetti in ${categoryName} > ${targetGroupName}.`); + setInfo(`${features.length} oggetti spostati in ${categoryName} > ${targetGroupName}.`); } function managedObjectDetails(layer) { @@ -1614,7 +1697,10 @@ function renderLayerManagerCategory(category, records) { const removeButton = managedCategoryCanRemove(category, records) ? `` : ""; - const rowActions = layerManagerRowActions(renameButton, removeButton); + const moveButton = records.length && ["drawings", "imported"].includes(category.key) + ? `` + : ""; + const rowActions = layerManagerRowActions(renameButton, moveButton, removeButton); group.classList.toggle("layer-manager-with-actions", Boolean(rowActions)); const visibleCount = records.filter(record => !lmeState.managerHiddenObjectIds.has(record.id)).length; const categoryVisible = !lmeState.managerHiddenCategories.has(category.key) @@ -1641,6 +1727,11 @@ function renderLayerManagerCategory(category, records) { event.stopPropagation(); removeManagedRecords(records, `il gruppo '${categoryName}'`); }); + group.querySelector(".layer-membership-button")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + showMoveManagedRecordsDialog(records, `il gruppo '${categoryName}'`); + }); bindManagedParentHighlight(group, `category:${category.key}`, records); const children = group.querySelector(".layer-manager-children"); if (records.length) children.appendChild(renderManagedGroupTools(records)); @@ -1674,6 +1765,7 @@ function renderLayerManagerBlock(category, source, records) { const blockName = managedBlockName(source, records); const rowActions = layerManagerRowActions( ``, + ``, ``); block.classList.add("layer-manager-with-actions"); const blockLocked = records.length > 0 && records.every(record => Boolean(record.layer?.feature?.properties?._lmeLocked)); @@ -1700,6 +1792,11 @@ function renderLayerManagerBlock(category, source, records) { event.stopPropagation(); removeManagedRecords(records, `il gruppo '${blockName}'`); }); + block.querySelector(".layer-membership-button").addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + showMoveManagedRecordsDialog(records, `il gruppo '${blockName}'`); + }); bindManagedParentHighlight(block, `block:${category.key}:${source}`, records); const children = block.querySelector(".layer-manager-children"); children.appendChild(renderManagedGroupTools(records)); @@ -1724,7 +1821,10 @@ function renderLayerManagerObject(record, index) { const removeButton = managedObjectCanRemove(record) ? `` : ""; - const rowActions = layerManagerRowActions(renameButton, removeButton); + const moveButton = ["drawings", "imported"].includes(record.categoryKey) + ? `` + : ""; + const rowActions = layerManagerRowActions(renameButton, moveButton, removeButton); object.classList.toggle("layer-manager-with-actions", Boolean(rowActions)); const objectName = managedObjectLabel(record, index); const locked = Boolean(record.layer?.feature?.properties?._lmeLocked); @@ -1743,6 +1843,11 @@ function renderLayerManagerObject(record, index) { event.stopPropagation(); removeManagedRecords([record], `'${objectName}'`); }); + object.querySelector(".layer-membership-button")?.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + showMoveManagedRecordsDialog([record], `'${objectName}'`); + }); object.querySelector(".layer-inline-lock")?.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); @@ -1786,7 +1891,7 @@ function renderLayerSelectionToolbar() { toolbar.innerHTML = `
${selectedCount} selezionati
- +
`; @@ -1818,17 +1923,11 @@ function renderManagedGroupTools(records) { const tools = document.createElement("div"); tools.className = "layer-group-tools"; const properties = records[0]?.layer?.feature?.properties || {}; - const sourceCategory = records[0]?.categoryKey; - const membershipAction = sourceCategory === "drawings" - ? `` - : sourceCategory === "imported" - ? `` - : ""; tools.innerHTML = `Azioni gruppo${escapeHtml(managedGroupTotals(records))}
-
${membershipAction}
`; +
`; const inputs = tools.querySelectorAll("input"); inputs.forEach(input => input.addEventListener("change", () => { applyManagedStyle(records, { color: inputs[0].value, weight: Number(inputs[1].value), opacity: Number(inputs[2].value) }); @@ -1845,8 +1944,6 @@ function renderManagedGroupTools(records) { const visible = !records.every(record => Boolean(record.layer?.feature?.properties?._lmeLabelVisible)); updateManagedProperties(records, properties => { properties._lmeLabelVisible = visible; }); }); - tools.querySelector("[data-group-action='membership']")?.addEventListener("click", () => - moveManagedRecordsToCategory(records, sourceCategory === "drawings" ? "imported" : "drawings")); return tools; } @@ -1869,11 +1966,6 @@ function renderManagedObjectTools(record) { const properties = record.layer.feature?.properties || (record.layer.feature = { type: "Feature", properties: {} }).properties; const selected = lmeState.managerSelectedObjectIds.has(record.id); const locked = Boolean(properties._lmeLocked); - const membershipAction = record.categoryKey === "drawings" - ? `` - : record.categoryKey === "imported" - ? `` - : ""; const tools = document.createElement("section"); tools.className = "layer-object-tools"; tools.innerHTML = ` @@ -1888,7 +1980,7 @@ function renderManagedObjectTools(record) { ${properties._lmePhoto ? `Foto allegata` : ""} -
${membershipAction}
`; +
`; tools.querySelector("[data-field='selected']").addEventListener("change", event => { if (event.target.checked) lmeState.managerSelectedObjectIds.add(record.id); else lmeState.managerSelectedObjectIds.delete(record.id); @@ -1918,8 +2010,6 @@ function renderManagedObjectTools(record) { tools.querySelector("[data-action='inspect']").addEventListener("click", () => showCoordinateInspectorForRecord(record)); tools.querySelector("[data-action='move']").addEventListener("click", () => moveManagedRecords([record])); tools.querySelector("[data-action='buffer']").addEventListener("click", () => bufferManagedRecords([record])); - tools.querySelector("[data-action='membership']")?.addEventListener("click", () => - moveManagedRecordsToCategory([record], record.categoryKey === "drawings" ? "imported" : "drawings")); return tools; }