diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
index 868b408..b5ce158 100644
--- a/Components/Pages/Home.razor
+++ b/Components/Pages/Home.razor
@@ -5,6 +5,7 @@
@inject OfficialCadastreService OfficialCadastreService
@inject LocationSearchService LocationSearchService
@inject AzureMapsService AzureMapsService
+@inject ProjectStorageService ProjectStorageService
@implements IAsyncDisposable
LME Mappe Catastali
@@ -161,19 +162,42 @@
-
Progetti e POI
+ Archivio progetti
+
+
+
+
+
+
+
+
+ @ProjectArchiveMessage
+
+ POI e disegni
+
-
-
-
-
}
@@ -268,7 +292,10 @@
@code {
private string ActiveTab { get; set; } = "map";
private string SearchText { get; set; } = string.Empty;
- private string ProjectName { get; set; } = string.Empty;
+ private string NewProjectName { get; set; } = string.Empty;
+ private string PoiName { get; set; } = string.Empty;
+ private string SelectedProjectId { get; set; } = string.Empty;
+ private string ProjectArchiveMessage { get; set; } = "Nessun progetto salvato.";
private string Status { get; set; } = "online";
private string SelectedBaseLayer { get; set; } = "satellite";
private string GoogleMapsApiKey { get; set; } = string.Empty;
@@ -284,6 +311,8 @@
private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; }
+ private bool IncludeCadastralParcels { get; set; } = true;
+ private bool ProjectBusy { get; set; }
private CancellationTokenSource? CatalogCancellation { get; set; }
private CancellationTokenSource? GpsLookupCancellation { get; set; }
private long LastGpsLookupAt;
@@ -293,6 +322,7 @@
private IReadOnlyList Municipalities { get; set; } = [];
private IReadOnlyList Sheets { get; set; } = [];
private IReadOnlyList Parcels { get; set; } = [];
+ private IReadOnlyList Projects { get; set; } = [];
private MunicipalityOption? SelectedMunicipality =>
Municipalities.FirstOrDefault(item => item.Code == SelectedMunicipalityCode);
@@ -354,6 +384,8 @@
{
CatalogMessage = $"Catalogo Comuni non disponibile: {error.Message}";
}
+
+ await RefreshProjectsAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -361,6 +393,32 @@
if (firstRender)
{
await JS.InvokeVoidAsync("lmeMap.init");
+ await MigrateLegacyProjectAsync();
+ }
+ }
+
+ private async Task MigrateLegacyProjectAsync()
+ {
+ try
+ {
+ var legacySnapshot = await JS.InvokeAsync("lmeMap.getLegacyProjectSnapshot");
+ if (string.IsNullOrWhiteSpace(legacySnapshot))
+ {
+ return;
+ }
+
+ var migrated = await ProjectStorageService.SaveAsync(
+ "Progetto precedente",
+ legacySnapshot,
+ false);
+ await JS.InvokeVoidAsync("lmeMap.clearLegacyProjectSnapshot");
+ await RefreshProjectsAsync();
+ SelectedProjectId = migrated.Id;
+ await InvokeAsync(StateHasChanged);
+ }
+ catch
+ {
+ // The legacy value remains available and migration can be retried at the next start.
}
}
@@ -371,6 +429,10 @@
{
LayerPanelOpen = false;
}
+ if (tab == "projects")
+ {
+ await RefreshProjectsAsync();
+ }
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
}
@@ -665,7 +727,7 @@
private Task FitImportedLayers() => JS.InvokeVoidAsync("lmeMap.fitImported").AsTask();
private Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask();
- private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", ProjectName).AsTask();
+ private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask();
private async Task ToggleDrawTools()
{
DrawToolsVisible = !DrawToolsVisible;
@@ -678,8 +740,136 @@
}
private Task ClearDrawings() => JS.InvokeVoidAsync("lmeMap.clearDrawings").AsTask();
- private Task SaveWorkspace() => JS.InvokeVoidAsync("lmeMap.saveWorkspace").AsTask();
- private Task LoadWorkspace() => JS.InvokeVoidAsync("lmeMap.loadWorkspace").AsTask();
+
+ private async Task RefreshProjectsAsync()
+ {
+ try
+ {
+ Projects = await ProjectStorageService.GetProjectsAsync();
+ if (Projects.All(project => project.Id != SelectedProjectId))
+ {
+ SelectedProjectId = string.Empty;
+ }
+ ProjectArchiveMessage = Projects.Count == 0
+ ? "Nessun progetto salvato."
+ : $"{Projects.Count} {(Projects.Count == 1 ? "progetto salvato" : "progetti salvati")} nell'archivio interno.";
+ }
+ catch (Exception error)
+ {
+ ProjectArchiveMessage = $"Archivio non disponibile: {error.GetBaseException().Message}";
+ }
+ }
+
+ private Task SelectProject(ChangeEventArgs args)
+ {
+ SelectedProjectId = args.Value?.ToString() ?? string.Empty;
+ return Task.CompletedTask;
+ }
+
+ private async Task SaveProject()
+ {
+ var name = NewProjectName.Trim();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", "Inserisci il nome del progetto.");
+ return;
+ }
+
+ var existing = Projects.FirstOrDefault(project =>
+ string.Equals(project.Name, name, StringComparison.CurrentCultureIgnoreCase));
+ if (existing is not null
+ && !await JS.InvokeAsync("confirm", $"Il progetto '{existing.Name}' esiste gia. Sovrascriverlo?"))
+ {
+ return;
+ }
+
+ ProjectBusy = true;
+ try
+ {
+ var snapshot = await JS.InvokeAsync("lmeMap.createProjectSnapshot", IncludeCadastralParcels);
+ var saved = await ProjectStorageService.SaveAsync(
+ name,
+ snapshot,
+ IncludeCadastralParcels,
+ existing?.Id);
+ await RefreshProjectsAsync();
+ SelectedProjectId = saved.Id;
+ NewProjectName = saved.Name;
+ await JS.InvokeVoidAsync(
+ "lmeMap.showInfo",
+ IncludeCadastralParcels
+ ? $"Progetto salvato: {saved.Name}, incluse le particelle rosse."
+ : $"Progetto salvato: {saved.Name}, senza particelle rosse.");
+ }
+ catch (Exception error)
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non salvato: {error.GetBaseException().Message}");
+ }
+ finally
+ {
+ ProjectBusy = false;
+ }
+ }
+
+ private async Task LoadProject()
+ {
+ var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
+ if (project is null)
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", "Seleziona il progetto da caricare.");
+ return;
+ }
+
+ ProjectBusy = true;
+ try
+ {
+ var snapshot = await ProjectStorageService.LoadAsync(project.Id);
+ await JS.InvokeVoidAsync("lmeMap.loadProjectSnapshot", snapshot, project.Name);
+ NewProjectName = project.Name;
+ IncludeCadastralParcels = project.IncludesCadastralParcels;
+ ActiveTab = "map";
+ await Task.Delay(30);
+ await JS.InvokeVoidAsync("lmeMap.resize");
+ }
+ catch (Exception error)
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non caricato: {error.GetBaseException().Message}");
+ }
+ finally
+ {
+ ProjectBusy = false;
+ }
+ }
+
+ private async Task DeleteProject()
+ {
+ var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
+ if (project is null)
+ {
+ return;
+ }
+ if (!await JS.InvokeAsync("confirm", $"Eliminare definitivamente il progetto '{project.Name}'?"))
+ {
+ return;
+ }
+
+ ProjectBusy = true;
+ try
+ {
+ await ProjectStorageService.DeleteAsync(project.Id);
+ SelectedProjectId = string.Empty;
+ await RefreshProjectsAsync();
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto eliminato: {project.Name}.");
+ }
+ catch (Exception error)
+ {
+ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Progetto non eliminato: {error.GetBaseException().Message}");
+ }
+ finally
+ {
+ ProjectBusy = false;
+ }
+ }
private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask();
private async Task ToggleTrail(ChangeEventArgs args)
{
diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj
index 13d8d72..ba9f1be 100644
--- a/LMEMappeCatastaliApp.csproj
+++ b/LMEMappeCatastaliApp.csproj
@@ -28,8 +28,8 @@
com.companyname.lmemappecatastaliapp
- 0.2.6
- 10
+ 0.2.7
+ 11
None
diff --git a/MauiProgram.cs b/MauiProgram.cs
index a74b593..546559b 100644
--- a/MauiProgram.cs
+++ b/MauiProgram.cs
@@ -21,6 +21,7 @@ public static class MauiProgram
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
diff --git a/README.md b/README.md
index deb30bf..052dc51 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,8 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
- Inquadramento e pulizia dei layer importati.
- Disegno di marker, linee, poligoni e rettangoli.
- Toolbar di disegno completa di icone, modifica, eliminazione singola e pulizia di tutti i disegni/POI.
-- POI/progetti salvabili in locale con `localStorage`.
+- Archivio multiprogetto nella cartella privata dell'app, con nome, selezione da elenco, sovrascrittura ed eliminazione.
+- Ogni progetto salva file GIS, POI, disegni, traccia e inquadratura; l'inclusione delle particelle catastali rosse e' opzionale.
- GPS nativo MAUI ad alta precisione con permessi Android/iOS.
- Marker posizione, cerchio accuratezza in metri, tracciamento percorso.
- Controllo se la posizione GPS cade dentro un poligono importato o disegnato.
diff --git a/Services/AzureMapsService.cs b/Services/AzureMapsService.cs
index 173f74d..a4e7f21 100644
--- a/Services/AzureMapsService.cs
+++ b/Services/AzureMapsService.cs
@@ -13,7 +13,7 @@ public sealed class AzureMapsService
public AzureMapsService()
{
- _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.6");
+ _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.7");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
}
diff --git a/Services/ProjectStorageService.cs b/Services/ProjectStorageService.cs
new file mode 100644
index 0000000..0406d72
--- /dev/null
+++ b/Services/ProjectStorageService.cs
@@ -0,0 +1,202 @@
+using System.Text;
+using System.Text.Json;
+
+namespace LMEMappeCatastaliApp.Services;
+
+public sealed class ProjectStorageService
+{
+ private const string ProjectExtension = ".lmeproject.json";
+ private readonly SemaphoreSlim _gate = new(1, 1);
+ private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)
+ {
+ WriteIndented = true
+ };
+
+ private string ProjectsDirectory => Path.Combine(FileSystem.AppDataDirectory, "Projects");
+
+ public async Task> GetProjectsAsync()
+ {
+ await _gate.WaitAsync();
+ try
+ {
+ var files = await ReadProjectFilesAsync();
+ return files
+ .Select(item => item.Document.ToSummary())
+ .OrderByDescending(item => item.UpdatedAt)
+ .ThenBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
+ .ToArray();
+ }
+ finally
+ {
+ _gate.Release();
+ }
+ }
+
+ public async Task SaveAsync(
+ string name,
+ string snapshotJson,
+ bool includesCadastralParcels,
+ string? projectId = null)
+ {
+ var normalizedName = NormalizeName(name);
+ using var snapshotDocument = JsonDocument.Parse(snapshotJson);
+ if (snapshotDocument.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidDataException("I dati della mappa non sono validi.");
+ }
+
+ await _gate.WaitAsync();
+ try
+ {
+ Directory.CreateDirectory(ProjectsDirectory);
+ var files = await ReadProjectFilesAsync();
+ var existing = files.FirstOrDefault(item =>
+ (!string.IsNullOrWhiteSpace(projectId) && item.Document.Id == projectId)
+ || string.Equals(item.Document.Name, normalizedName, StringComparison.CurrentCultureIgnoreCase));
+
+ var now = DateTimeOffset.UtcNow;
+ var id = existing?.Document.Id ?? Guid.NewGuid().ToString("N");
+ var document = new MapProjectDocument
+ {
+ SchemaVersion = 1,
+ Id = id,
+ Name = normalizedName,
+ CreatedAt = existing?.Document.CreatedAt ?? now,
+ UpdatedAt = now,
+ IncludesCadastralParcels = includesCadastralParcels,
+ Map = snapshotDocument.RootElement.Clone()
+ };
+
+ var path = existing?.Path ?? Path.Combine(ProjectsDirectory, BuildFileName(normalizedName, id));
+ var temporaryPath = path + ".tmp";
+ try
+ {
+ await File.WriteAllTextAsync(
+ temporaryPath,
+ JsonSerializer.Serialize(document, _jsonOptions),
+ new UTF8Encoding(false));
+ File.Move(temporaryPath, path, true);
+ }
+ finally
+ {
+ if (File.Exists(temporaryPath))
+ {
+ File.Delete(temporaryPath);
+ }
+ }
+
+ return document.ToSummary();
+ }
+ finally
+ {
+ _gate.Release();
+ }
+ }
+
+ public async Task LoadAsync(string projectId)
+ {
+ await _gate.WaitAsync();
+ try
+ {
+ var project = (await ReadProjectFilesAsync())
+ .FirstOrDefault(item => item.Document.Id == projectId)
+ ?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
+ return project.Document.Map.GetRawText();
+ }
+ finally
+ {
+ _gate.Release();
+ }
+ }
+
+ public async Task DeleteAsync(string projectId)
+ {
+ await _gate.WaitAsync();
+ try
+ {
+ var project = (await ReadProjectFilesAsync())
+ .FirstOrDefault(item => item.Document.Id == projectId)
+ ?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
+ File.Delete(project.Path);
+ }
+ finally
+ {
+ _gate.Release();
+ }
+ }
+
+ private async Task> ReadProjectFilesAsync()
+ {
+ Directory.CreateDirectory(ProjectsDirectory);
+ var projects = new List();
+ foreach (var path in Directory.EnumerateFiles(ProjectsDirectory, $"*{ProjectExtension}"))
+ {
+ try
+ {
+ await using var stream = File.OpenRead(path);
+ var document = await JsonSerializer.DeserializeAsync(stream, _jsonOptions);
+ if (document is not null
+ && !string.IsNullOrWhiteSpace(document.Id)
+ && !string.IsNullOrWhiteSpace(document.Name)
+ && document.Map.ValueKind == JsonValueKind.Object)
+ {
+ projects.Add(new ProjectFileEntry(path, document));
+ }
+ }
+ catch (JsonException)
+ {
+ // A damaged project is ignored so the remaining archive stays usable.
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ return projects;
+ }
+
+ private static string NormalizeName(string name)
+ {
+ var normalized = string.Join(' ', (name ?? string.Empty).Split(
+ [' ', '\t', '\r', '\n'],
+ StringSplitOptions.RemoveEmptyEntries));
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ throw new ArgumentException("Inserisci il nome del progetto.");
+ }
+
+ return normalized.Length <= 80 ? normalized : normalized[..80];
+ }
+
+ private static string BuildFileName(string name, string id)
+ {
+ var invalid = Path.GetInvalidFileNameChars().ToHashSet();
+ var safeName = new string(name
+ .Select(character => invalid.Contains(character) ? '-' : character)
+ .ToArray())
+ .Trim(' ', '.');
+ safeName = string.IsNullOrWhiteSpace(safeName) ? "Progetto" : safeName;
+ return $"{safeName}--{id[..8]}{ProjectExtension}";
+ }
+
+ private sealed record ProjectFileEntry(string Path, MapProjectDocument Document);
+
+ private sealed class MapProjectDocument
+ {
+ public int SchemaVersion { get; init; }
+ public string Id { get; init; } = string.Empty;
+ public string Name { get; init; } = string.Empty;
+ public DateTimeOffset CreatedAt { get; init; }
+ public DateTimeOffset UpdatedAt { get; init; }
+ public bool IncludesCadastralParcels { get; init; }
+ public JsonElement Map { get; init; }
+
+ public MapProjectSummary ToSummary() => new(Id, Name, UpdatedAt, IncludesCadastralParcels);
+ }
+}
+
+public sealed record MapProjectSummary(
+ string Id,
+ string Name,
+ DateTimeOffset UpdatedAt,
+ bool IncludesCadastralParcels);
diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css
index 8381965..3b17da3 100644
--- a/wwwroot/css/app.css
+++ b/wwwroot/css/app.css
@@ -459,6 +459,19 @@ label {
height: 18px;
}
+.project-save-option {
+ padding: 10px 11px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: #fff;
+}
+
+.project-archive-state {
+ color: var(--muted);
+ font-size: 11px;
+ line-height: 1.35;
+}
+
.map-info {
position: absolute;
left: 12px;
diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js
index 0ceac13..e882673 100644
--- a/wwwroot/js/lme-map.js
+++ b/wwwroot/js/lme-map.js
@@ -43,8 +43,10 @@ window.lmeMap = {
addProjectMarker,
setDrawToolsVisible,
clearDrawings,
- saveWorkspace,
- loadWorkspace,
+ createProjectSnapshot,
+ loadProjectSnapshot,
+ getLegacyProjectSnapshot,
+ clearLegacyProjectSnapshot,
setBaseLayer,
configureGoogleMaps,
configureAzureMaps,
@@ -197,7 +199,7 @@ function displayOfficialParcel(xmlText, latitude, longitude, parcel, label, shou
}
const existingLayer = normalizedParcel
- ? targetGroup.getLayers().find(item => item.lmeParcelReference === ref)
+ ? findParcelLayerByReference(targetGroup, ref)
: null;
if (existingLayer) {
if (shouldFit) {
@@ -235,6 +237,23 @@ function normalizeParcel(parcel) {
return String(parcel || "").trim().toUpperCase();
}
+function findParcelLayerByReference(group, reference) {
+ let match = null;
+ group.eachLayer(layer => {
+ if (match) {
+ return;
+ }
+ if (layer.lmeParcelReference === reference) {
+ match = layer;
+ return;
+ }
+ if (typeof layer.eachLayer === "function") {
+ match = findParcelLayerByReference(layer, reference);
+ }
+ });
+ return match;
+}
+
function resize() {
if (!lmeState.map) {
return;
@@ -459,6 +478,10 @@ function addProjectMarker(name) {
const center = lmeState.map.getCenter();
const label = (name || "Nuovo POI").trim();
const marker = L.marker(center, { draggable: true }).addTo(lmeState.drawings);
+ marker.feature = {
+ type: "Feature",
+ properties: { name: label }
+ };
marker.bindPopup(`${escapeHtml(label)}
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}`).openPopup();
setInfo(`POI aggiunto: ${label}`);
}
@@ -528,33 +551,113 @@ function clearDrawings() {
setInfo("Disegni e POI eliminati dalla mappa.");
}
-function saveWorkspace() {
- const data = {
+function createProjectSnapshot(includeCadastralParcels) {
+ const center = lmeState.map.getCenter();
+ return JSON.stringify({
+ schemaVersion: 1,
+ view: {
+ latitude: center.lat,
+ longitude: center.lng,
+ zoom: lmeState.map.getZoom()
+ },
drawings: lmeState.drawings.toGeoJSON(),
imported: lmeState.imported.toGeoJSON(),
- trail: lmeState.gpsTrailPoints
- };
- localStorage.setItem("lmeMapWorkspace", JSON.stringify(data));
- setInfo("Progetti, POI, file e percorso salvati sul dispositivo.");
+ selectedParcels: includeCadastralParcels
+ ? lmeState.selectedParcelsLayer.toGeoJSON()
+ : { type: "FeatureCollection", features: [] },
+ trail: lmeState.gpsTrailPoints.map(point => [
+ Number(point.lat ?? point[0]),
+ Number(point.lng ?? point[1])
+ ])
+ });
}
-function loadWorkspace() {
- const raw = localStorage.getItem("lmeMapWorkspace");
- if (!raw) {
- setInfo("Nessun progetto salvato trovato.");
- return;
- }
+function getLegacyProjectSnapshot() {
+ return localStorage.getItem("lmeMapWorkspace");
+}
- const data = JSON.parse(raw);
- lmeState.drawings.clearLayers();
- lmeState.imported.clearLayers();
- L.geoJSON(data.drawings, { style: { color: "#f97316", weight: 3, fillOpacity: 0.16 } }).addTo(lmeState.drawings);
- L.geoJSON(data.imported, { style: { color: "#22c55e", weight: 3, fillOpacity: 0.18 } }).addTo(lmeState.imported);
- lmeState.gpsTrailPoints = data.trail || [];
- lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
- fitLayer(L.featureGroup([lmeState.drawings, lmeState.imported]));
- updateGpsInsideState();
- setInfo("Progetto caricato.");
+function clearLegacyProjectSnapshot() {
+ localStorage.removeItem("lmeMapWorkspace");
+}
+
+function loadProjectSnapshot(raw, projectName) {
+ try {
+ const data = typeof raw === "string" ? JSON.parse(raw) : raw;
+ if (!data || typeof data !== "object") {
+ throw new Error("contenuto non valido");
+ }
+
+ lmeState.drawings.clearLayers();
+ lmeState.imported.clearLayers();
+ lmeState.selectedParcelsLayer.clearLayers();
+ lmeState.gpsParcelLayer.clearLayers();
+
+ L.geoJSON(data.drawings || emptyFeatureCollection(), {
+ style: { color: "#f97316", weight: 3, fillOpacity: 0.16 },
+ onEachFeature: bindStoredFeaturePopup
+ }).addTo(lmeState.drawings);
+ L.geoJSON(data.imported || emptyFeatureCollection(), {
+ style: { color: "#22c55e", weight: 3, opacity: 0.95, fillOpacity: 0.18 },
+ pointToLayer: (feature, latlng) => L.circleMarker(latlng, {
+ radius: 7,
+ color: "#22c55e",
+ fillColor: "#ffffff",
+ fillOpacity: 0.9
+ }),
+ onEachFeature: bindStoredFeaturePopup
+ }).addTo(lmeState.imported);
+ L.geoJSON(data.selectedParcels || emptyFeatureCollection(), {
+ style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 },
+ onEachFeature: (feature, layer) => {
+ const properties = feature?.properties || {};
+ const reference = properties.NATIONALCADASTRALREFERENCE
+ || properties.INSPIREID_LOCALID
+ || properties.LABEL
+ || "Particella catastale";
+ layer.lmeParcelReference = reference;
+ layer.bindPopup(`${escapeHtml(reference)}`);
+ }
+ }).addTo(lmeState.selectedParcelsLayer);
+
+ lmeState.gpsTrailPoints = Array.isArray(data.trail) ? data.trail : [];
+ lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints);
+
+ const projectLayers = L.featureGroup([
+ lmeState.drawings,
+ lmeState.imported,
+ lmeState.selectedParcelsLayer,
+ lmeState.gpsTrail
+ ]);
+ if (projectLayers.getBounds().isValid()) {
+ fitLayer(projectLayers);
+ } else if (Number.isFinite(data.view?.latitude)
+ && Number.isFinite(data.view?.longitude)
+ && Number.isFinite(data.view?.zoom)) {
+ lmeState.map.setView([data.view.latitude, data.view.longitude], data.view.zoom);
+ }
+
+ const parcelCount = lmeState.selectedParcelsLayer.toGeoJSON().features?.length || 0;
+ setText("parcelState", parcelCount
+ ? `${parcelCount} particelle del progetto`
+ : "Nessuna particella selezionata");
+ updateGpsInsideState();
+ setInfo(`Progetto caricato: ${projectName || "senza nome"}.`);
+ } catch (error) {
+ setInfo(`Progetto non leggibile: ${error.message}`);
+ throw error;
+ }
+}
+
+function emptyFeatureCollection() {
+ return { type: "FeatureCollection", features: [] };
+}
+
+function bindStoredFeaturePopup(feature, layer) {
+ const properties = feature?.properties || {};
+ const label = properties.name || properties.Name || properties.label || properties.LABEL;
+ if (label) {
+ layer.bindPopup(`${escapeHtml(label)}`);
+ }
}
function setBaseLayer(name) {