@page "/" @inject IJSRuntime JS @inject PreciseLocationService LocationService @inject CadastralCatalogService CatalogService @inject OfficialCadastreService OfficialCadastreService @inject LocationSearchService LocationSearchService @implements IAsyncDisposable LME Mappe Catastali
LME Catasto @Status
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 ProjectName { get; set; } = string.Empty; private string Status { get; set; } = "online"; private string SelectedBaseLayer { get; set; } = "satellite"; private string GoogleMapsApiKey { 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 bool CatalogBusy { get; set; } private bool GpsActive { get; set; } private bool DrawToolsVisible { get; set; } private bool TrailEnabled { get; set; } 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 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", "projects" => "Progetti e dati", "settings" => "Impostazioni", _ => string.Empty }; private string PanelEyebrow => ActiveTab switch { "cadastre" => "Regione, Comune, Foglio, Particella", "projects" => "KML, KMZ, GeoJSON e POI", "settings" => "Mappa, GPS e servizi ufficiali", _ => string.Empty }; private readonly List CadastralLayers = [ new("CP.CadastralParcel", "Particelle", true), new("codice_plla", "Numeri particelle", true), new("CP.CadastralZoning", "Fogli", true), new("fabbricati", "Fabbricati", true), new("strade", "Strade", 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 { Regions = await CatalogService.GetRegionsAsync(); } catch (Exception error) { CatalogMessage = $"Catalogo Comuni non disponibile: {error.Message}"; } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("lmeMap.init"); } } private async Task SelectTab(string tab) { ActiveTab = tab; 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 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 Task ClearImportedLayers() => JS.InvokeVoidAsync("lmeMap.clearImported").AsTask(); private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", ProjectName).AsTask(); 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 Task SaveWorkspace() => JS.InvokeVoidAsync("lmeMap.saveWorkspace").AsTask(); private Task LoadWorkspace() => JS.InvokeVoidAsync("lmeMap.loadWorkspace").AsTask(); 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()); } 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 Task ChangeCadastreOpacity(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setCadastreOpacity", args.Value?.ToString()).AsTask(); private Task ToggleCadastreLayer(string layer, ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer, (bool?)args.Value == true).AsTask(); private Task ExportCadastralImage() => JS.InvokeVoidAsync("lmeMap.exportCadastreView").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 record CadastralLayer(string Key, string Name, bool Enabled); public ValueTask DisposeAsync() { CancelCatalogWork(); LocationService.StopTracking(); GpsLookupCancellation?.Cancel(); GpsLookupCancellation?.Dispose(); return ValueTask.CompletedTask; } }