diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index 375317f..c65fac7 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -190,7 +190,7 @@ Mostra superficie, perimetro e lati @@ -263,7 +263,7 @@ @@ -361,6 +361,7 @@ private string SelectedParcelNumber { get; set; } = string.Empty; private string CatalogMessage { get; set; } = string.Empty; private double? CatalogFraction { get; set; } + private double CadastralOpacity { get; set; } = 0.75; private bool CatalogBusy { get; set; } private bool GpsActive { get; set; } private bool ShowGpsPosition { get; set; } = true; @@ -460,6 +461,43 @@ } } + private async Task ConfigureProjectMapProviderAsync(string snapshot) + { + string? baseLayer = null; + try + { + using var document = System.Text.Json.JsonDocument.Parse(snapshot); + if (document.RootElement.TryGetProperty("displayOptions", out var displayOptions) + && displayOptions.TryGetProperty("baseLayer", out var baseLayerProperty)) + { + baseLayer = baseLayerProperty.GetString(); + } + } + catch + { + return; + } + + try + { + if (baseLayer?.StartsWith("google-", StringComparison.Ordinal) == true + && !string.IsNullOrWhiteSpace(GoogleMapsApiKey)) + { + await JS.InvokeVoidAsync("lmeMap.configureGoogleMaps", GoogleMapsApiKey.Trim()); + } + + if (baseLayer?.StartsWith("azure-", StringComparison.Ordinal) == true + && !string.IsNullOrWhiteSpace(AzureMapsSubscriptionKey)) + { + await JS.InvokeVoidAsync("lmeMap.configureAzureMaps", AzureMapsSubscriptionKey.Trim()); + } + } + catch + { + // Loading continues with the default map if the saved provider is unavailable. + } + } + private async Task CenterOnInitialLocationAsync() { NetworkLocationSnapshot? networkLocation = null; @@ -976,7 +1014,9 @@ try { var snapshot = await ProjectStorageService.LoadAsync(project.Id); - ShowImportedSheetLayer = await JS.InvokeAsync("lmeMap.loadProjectSnapshot", snapshot, project.Name); + await ConfigureProjectMapProviderAsync(snapshot); + var displayOptions = await JS.InvokeAsync("lmeMap.loadProjectSnapshot", snapshot, project.Name); + ApplyProjectDisplayOptions(displayOptions); NewProjectName = project.Name; IncludeCadastralParcels = project.IncludesCadastralParcels; ActiveTab = "map"; @@ -993,6 +1033,25 @@ } } + private void ApplyProjectDisplayOptions(ProjectDisplayOptions options) + { + SelectedBaseLayer = options.BaseLayer; + CadastralOpacity = options.CadastreOpacity; + IncludeKmlSheetLayer = options.IncludeKmlSheetLayer; + ShowImportedSheetLayer = options.ImportedSheetsVisible; + ShowImportedMeasurements = options.ShowImportedMeasurements; + ShowDrawingMeasurements = options.ShowDrawingMeasurements; + DrawToolsVisible = options.DrawToolsVisible; + ShowGpsPosition = options.GpsPositionVisible; + TrailEnabled = options.TrailEnabled; + + var activeLayers = (options.CadastreLayers ?? []).ToHashSet(StringComparer.Ordinal); + foreach (var layer in CadastralLayers) + { + layer.Enabled = activeLayers.Contains(layer.Key); + } + } + private async Task DeleteProject() { var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId); @@ -1114,7 +1173,18 @@ await JS.InvokeVoidAsync("lmeMap.showInfo", $"Chiave Azure Maps non salvata: {error.GetBaseException().Message}"); } } - private Task ChangeCadastreOpacity(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", args.Value?.ToString()).AsTask(); + private async Task ChangeCadastreOpacity(ChangeEventArgs args) + { + var text = args.Value?.ToString(); + if (!double.TryParse(text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var value) + && !double.TryParse(text, out value)) + { + return; + } + + CadastralOpacity = Math.Clamp(value, 0, 1); + await JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", CadastralOpacity); + } private async Task ToggleCadastreLayer(CadastralLayer layer, ChangeEventArgs args) { @@ -1149,6 +1219,20 @@ public string Detail { get; } = detail; } + private sealed class ProjectDisplayOptions + { + public string BaseLayer { get; init; } = "satellite"; + public double CadastreOpacity { get; init; } = 0.75; + public string[] CadastreLayers { get; init; } = []; + public bool IncludeKmlSheetLayer { get; init; } + public bool ImportedSheetsVisible { get; init; } + public bool ShowImportedMeasurements { get; init; } + public bool ShowDrawingMeasurements { get; init; } = true; + public bool DrawToolsVisible { get; init; } + public bool GpsPositionVisible { get; init; } = true; + public bool TrailEnabled { get; init; } + } + public ValueTask DisposeAsync() { CancelCatalogWork(); diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index beb25a5..3f4ff23 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -28,8 +28,8 @@ com.companyname.lmemappecatastaliapp - 0.2.11 - 15 + 0.2.12 + 16 None diff --git a/README.md b/README.md index 8e18a11..4ac6e73 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe - Filtro KML/KMZ `solo particelle` attivo per impostazione predefinita, con separazione dei MultiPolygon ed esclusione di GroundOverlay e perimetri del foglio; il layer foglio resta importabile e puo essere mostrato o nascosto separatamente. - Inquadramento e pulizia dei layer importati. - Disegno di marker, linee, poligoni e rettangoli. -- Area, Distanza e Cerchio disponibili nella barra Leaflet laterale direttamente sulla mappa, con superficie, perimetro, raggio, circonferenza e lunghezza dei singoli lati. +- Area, Distanza e Cerchio disponibili nella barra Leaflet laterale, nascosta all'avvio e controllata dal pulsante nel tab Dati insieme agli altri strumenti. - Misure permanenti attivabili anche sui layer importati da KML, KMZ e GeoJSON. - Toolbar di disegno completa di icone, modifica, eliminazione singola e pulizia di tutti i disegni/POI. - 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 e il foglio KML/KMZ mantiene la propria visibilita separata. +- Ogni progetto salva file GIS, POI, disegni, traccia, inquadratura e tutte le opzioni di visualizzazione: sfondo, layer catastali, trasparenza, misure, foglio importato, barra strumenti e preferenze GPS. - GPS nativo MAUI ad alta precisione con permessi Android/iOS. - Centratura automatica una tantum all'apertura: prima tramite posizione approssimativa della connessione Internet, poi tramite singola lettura GPS se la rete non restituisce una posizione; nessun punto o cerchio viene mostrato e il tracking continuo non viene avviato. - Punto e cerchio di precisione del GPS live possono essere mostrati o nascosti dalle impostazioni senza fermare il rilevamento. diff --git a/Services/AzureMapsService.cs b/Services/AzureMapsService.cs index e34da2d..e85103a 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.11"); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.12"); _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json"); } diff --git a/Services/NetworkLocationService.cs b/Services/NetworkLocationService.cs index 612fd8a..dcfceb1 100644 --- a/Services/NetworkLocationService.cs +++ b/Services/NetworkLocationService.cs @@ -13,7 +13,7 @@ public sealed class NetworkLocationService public NetworkLocationService() { - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.11"); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2.12"); } public async Task GetApproximateLocationAsync( diff --git a/wwwroot/js/lme-map.js b/wwwroot/js/lme-map.js index 3fb27ce..e0150ec 100644 --- a/wwwroot/js/lme-map.js +++ b/wwwroot/js/lme-map.js @@ -12,7 +12,6 @@ const lmeState = { importedMeasurements: null, drawings: null, drawingMeasurements: null, - quickMeasureControl: null, drawControl: null, activeDrawHandler: null, drawEventsBound: false, @@ -144,7 +143,6 @@ function init() { refreshCadastreLayer(); bindFileImport(); bindDrawingEvents(); - addQuickMeasureControl(); if (!lmeState.resizeBound) { window.addEventListener("resize", resize); @@ -950,11 +948,7 @@ function addProjectMarker(name) { setInfo(`POI aggiunto: ${label}`); } -function addQuickMeasureControl() { - if (!lmeState.map || lmeState.quickMeasureControl) { - return; - } - +function configureDrawLabels() { L.drawLocal.draw.toolbar.buttons.polygon = "Misura area"; L.drawLocal.draw.toolbar.buttons.polyline = "Misura distanza"; L.drawLocal.draw.toolbar.buttons.circle = "Misura cerchio"; @@ -969,32 +963,6 @@ function addQuickMeasureControl() { L.drawLocal.draw.handlers.polyline.tooltip.cont = "Tocca per aggiungere un tratto."; L.drawLocal.draw.handlers.polyline.tooltip.end = "Tocca l'ultimo punto per terminare."; L.drawLocal.draw.handlers.circle.tooltip.start = "Tocca il centro e trascina per il raggio."; - - const orangeArea = { color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 }; - lmeState.quickMeasureControl = new L.Control.Draw({ - position: "topright", - edit: false, - draw: { - circle: { metric: true, feet: false, showRadius: true, shapeOptions: orangeArea }, - circlemarker: false, - marker: false, - polygon: { - metric: true, - feet: false, - showArea: true, - allowIntersection: false, - shapeOptions: orangeArea - }, - polyline: { - metric: true, - feet: false, - showLength: true, - shapeOptions: { color: "#f97316", weight: 4, opacity: 0.95 } - }, - rectangle: false - } - }); - lmeState.map.addControl(lmeState.quickMeasureControl); } function bindDrawingEvents() { @@ -1036,21 +1004,34 @@ function enableDrawTools() { return; } + configureDrawLabels(); + const orangeArea = { color: "#f97316", weight: 4, fillColor: "#fb923c", fillOpacity: 0.2 }; lmeState.drawControl = new L.Control.Draw({ position: "topright", edit: { featureGroup: lmeState.drawings }, draw: { - circle: false, + circle: { metric: true, feet: false, showRadius: true, shapeOptions: orangeArea }, circlemarker: false, marker: true, - polygon: false, - polyline: false, - rectangle: { metric: true, shapeOptions: { color: "#f97316", weight: 4, fillOpacity: 0.2 } } + polygon: { + metric: true, + feet: false, + showArea: true, + allowIntersection: false, + shapeOptions: orangeArea + }, + polyline: { + metric: true, + feet: false, + showLength: true, + shapeOptions: { color: "#f97316", weight: 4, opacity: 0.95 } + }, + rectangle: { metric: true, shapeOptions: orangeArea } } }); lmeState.map.addControl(lmeState.drawControl); bindDrawingEvents(); - setInfo("Barra completa attiva: rettangolo, POI, modifica e cancellazione."); + setInfo("Barra strumenti attiva: misure, disegni, POI, modifica e cancellazione."); } function startMeasurementDrawing(mode) { @@ -1377,7 +1358,7 @@ function formatNumber(value, digits) { function createProjectSnapshot(includeCadastralParcels) { const center = lmeState.map.getCenter(); return JSON.stringify({ - schemaVersion: 2, + schemaVersion: 3, view: { latitude: center.lat, longitude: center.lng, @@ -1387,6 +1368,7 @@ function createProjectSnapshot(includeCadastralParcels) { imported: lmeState.imported.toGeoJSON(), importedSheets: lmeState.importedSheets.toGeoJSON(), importedSheetsVisible: lmeState.importedSheetsVisible, + displayOptions: captureDisplayOptions(), selectedParcels: includeCadastralParcels ? lmeState.selectedParcelsLayer.toGeoJSON() : { type: "FeatureCollection", features: [] }, @@ -1460,7 +1442,12 @@ function loadProjectSnapshot(raw, projectName) { }), onEachFeature: bindStoredFeaturePopup }).addTo(lmeState.importedSheets); - lmeState.importedSheetsVisible = data.schemaVersion >= 2 && Boolean(data.importedSheetsVisible); + const projectDisplayOptions = data.schemaVersion >= 3 && data.displayOptions + ? data.displayOptions + : null; + lmeState.importedSheetsVisible = projectDisplayOptions + ? Boolean(projectDisplayOptions.importedSheetsVisible) + : data.schemaVersion >= 2 && Boolean(data.importedSheetsVisible); applyImportedSheetVisibility(); L.geoJSON(data.selectedParcels || emptyFeatureCollection(), { style: { color: "#ef4444", weight: 4, opacity: 1, fillColor: "#fbbf24", fillOpacity: 0.26 }, @@ -1477,6 +1464,9 @@ function loadProjectSnapshot(raw, projectName) { lmeState.gpsTrailPoints = Array.isArray(data.trail) ? data.trail : []; lmeState.gpsTrail.setLatLngs(lmeState.gpsTrailPoints); + if (projectDisplayOptions) { + applyProjectDisplayOptions(projectDisplayOptions); + } refreshImportedMeasurements(); refreshDrawingMeasurements(); @@ -1507,13 +1497,60 @@ function loadProjectSnapshot(raw, projectName) { ? ` ${importedParts.sheets.features.length} elementi del foglio sono nascosti.` : ""; setInfo(`Progetto caricato: ${projectName || "senza nome"}.${hiddenSheets}`); - return lmeState.importedSheetsVisible; + return captureDisplayOptions(); } catch (error) { setInfo(`Progetto non leggibile: ${error.message}`); throw error; } } +function captureDisplayOptions() { + return { + baseLayer: lmeState.currentBaseName, + cadastreOpacity: lmeState.cadastreOpacity, + cadastreLayers: [...lmeState.cadastreLayers], + includeKmlSheetLayer: lmeState.includeKmlSheetLayer, + importedSheetsVisible: lmeState.importedSheetsVisible, + showImportedMeasurements: lmeState.showImportedMeasurements, + showDrawingMeasurements: lmeState.showDrawingMeasurements, + drawToolsVisible: Boolean(lmeState.drawControl), + gpsPositionVisible: lmeState.gpsPositionVisible, + trailEnabled: lmeState.gpsTrailEnabled + }; +} + +function applyProjectDisplayOptions(options) { + const supportedCadastreLayers = new Set([ + "CP.CadastralParcel", + "codice_plla", + "CP.CadastralZoning", + "fabbricati", + "strade", + "acque" + ]); + if (Array.isArray(options.cadastreLayers)) { + lmeState.cadastreLayers = options.cadastreLayers.filter(layer => supportedCadastreLayers.has(layer)); + } + + const opacity = Number(options.cadastreOpacity); + if (Number.isFinite(opacity)) { + lmeState.cadastreOpacity = Math.max(0, Math.min(1, opacity)); + } + lmeState.includeKmlSheetLayer = Boolean(options.includeKmlSheetLayer); + lmeState.importedSheetsVisible = Boolean(options.importedSheetsVisible); + lmeState.showImportedMeasurements = Boolean(options.showImportedMeasurements); + lmeState.showDrawingMeasurements = options.showDrawingMeasurements !== false; + applyImportedSheetVisibility(); + refreshCadastreLayer(); + + if (typeof options.baseLayer === "string") { + setBaseLayer(options.baseLayer); + } + setGpsPositionVisible(options.gpsPositionVisible !== false); + toggleTrail(Boolean(options.trailEnabled)); + setDrawToolsVisible(Boolean(options.drawToolsVisible)); +} + function partitionStoredImportedFeatures(imported, storedSheets) { const parcelFeatures = []; const sheetFeatures = [...(storedSheets?.features || [])]; @@ -1748,7 +1785,11 @@ function htmlToText(value) { } function setCadastreOpacity(value) { - lmeState.cadastreOpacity = Number(value || 0.75); + const opacity = Number(value); + if (!Number.isFinite(opacity)) { + return; + } + lmeState.cadastreOpacity = Math.max(0, Math.min(1, opacity)); if (lmeState.cadastre) { lmeState.cadastre.setOpacity(lmeState.cadastreOpacity); }