@page "/" @inject IJSRuntime JS @inject PreciseLocationService LocationService @inject CadastralCatalogService CatalogService @inject OfficialCadastreService OfficialCadastreService @inject LocationSearchService LocationSearchService @inject NetworkLocationService NetworkLocationService @inject AzureMapsService AzureMapsService @inject ProjectStorageService ProjectStorageService @inject MapExportService MapExportService @implements IAsyncDisposable LME Mappe Catastali
LME Catasto @Status
@if (LayerPanelOpen) {
Layer catastali
@foreach (var layer in CadastralLayers) { }
}
GPS non attivo Nessuna particella selezionata
@if (ActiveTab is not "map" and not "search") { }
Pronto
@code { private string ActiveTab { get; set; } = "map"; private string SearchText { get; set; } = string.Empty; private string NewProjectName { get; set; } = string.Empty; private string ExportFileName { get; set; } = "Mappa"; private string ExportFormat { get; set; } = "geojson"; 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; private string AzureMapsSubscriptionKey { get; set; } = string.Empty; private string SelectedRegionFile { get; set; } = string.Empty; private string SelectedMunicipalityCode { get; set; } = string.Empty; private string SelectedSheet { get; set; } = string.Empty; 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; private bool DrawToolsVisible { get; set; } private bool TrailEnabled { get; set; } private bool LayerPanelOpen { get; set; } private bool IncludeCadastralParcels { get; set; } = true; private bool IncludeKmlSheetLayer { get; set; } private bool ShowImportedSheetLayer { get; set; } private bool ShowImportedMeasurements { get; set; } private bool ShowDrawingMeasurements { get; set; } = true; private bool ProjectBusy { get; set; } private bool ExportBusy { get; set; } private bool ExportImported { get; set; } = true; private bool ExportImportedSheets { get; set; } = true; private bool ExportCadastral { get; set; } = true; private bool ExportDrawings { get; set; } = true; private bool ExportPoi { get; set; } = true; private bool ExportTrail { get; set; } = true; private CancellationTokenSource? CatalogCancellation { get; set; } private CancellationTokenSource? GpsLookupCancellation { get; set; } private long LastGpsLookupAt; private int GpsLookupInProgress; private IReadOnlyList Regions { get; set; } = []; 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); private ParcelOption? SelectedParcel => Parcels.FirstOrDefault(item => item.Number == SelectedParcelNumber); private string PanelTitle => ActiveTab switch { "cadastre" => "Ricerca catastale", "data" => "Dati e disegni", "projects" => "Archivio progetti", "save" => "Salvataggio ed esportazione", "settings" => "Impostazioni", _ => string.Empty }; private string PanelEyebrow => ActiveTab switch { "cadastre" => "Regione, Comune, Foglio, Particella", "data" => "File GIS, misure e POI", "projects" => "Salva, apri e visualizza i tuoi lavori", "save" => "Esporta file GIS", "settings" => "Mappa, GPS e servizi ufficiali", _ => string.Empty }; private readonly List CadastralLayers = [ new("CP.CadastralParcel", "Confini particelle", true), new("codice_plla", "Numeri particelle", true, "Visibili circa da zoom 17"), new("CP.CadastralZoning", "Fogli catastali", true), new("fabbricati", "Fabbricati", true), new("strade", "Strade catastali", false), new("acque", "Acque", false) ]; protected override async Task OnInitializedAsync() { try { GoogleMapsApiKey = await SecureStorage.Default.GetAsync("google_maps_api_key") ?? string.Empty; } catch { GoogleMapsApiKey = string.Empty; } try { AzureMapsSubscriptionKey = await SecureStorage.Default.GetAsync("azure_maps_subscription_key") ?? string.Empty; } catch { AzureMapsSubscriptionKey = string.Empty; } try { Regions = await CatalogService.GetRegionsAsync(); } catch (Exception error) { CatalogMessage = $"Catalogo Comuni non disponibile: {error.Message}"; } await RefreshProjectsAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("lmeMap.init"); await CenterOnInitialLocationAsync(); await MigrateLegacyProjectAsync(); } } 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; try { using var networkTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(8)); networkLocation = await NetworkLocationService.GetApproximateLocationAsync(networkTimeout.Token); } catch { // An unavailable IP service must not prevent the GPS fallback. } if (networkLocation is not null) { await JS.InvokeVoidAsync( "lmeMap.showInitialPosition", networkLocation.Latitude, networkLocation.Longitude, true, networkLocation.Label); return; } try { using var gpsTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(18)); var location = await LocationService.GetCurrentLocationAsync(gpsTimeout.Token); if (location is not null) { await JS.InvokeVoidAsync( "lmeMap.showInitialPosition", location.Latitude, location.Longitude, false, string.Empty); } else { await JS.InvokeVoidAsync("lmeMap.showInfo", "Posizione iniziale non disponibile. La mappa resta utilizzabile normalmente."); } } catch (OperationCanceledException) { await JS.InvokeVoidAsync("lmeMap.showInfo", "Posizione iniziale non disponibile. Usa Avvia GPS per riprovare."); } catch (Exception error) { await JS.InvokeVoidAsync("lmeMap.showInfo", $"Posizione iniziale non disponibile: {error.GetBaseException().Message}"); } } 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. } } private async Task SelectTab(string tab) { ActiveTab = tab; if (tab != "map") { LayerPanelOpen = false; } if (tab == "projects") { await RefreshProjectsAsync(); } await Task.Delay(30); await JS.InvokeVoidAsync("lmeMap.resize"); } private async Task ToggleLayerPanel() { LayerPanelOpen = !LayerPanelOpen; if (LayerPanelOpen) { ActiveTab = "map"; } await Task.Delay(30); await JS.InvokeVoidAsync("lmeMap.resize"); } private async Task SearchLocation() { if (string.IsNullOrWhiteSpace(SearchText)) { await JS.InvokeVoidAsync("lmeMap.showInfo", "Inserisci una localita, un indirizzo o due coordinate."); return; } try { Status = "ricerca..."; var result = await LocationSearchService.SearchAsync(SearchText); if (result is null) { Status = "online"; await JS.InvokeVoidAsync("lmeMap.showInfo", "Localita non trovata. Prova ad aggiungere Comune e Provincia."); return; } await JS.InvokeVoidAsync("lmeMap.showSearchResult", result.Latitude, result.Longitude, result.Label); ActiveTab = "map"; Status = "online"; } catch (Exception error) { Status = "errore ricerca"; await JS.InvokeVoidAsync("lmeMap.showInfo", $"Ricerca non riuscita: {error.Message}"); } } private async Task OnRegionChanged(ChangeEventArgs args) { CancelCatalogWork(); SelectedRegionFile = args.Value?.ToString() ?? string.Empty; SelectedMunicipalityCode = string.Empty; SelectedSheet = string.Empty; SelectedParcelNumber = string.Empty; Sheets = []; Parcels = []; CatalogMessage = string.Empty; Municipalities = string.IsNullOrWhiteSpace(SelectedRegionFile) ? [] : await CatalogService.GetMunicipalitiesAsync(SelectedRegionFile); } private async Task OnMunicipalityChanged(ChangeEventArgs args) { SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty; SelectedSheet = string.Empty; SelectedParcelNumber = string.Empty; Sheets = []; Parcels = []; var municipality = SelectedMunicipality; if (municipality is null) { return; } var token = BeginCatalogWork(); CatalogBusy = true; CatalogMessage = "Preparo il catalogo regionale..."; CatalogFraction = null; try { Sheets = await CatalogService.GetSheetsAsync(municipality, CreateProgress(), token); CatalogMessage = $"Seleziona uno dei {Sheets.Count} fogli disponibili."; var mapLocation = await LocationSearchService.SearchAsync($"{municipality.Name}, Italia", token); if (mapLocation is not null) { await JS.InvokeVoidAsync("lmeMap.showSearchResult", mapLocation.Latitude, mapLocation.Longitude, mapLocation.Label); } } catch (OperationCanceledException) { } catch (Exception error) { CatalogMessage = $"Impossibile caricare i fogli: {error.Message}"; await JS.InvokeVoidAsync("lmeMap.showInfo", CatalogMessage); } finally { CatalogBusy = false; CatalogFraction = null; } } private async Task OnSheetChanged(ChangeEventArgs args) { SelectedSheet = args.Value?.ToString() ?? string.Empty; SelectedParcelNumber = string.Empty; Parcels = []; var municipality = SelectedMunicipality; if (municipality is null || string.IsNullOrWhiteSpace(SelectedSheet)) { return; } var token = BeginCatalogWork(); CatalogBusy = true; CatalogMessage = "Carico le particelle del foglio..."; CatalogFraction = null; try { Parcels = await CatalogService.GetParcelsAsync(municipality, SelectedSheet, CreateProgress(), token); CatalogMessage = $"Seleziona una delle {Parcels.Count} particelle disponibili."; } catch (OperationCanceledException) { } catch (Exception error) { CatalogMessage = $"Impossibile caricare le particelle: {error.Message}"; await JS.InvokeVoidAsync("lmeMap.showInfo", CatalogMessage); } finally { CatalogBusy = false; CatalogFraction = null; } } private Task OnParcelChanged(ChangeEventArgs args) { SelectedParcelNumber = args.Value?.ToString() ?? string.Empty; return Task.CompletedTask; } private async Task LocateParcel() { var municipality = SelectedMunicipality; var parcel = SelectedParcel; if (municipality is null || parcel is null) { return; } try { var label = $"{municipality.Name} - foglio {DisplaySheet(SelectedSheet)}, particella {parcel.Number}"; CatalogBusy = true; CatalogMessage = "Scarico il confine dal servizio ufficiale AdE..."; var xml = await OfficialCadastreService.GetParcelsNearAsync(parcel.Latitude, parcel.Longitude, 8); await JS.InvokeVoidAsync("lmeMap.displayOfficialParcel", xml, parcel.Latitude, parcel.Longitude, parcel.Number, label, true); ActiveTab = "map"; await Task.Delay(30); await JS.InvokeVoidAsync("lmeMap.resize"); } catch (Exception error) { CatalogMessage = $"Particella non visualizzata: {error.Message}"; await JS.InvokeVoidAsync("lmeMap.showInfo", CatalogMessage); } finally { CatalogBusy = false; } } private async Task ToggleGps() { if (GpsActive) { await StopGps(); } else { await StartGps(); } } private async Task ToggleGpsPosition(ChangeEventArgs args) { ShowGpsPosition = (bool?)args.Value == true; await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition); } private async Task StartGps() { GpsActive = true; GpsLookupCancellation?.Cancel(); GpsLookupCancellation?.Dispose(); GpsLookupCancellation = new CancellationTokenSource(); var lookupToken = GpsLookupCancellation.Token; try { await LocationService.StartTrackingAsync( location => HandleGpsLocationAsync(location, lookupToken), async error => await InvokeAsync(() => JS.InvokeVoidAsync("lmeMap.setGpsError", error))); GpsActive = LocationService.IsTracking; } catch (Exception error) { GpsActive = false; await JS.InvokeVoidAsync("lmeMap.setGpsError", error.Message); } } private async Task StopGps() { LocationService.StopTracking(); GpsLookupCancellation?.Cancel(); GpsLookupCancellation?.Dispose(); GpsLookupCancellation = null; GpsActive = false; await JS.InvokeVoidAsync("lmeMap.stopGps"); } private async Task HandleGpsLocationAsync(LocationSnapshot location, CancellationToken cancellationToken) { await InvokeAsync(() => JS.InvokeVoidAsync("lmeMap.setNativeGpsPosition", location)); var now = Environment.TickCount64; if (now - Interlocked.Read(ref LastGpsLookupAt) < 3_500 || Interlocked.CompareExchange(ref GpsLookupInProgress, 1, 0) != 0) { return; } Interlocked.Exchange(ref LastGpsLookupAt, now); try { var radius = Math.Clamp(location.Accuracy ?? 5, 2, 25); var xml = await OfficialCadastreService.GetParcelsNearAsync( location.Latitude, location.Longitude, radius, cancellationToken); await InvokeAsync(() => JS.InvokeVoidAsync( "lmeMap.displayOfficialParcel", xml, location.Latitude, location.Longitude, null, "Posizione GPS", false)); } catch (OperationCanceledException) { } catch (Exception error) { await InvokeAsync(() => JS.InvokeVoidAsync( "lmeMap.showInfo", $"Aggiornamento particella GPS non disponibile: {error.GetBaseException().Message}")); } finally { Interlocked.Exchange(ref GpsLookupInProgress, 0); } } private IProgress CreateProgress() => new Progress(progress => { _ = InvokeAsync(() => { CatalogMessage = progress.Message; CatalogFraction = progress.Fraction; StateHasChanged(); }); }); private CancellationToken BeginCatalogWork() { CancelCatalogWork(); CatalogCancellation = new CancellationTokenSource(); return CatalogCancellation.Token; } private void CancelCatalogWork() { CatalogCancellation?.Cancel(); CatalogCancellation?.Dispose(); CatalogCancellation = null; } private static string DisplaySheet(string value) { var trimmed = value.TrimStart('0'); return string.IsNullOrEmpty(trimmed) ? "0" : trimmed; } private Task FitImportedLayers() => JS.InvokeVoidAsync("lmeMap.fitImported").AsTask(); private async Task ClearImportedLayers() { ShowImportedSheetLayer = false; await JS.InvokeVoidAsync("lmeMap.clearImported"); } private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask(); private async Task ApplyKmlSheetLayerOption() { ShowImportedSheetLayer = IncludeKmlSheetLayer; await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer); await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer); } private Task ApplyImportedSheetLayerVisibility() { return JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer).AsTask(); } private async Task ToggleImportedMeasurements(ChangeEventArgs args) { ShowImportedMeasurements = (bool?)args.Value == true; await JS.InvokeVoidAsync("lmeMap.setImportedMeasurementsVisible", ShowImportedMeasurements); } private async Task ToggleDrawingMeasurements(ChangeEventArgs args) { ShowDrawingMeasurements = (bool?)args.Value == true; await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements); } private async Task ToggleDrawTools() { DrawToolsVisible = !DrawToolsVisible; await JS.InvokeVoidAsync("lmeMap.setDrawToolsVisible", DrawToolsVisible); if (DrawToolsVisible) { await SelectTab("map"); } } private Task ClearDrawings() => JS.InvokeVoidAsync("lmeMap.clearDrawings").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 ExportMapFile() { if (!ExportImported && !ExportImportedSheets && !ExportCadastral && !ExportDrawings && !ExportPoi && !ExportTrail) { await JS.InvokeVoidAsync("lmeMap.showInfo", "Seleziona almeno un tipo di oggetto da esportare."); return; } ExportBusy = true; try { var geoJson = await JS.InvokeAsync("lmeMap.createMapExportGeoJson", new { includeImported = ExportImported, includeImportedSheets = ExportImportedSheets, includeCadastral = ExportCadastral, includeDrawings = ExportDrawings, includePoi = ExportPoi, includeTrail = ExportTrail }); var exported = await MapExportService.CreateAsync(ExportFileName, ExportFormat, geoJson); await Share.Default.RequestAsync(new ShareFileRequest { Title = $"Salva o condividi {exported.FileName}", File = new ShareFile(exported.Path, exported.MimeType) }); await JS.InvokeVoidAsync( "lmeMap.showInfo", $"File {exported.FileName} pronto: {exported.FeatureCount} oggetti esportati."); } catch (Exception error) { await JS.InvokeVoidAsync("lmeMap.showInfo", $"Esportazione non riuscita: {error.GetBaseException().Message}"); } finally { ExportBusy = false; } } 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 ConfigureProjectMapProviderAsync(snapshot); var displayOptions = await JS.InvokeAsync("lmeMap.loadProjectSnapshot", snapshot, project.Name); ApplyProjectDisplayOptions(displayOptions); NewProjectName = project.Name; ExportFileName = 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 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); 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) { TrailEnabled = (bool?)args.Value == true; await JS.InvokeVoidAsync("lmeMap.toggleTrail", TrailEnabled); } private async Task ChangeBaseLayer(ChangeEventArgs args) { var requestedLayer = args.Value?.ToString() ?? "satellite"; try { if (requestedLayer.StartsWith("google-", StringComparison.Ordinal)) { if (string.IsNullOrWhiteSpace(GoogleMapsApiKey)) { await JS.InvokeVoidAsync("lmeMap.showInfo", "Inserisci e salva una chiave Google Maps Platform per usare questo sfondo."); return; } await JS.InvokeVoidAsync("lmeMap.configureGoogleMaps", GoogleMapsApiKey.Trim()); } if (requestedLayer.StartsWith("azure-", StringComparison.Ordinal)) { if (string.IsNullOrWhiteSpace(AzureMapsSubscriptionKey)) { await JS.InvokeVoidAsync("lmeMap.showInfo", "Inserisci e salva la chiave della sottoscrizione Azure Maps."); return; } await AzureMapsService.ValidateKeyAsync(AzureMapsSubscriptionKey); await JS.InvokeVoidAsync("lmeMap.configureAzureMaps", AzureMapsSubscriptionKey.Trim()); } await JS.InvokeVoidAsync("lmeMap.setBaseLayer", requestedLayer); SelectedBaseLayer = requestedLayer; } catch (Exception error) { await JS.InvokeVoidAsync("lmeMap.showInfo", $"Sfondo non disponibile: {error.GetBaseException().Message}"); } } private async Task SaveGoogleMapsApiKey() { try { if (string.IsNullOrWhiteSpace(GoogleMapsApiKey)) { SecureStorage.Default.Remove("google_maps_api_key"); await JS.InvokeVoidAsync("lmeMap.showInfo", "Chiave Google rimossa."); return; } GoogleMapsApiKey = GoogleMapsApiKey.Trim(); await SecureStorage.Default.SetAsync("google_maps_api_key", GoogleMapsApiKey); await JS.InvokeVoidAsync("lmeMap.configureGoogleMaps", GoogleMapsApiKey); await JS.InvokeVoidAsync("lmeMap.showInfo", "Chiave Google salvata sul dispositivo."); } catch (Exception error) { await JS.InvokeVoidAsync("lmeMap.showInfo", $"Chiave Google non salvata: {error.Message}"); } } private async Task SaveAzureMapsKey() { try { if (string.IsNullOrWhiteSpace(AzureMapsSubscriptionKey)) { SecureStorage.Default.Remove("azure_maps_subscription_key"); await JS.InvokeVoidAsync("lmeMap.clearAzureMaps"); if (SelectedBaseLayer.StartsWith("azure-", StringComparison.Ordinal)) { SelectedBaseLayer = "satellite"; } await JS.InvokeVoidAsync("lmeMap.showInfo", "Chiave Azure Maps rimossa."); return; } AzureMapsSubscriptionKey = AzureMapsSubscriptionKey.Trim(); await AzureMapsService.ValidateKeyAsync(AzureMapsSubscriptionKey); await SecureStorage.Default.SetAsync("azure_maps_subscription_key", AzureMapsSubscriptionKey); await JS.InvokeVoidAsync("lmeMap.configureAzureMaps", AzureMapsSubscriptionKey); await JS.InvokeVoidAsync("lmeMap.showInfo", "Chiave Azure Maps valida e salvata sul dispositivo."); } catch (Exception error) { await JS.InvokeVoidAsync("lmeMap.showInfo", $"Chiave Azure Maps non salvata: {error.GetBaseException().Message}"); } } 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) { layer.Enabled = args.Value is true; await JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer.Key, layer.Enabled); } private async Task ExportCadastralImage() { ActiveTab = "map"; LayerPanelOpen = false; await Task.Delay(30); await JS.InvokeVoidAsync("lmeMap.resize"); await JS.InvokeVoidAsync("lmeMap.exportCadastreView"); } private Task ClearSelectedParcels() => JS.InvokeVoidAsync("lmeMap.clearSelectedParcels").AsTask(); private Task ClearMapElements() => JS.InvokeVoidAsync("lmeMap.clearMapElements").AsTask(); private Task OpenOfficialGeoportal() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( new Uri("https://geoportale.cartografia.agenziaentrate.gov.it/")); private Task OpenOfficialReservedArea() => Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( new Uri("https://www.agenziaentrate.gov.it/portale/area-riservata")); private sealed class CadastralLayer(string key, string name, bool enabled, string detail = "") { public string Key { get; } = key; public string Name { get; } = name; public bool Enabled { get; set; } = enabled; 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(); LocationService.StopTracking(); GpsLookupCancellation?.Cancel(); GpsLookupCancellation?.Dispose(); return ValueTask.CompletedTask; } }