diff --git a/Components/MenuHelp.razor b/Components/MenuHelp.razor
index 10ae50a..fecdbde 100644
--- a/Components/MenuHelp.razor
+++ b/Components/MenuHelp.razor
@@ -80,7 +80,7 @@
"Nuovo progetto: inserisci un nome, scegli se includere i contorni catastali evidenziati e premi Salva progetto. Vengono conservati oggetti, gruppi, nomi, attributi e opzioni di visualizzazione.",
"Aggiornamento: dopo aver aperto un progetto, usa di nuovo Salva progetto con lo stesso nome per aggiornarlo. Se nelle Impostazioni attivi Aggiorna automaticamente il progetto aperto, le modifiche vengono registrate dopo 3 secondi di inattivita.",
"Cartelle: seleziona un progetto, scrivi per esempio Cantieri 2026 e premi Sposta per organizzarlo nell'archivio; questa operazione non sposta le geometrie sulla mappa.",
- "Duplica crea una copia modificabile, utile prima di una variante importante o quando ricevi un progetto in sola lettura.",
+ "Rinomina cambia il nome del progetto selezionato senza alterarne contenuti e revisioni. Duplica crea invece una copia modificabile, utile prima di una variante importante o quando ricevi un progetto in sola lettura.",
"Archivia nasconde logicamente i lavori conclusi senza eliminarli; Ripristina li riporta tra quelli attivi. Elimina rimuove invece il progetto dall'archivio.",
"Condividi modificabile consente al destinatario di continuare il lavoro; Condividi in sola lettura protegge l'originale e richiede una duplicazione per modificarlo.",
"Revisioni: seleziona una data e usa Apri revisione per consultarne lo stato oppure Confronta per vedere le differenze con la versione corrente.",
@@ -88,7 +88,8 @@
],
"tools" =>
[
- "Tabella oggetti: apre una vista ordinata di particelle, file importati e disegni. Usa i flag per selezionare piu righe e il lucchetto per proteggerle dalle modifiche.",
+ "Tabella oggetti: apre una vista ordinata di particelle, file importati e disegni. Usa i flag per selezionare piu righe, il lucchetto per proteggerle e la matita per modificare i metadati.",
+ "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.",
"Registro operazioni: mostra in ordine cronologico disegni, modifiche, rinomine, spostamenti ed eliminazioni, cosi puoi capire cosa e cambiato durante la sessione.",
"Cestino: Ripristina ultima rimozione recupera l'elemento eliminato piu recente. Svuota cestino elimina definitivamente tutti gli elementi rimossi e non consente ulteriori recuperi.",
diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
index 2951382..90dd7ba 100644
--- a/Components/Pages/Home.razor
+++ b/Components/Pages/Home.razor
@@ -354,6 +354,7 @@
+
@@ -1403,6 +1404,39 @@
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto duplicato: {copy.Name}.");
}
+ private async Task RenameProject()
+ {
+ if (SelectedProject is not { } project) return;
+ var requestedName = await JS.InvokeAsync("lmeMap.promptProjectName", project.Name);
+ if (string.IsNullOrWhiteSpace(requestedName)
+ || string.Equals(requestedName.Trim(), project.Name, StringComparison.CurrentCulture))
+ {
+ return;
+ }
+
+ ProjectBusy = true;
+ try
+ {
+ var renamed = await ProjectStorageService.RenameAsync(project.Id, requestedName);
+ await RefreshProjectsAsync();
+ SelectedProjectId = renamed.Id;
+ NewProjectName = renamed.Name;
+ if (!HasUnsavedChanges)
+ {
+ ExportFileName = renamed.Name;
+ }
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto rinominato: {renamed.Name}.");
+ }
+ catch (Exception error)
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non rinominato: {error.GetBaseException().Message}");
+ }
+ finally
+ {
+ ProjectBusy = false;
+ }
+ }
+
private async Task UpdateProjectFolder()
{
if (SelectedProject is null) return;
diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj
index 41bc10a..4ca4f85 100644
--- a/LMEMappeCatastaliApp.csproj
+++ b/LMEMappeCatastaliApp.csproj
@@ -28,8 +28,8 @@
com.companyname.lmemappecatastaliapp
- 0.4.4
- 39
+ 0.4.5
+ 40$(ApplicationDisplayVersion)
diff --git a/README.md b/README.md
index e8a85b3..085429c 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@
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.5
+
+- Corretto il layout del pannello Layer sui telefoni e sui foldable: checkbox, lucchetto, ordinamento e freccia di espansione non si sovrappongono e restano allineati.
+- La tabella oggetti include un comando dedicato per modificare i metadati di ciascun elemento.
+- L'editor metadati permette di copiare i campi da un altro oggetto, scegliendo se importare anche i valori oppure soltanto i nomi.
+- Il pannello Progetti consente di rinominare un progetto duplicato o esistente senza perdere contenuti, identita e revisioni.
+- La barra Leaflet include una guida completa con istruzioni ed esempi concreti per disegno, modifica, eliminazione, riparazione, fori e confronto confini.
+
## Versione 0.4.4
- I comandi `Rinomina`, `Sposta nel gruppo` e `Rimuovi` occupano una riga completa e restano visibili anche nei pannelli Layer molto stretti.
diff --git a/Services/MapExportService.cs b/Services/MapExportService.cs
index ac06055..76e1b08 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.4"),
+ new XAttribute("creator", "LME Mappe Catastali 0.4.5"),
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/Services/ProjectStorageService.cs b/Services/ProjectStorageService.cs
index 5d59e1d..a8463aa 100644
--- a/Services/ProjectStorageService.cs
+++ b/Services/ProjectStorageService.cs
@@ -147,6 +147,36 @@ public sealed class ProjectStorageService
finally { _gate.Release(); }
}
+ public async Task RenameAsync(string projectId, string name)
+ {
+ var normalizedName = NormalizeName(name);
+ await _gate.WaitAsync();
+ try
+ {
+ var files = await ReadProjectFilesAsync();
+ var project = files.FirstOrDefault(item => item.Document.Id == projectId)
+ ?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
+ if (project.Document.AccessMode == "readonly")
+ {
+ throw new InvalidOperationException("Il progetto e' in sola lettura. Duplicalo per modificarlo.");
+ }
+ if (files.Any(item => item.Document.Id != projectId
+ && string.Equals(item.Document.Name, normalizedName, StringComparison.CurrentCultureIgnoreCase)))
+ {
+ throw new InvalidOperationException($"Esiste gia' un progetto chiamato '{normalizedName}'.");
+ }
+
+ project.Document.Name = normalizedName;
+ project.Document.UpdatedAt = DateTimeOffset.UtcNow;
+ await File.WriteAllTextAsync(
+ project.Path,
+ JsonSerializer.Serialize(project.Document, _jsonOptions),
+ new UTF8Encoding(false));
+ return project.Document.ToSummary();
+ }
+ finally { _gate.Release(); }
+ }
+
public async Task UpdateOrganizationAsync(string projectId, string? folder = null, bool? isArchived = null)
{
await _gate.WaitAsync();
diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css
index 93af00a..d573a12 100644
--- a/wwwroot/css/app.css
+++ b/wwwroot/css/app.css
@@ -618,10 +618,10 @@ input[type="checkbox"] {
.layer-manager-block > summary,
.layer-manager-object > summary {
display: grid;
- grid-template-columns: 28px 28px minmax(0, 1fr) 50px;
+ grid-template-columns: 28px 28px minmax(0, 1fr) minmax(28px, auto);
grid-template-areas:
"handle expand title count"
- ". . visibility moves"
+ "visibility lock moves moves"
"actions actions actions actions";
align-items: start;
gap: 6px;
@@ -633,7 +633,6 @@ input[type="checkbox"] {
.layer-expand-button {
grid-area: expand;
- grid-row: 1 / span 3;
align-self: center;
justify-self: center;
width: 28px;
@@ -733,6 +732,7 @@ input[type="checkbox"] {
grid-area: moves;
display: inline-grid;
grid-template-columns: 24px 24px;
+ justify-self: end;
gap: 2px;
}
@@ -778,12 +778,12 @@ input[type="checkbox"] {
}
.layer-inline-lock {
- grid-area: visibility;
+ grid-area: lock;
justify-self: center;
width: 28px;
min-width: 28px;
min-height: 24px;
- margin-left: 26px;
+ margin: 0;
padding: 0;
background: transparent;
color: var(--ink);
@@ -1047,6 +1047,49 @@ input[type="checkbox"] {
font-size: 12px;
}
+.attribute-copy-panel {
+ display: grid;
+ grid-template-columns: minmax(170px, 1fr) auto;
+ grid-template-areas:
+ "heading heading"
+ "source source"
+ "toggle action"
+ "hint hint";
+ align-items: end;
+ gap: 8px 12px;
+ padding: 10px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: #f5f8f9;
+}
+
+.attribute-copy-panel > strong {
+ grid-area: heading;
+}
+
+.attribute-copy-panel > label:first-child {
+ grid-area: source;
+ min-width: 0;
+}
+
+.attribute-copy-panel > .toggle-line {
+ grid-area: toggle;
+}
+
+.attribute-copy-panel > button {
+ grid-area: action;
+}
+
+.attribute-copy-panel > label:first-child select {
+ width: 100%;
+}
+
+.attribute-copy-panel > small {
+ grid-area: hint;
+ color: var(--muted);
+ line-height: 1.4;
+}
+
.attribute-editor-row {
display: grid;
grid-template-columns: minmax(130px, 0.8fr) minmax(180px, 2fr) 36px;
@@ -1111,6 +1154,47 @@ input[type="checkbox"] {
text-decoration: none;
}
+.leaflet-help {
+ display: grid;
+ gap: 14px;
+}
+
+.leaflet-help > p {
+ margin: 0;
+ color: var(--muted);
+ line-height: 1.45;
+}
+
+.leaflet-help-list {
+ display: grid;
+ gap: 0;
+ margin: 0;
+}
+
+.leaflet-help-list > div {
+ display: grid;
+ grid-template-columns: 42px minmax(0, 1fr);
+ gap: 10px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--line);
+}
+
+.leaflet-help-list dt {
+ font-size: 20px;
+ font-weight: 900;
+ text-align: center;
+}
+
+.leaflet-help-list dd {
+ margin: 0;
+ line-height: 1.45;
+}
+
+.leaflet-help-list dd strong {
+ display: block;
+ margin-bottom: 2px;
+}
+
.gps-overlay {
position: absolute;
top: 72px;
@@ -1661,6 +1745,17 @@ footer {
align-self: center;
}
+ .attribute-copy-panel {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas:
+ "heading"
+ "source"
+ "toggle"
+ "action"
+ "hint";
+ align-items: stretch;
+ }
+
.gps-overlay {
top: 68px;
right: 8px;
diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js
index e31b48e..5cd4f66 100644
--- a/wwwroot/js/lme-map.js
+++ b/wwwroot/js/lme-map.js
@@ -132,6 +132,7 @@ window.lmeMap = {
setProjectReadOnly,
restoreAutosave,
compareProjectSnapshots,
+ promptProjectName,
setAdvancedOptions,
createMapExportGeoJson,
createProjectSnapshot,
@@ -2277,13 +2278,25 @@ function managedHslToRgb(hue, saturation, lightness) {
return values.map(channel => Math.round((channel + offset) * 255));
}
+function editableManagedMetadataEntries(record) {
+ const properties = record?.layer?.feature?.properties || {};
+ const reserved = new Set(["name", "Name", "label", "LABEL"]);
+ return Object.entries(properties)
+ .filter(([key, value]) => !key.startsWith("_lme") && !reserved.has(key) && ["string", "number", "boolean"].includes(typeof value));
+}
+
function editManagedAttributes(record) {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return; }
- const properties = record.layer?.feature?.properties || {};
const reserved = new Set(["name", "Name", "label", "LABEL"]);
- const entries = Object.entries(properties)
- .filter(([key, value]) => !key.startsWith("_lme") && !reserved.has(key) && ["string", "number", "boolean"].includes(typeof value));
- const dialog = showAppDialog("Attributi oggetto", `
Ogni attributo viene salvato nel progetto e negli export GeoJSON, KML e KMZ.
Gli strumenti operano direttamente sulla mappa. Per le azioni su oggetti esistenti, seleziona prima le righe nel pannello Layer, nella tabella oggetti oppure tocca un oggetto sulla mappa.
+
+
+
IngrandisciAumenta lo zoom mantenendo il centro attuale. Esempio: avvicinati a un vertice per posizionarlo con maggiore precisione.
+
−
RiduciAllarga l'area visibile. Esempio: usalo per controllare insieme particelle appartenenti a fogli vicini.
+
●
Punto di interesseTocca la posizione da memorizzare. Esempio: segna un cancello, un cippo o il punto di accesso al fondo.
+
/
LineaTocca i vertici del percorso e termina con doppio tocco o sul primo comando della barra. Durante il disegno viene mostrata la distanza; utile per un fronte strada o una recinzione.
+
⬠
PoligonoTocca almeno tre vertici e chiudi la figura. Esempio: delimita un'area di intervento; l'app calcola area e perimetro.
+
□
RettangoloTrascina tra due angoli opposti. Esempio: crea rapidamente l'ingombro rettangolare di un fabbricato.
+
○
CerchioTocca il centro e trascina fino al raggio desiderato. Esempio: rappresenta una fascia di rispetto attorno a un pozzo.
+
✎
Modifica disegniPremi la matita e scegli Solo selezionato oppure Tutti. Sposta i vertici e conferma con Salva; gli oggetti bloccati devono prima essere sbloccati.
+
▱
Elimina disegniPremi il cestino e scegli Solo selezionato oppure Tutti. L'oggetto rimosso può essere recuperato con Annulla o dal cestino negli strumenti avanzati.
+
✓
Controlla e riparaVerifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.
+
◉
Crea foroSeleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.
+
⇄
Confronta due confiniSeleziona esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde, mostrando le rispettive aree.
+
?
GuidaRiapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra, così l'operazione non resta senza riscontro.