Aggiunge spostamento tra gruppi 0.4.2

This commit is contained in:
livio committed 2026-09-25 08:59:38 +02:00
1 parent eb5f51795b
commit 27df3b447e
5 files changed
+140 -31

No files matched your search

+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>com.companyname.lmemappecatastaliapp</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>0.4.1</ApplicationDisplayVersion>
<ApplicationVersion>36</ApplicationVersion>
<ApplicationDisplayVersion>0.4.2</ApplicationDisplayVersion>
<ApplicationVersion>37</ApplicationVersion>
<Version>$(ApplicationDisplayVersion)</Version>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
+7
View File
@@ -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.
+1 -1
View File
@@ -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())
+12
View File
@@ -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;
+118 -28
View File
@@ -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 `<span class="layer-row-actions">${renameButton}${removeButton}</span>`;
return `<span class="layer-row-actions">${renameButton}${moveButton}${removeButton}</span>`;
}
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 =>
`<option value="${escapeHtml(destination.key)}">${escapeHtml(destination.name)}</option>`).join("");
const dialog = showAppDialog("Sposta nel gruppo", `<form class="lme-edit-form move-group-form"><p>Cambia l'appartenenza di ${escapeHtml(subject)} senza modificarne la posizione sulla mappa.</p><label>Categoria<select name="category">${categoryOptions}</select></label><label>Gruppo esistente<select name="group"></select></label><label>Oppure crea un nuovo gruppo<input name="newGroup" type="text" maxlength="120" placeholder="Nome del nuovo gruppo"></label><div class="dialog-command-row"><button type="button" data-cancel>Annulla</button><button type="submit" class="primary-command">Sposta nel gruppo</button></div></form>`);
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 =>
`<option value="${escapeHtml(group.source)}">${escapeHtml(group.name)}</option>`).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)
? `<button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button>`
: "";
const rowActions = layerManagerRowActions(renameButton, removeButton);
const moveButton = records.length && ["drawings", "imported"].includes(category.key)
? `<button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button>`
: "";
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(
`<button type="button" class="layer-rename-button" aria-label="Rinomina gruppo" title="Rinomina">Rinomina</button>`,
`<button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button>`,
`<button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button>`);
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)
? `<button type="button" class="layer-remove-button" aria-label="Rimuovi oggetto dal progetto" title="Rimuovi dal progetto">Rimuovi</button>`
: "";
const rowActions = layerManagerRowActions(renameButton, removeButton);
const moveButton = ["drawings", "imported"].includes(record.categoryKey)
? `<button type="button" class="layer-membership-button" aria-label="Sposta oggetto in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button>`
: "";
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 = `<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-batch-actions">
<button type="button" data-action="duplicate">Duplica</button><button type="button" data-action="move">Sposta</button>
<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>
<button type="button" data-action="split">Dividi</button><button type="button" data-action="delete" class="danger-command">Rimuovi</button>
</div>`;
@@ -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"
? `<button type="button" data-group-action="membership">Sposta in Progetti e dati importati</button>`
: sourceCategory === "imported"
? `<button type="button" data-group-action="membership">Sposta in Disegni e POI</button>`
: "";
tools.innerHTML = `<strong>Azioni gruppo</strong><span class="layer-group-totals">${escapeHtml(managedGroupTotals(records))}</span><div class="layer-style-controls">
<label>Colore<input type="color" value="${normalizedColor(properties._lmeColor, "#16806a")}"></label>
<label>Spessore<input type="range" min="1" max="10" step="1" value="${Number(properties._lmeWeight) || 3}"></label>
<label>Opacita<input type="range" min="0.1" max="1" step="0.1" value="${Number(properties._lmeOpacity) || 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>${membershipAction}</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>`;
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"
? `<button type="button" data-action="membership">Sposta in Progetti e dati importati</button>`
: record.categoryKey === "imported"
? `<button type="button" data-action="membership">Sposta in Disegni e POI</button>`
: "";
const tools = document.createElement("section");
tools.className = "layer-object-tools";
tools.innerHTML = `<label class="layer-manager-suboption"><input type="checkbox" data-field="selected" ${selected ? "checked" : ""}><span>Seleziona per azioni multiple</span></label>
@@ -1888,7 +1980,7 @@ function renderManagedObjectTools(record) {
<label>Note<textarea data-field="notes" rows="2" maxlength="1000" placeholder="Note tecniche">${escapeHtml(properties._lmeNotes || "")}</textarea></label>
<label class="layer-photo-picker">Foto<input type="file" data-field="photo" accept="image/*"></label>
${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>${membershipAction}</div>`;
<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);
@@ -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;
}