Files
LMEMappeCatastaliApp/Components/Pages/Home.razor
T

672 lines
27 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/"
@inject IJSRuntime JS
@inject PreciseLocationService LocationService
@inject CadastralCatalogService CatalogService
@inject OfficialCadastreService OfficialCadastreService
@inject LocationSearchService LocationSearchService
@implements IAsyncDisposable
<PageTitle>LME Mappe Catastali</PageTitle>
<div class="map-app">
<section class="map-stage">
<div id="map"></div>
<header class="map-toolbar">
<div class="app-identity">
<strong>LME Catasto</strong>
<span>@Status</span>
</div>
<button class="toolbar-button" @onclick='() => SelectTab(ActiveTab == "search" ? "map" : "search")'>Cerca</button>
</header>
<div class="map-search @(ActiveTab == "search" ? "is-open" : null)">
<input @bind="SearchText" @bind:event="oninput" placeholder="Localita, indirizzo o coordinate" />
<button @onclick="SearchLocation">Vai</button>
</div>
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)">
<div class="gps-state">
<span id="gpsState">GPS non attivo</span>
<strong id="parcelState">Nessuna particella selezionata</strong>
</div>
<button class="gps-main" @onclick="ToggleGps">@(GpsActive ? "Ferma GPS" : "Avvia GPS")</button>
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button>
</div>
@if (ActiveTab is not "map" and not "search")
{
<aside class="tool-sheet">
<div class="sheet-header">
<div>
<p class="eyebrow">@PanelEyebrow</p>
<h1>@PanelTitle</h1>
</div>
<button class="close-button" @onclick='() => SelectTab("map")' aria-label="Chiudi pannello">×</button>
</div>
@if (ActiveTab == "cadastre")
{
<section class="panel-content">
<label>
Regione
<select value="@SelectedRegionFile" @onchange="OnRegionChanged">
<option value="">Seleziona regione</option>
@foreach (var region in Regions)
{
<option value="@region.DataFile">@region.Name</option>
}
</select>
</label>
<label>
Comune
<select value="@SelectedMunicipalityCode" @onchange="OnMunicipalityChanged" disabled="@(Municipalities.Count == 0)">
<option value="">Seleziona comune</option>
@foreach (var municipality in Municipalities)
{
<option value="@municipality.Code">@municipality.Name (@municipality.Code)</option>
}
</select>
</label>
<label>
Foglio
<select value="@SelectedSheet" @onchange="OnSheetChanged" disabled="@(Sheets.Count == 0 || CatalogBusy)">
<option value="">Seleziona foglio</option>
@foreach (var sheet in Sheets)
{
<option value="@sheet">@DisplaySheet(sheet)</option>
}
</select>
</label>
<label>
Particella
<select value="@SelectedParcelNumber" @onchange="OnParcelChanged" disabled="@(Parcels.Count == 0 || CatalogBusy)">
<option value="">Seleziona particella</option>
@foreach (var parcel in Parcels)
{
<option value="@parcel.Number">@parcel.Number</option>
}
</select>
</label>
@if (!string.IsNullOrWhiteSpace(CatalogMessage))
{
<div class="catalog-progress">
<span>@CatalogMessage</span>
@if (CatalogFraction.HasValue)
{
<progress value="@CatalogFraction.Value" max="1"></progress>
}
</div>
}
<button class="primary-command" @onclick="LocateParcel" disabled="@(SelectedParcel is null || CatalogBusy)">Mostra particella sulla mappa</button>
<div class="service-note">
<strong>Mappa pubblica ufficiale</strong>
<p>I confini visualizzati arrivano dai servizi WMS/WFS open data dell'Agenzia delle Entrate e non richiedono CAPTCHA.</p>
</div>
</section>
}
else if (ActiveTab == "projects")
{
<section class="panel-content">
<h2>File GIS</h2>
<label class="file-picker">
<span>Carica KML, KMZ o GeoJSON</span>
<input id="gisFileInput" type="file" accept=".kml,.kmz,.geojson,.json,application/vnd.google-earth.kml+xml,application/vnd.google-earth.kmz,application/geo+json" />
</label>
<div class="button-row">
<button @onclick="FitImportedLayers">Inquadra</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Pulisci</button>
</div>
<h2>Progetti e POI</h2>
<label>
Nome
<input @bind="ProjectName" @bind:event="oninput" placeholder="Nome progetto o POI" />
</label>
<button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button>
<button class="secondary-command" aria-pressed="@DrawToolsVisible" @onclick="ToggleDrawTools">
@(DrawToolsVisible ? "Nascondi strumenti di disegno" : "Mostra strumenti di disegno")
</button>
<div class="button-row">
<button @onclick="SaveWorkspace">Salva</button>
<button @onclick="LoadWorkspace">Carica</button>
</div>
<button class="danger-command" @onclick="ClearDrawings">Elimina disegni e POI</button>
</section>
}
else if (ActiveTab == "settings")
{
<section class="panel-content">
<h2>Visualizzazione</h2>
<label>
Sfondo
<select value="@SelectedBaseLayer" @onchange="ChangeBaseLayer">
<option value="satellite">Satellite - Esri World Imagery</option>
<option value="osm">Strade - OpenStreetMap</option>
<option value="streets-esri">Strade dettagliate - Esri World Street Map</option>
<option value="topographic">Topografica - Esri World Topographic</option>
<option value="terrain">Terreno - OpenTopoMap</option>
<option value="carto">Strade chiare - CARTO Voyager</option>
<option value="google-satellite">Satellite - Google Maps</option>
<option value="google-roadmap">Strade - Google Maps</option>
</select>
</label>
<label>
Chiave Google Maps Platform
<input type="password" @bind="GoogleMapsApiKey" placeholder="API key" autocomplete="off" />
</label>
<button class="secondary-command" @onclick="SaveGoogleMapsApiKey">Salva chiave Google</button>
<label>
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="0.75" @onchange="ChangeCadastreOpacity" />
</label>
<div class="layer-toggles">
@foreach (var layer in CadastralLayers)
{
<label>
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer.Key, args)" />
@layer.Name
</label>
}
</div>
<h2>GPS e percorso</h2>
<label class="toggle-line">
<input type="checkbox" checked="@TrailEnabled" @onchange="ToggleTrail" />
Registra la traccia mentre cammini
</label>
<h2>Servizi Agenzia delle Entrate</h2>
<div class="service-note protected-service">
<strong>Download documenti e fogli ufficiali</strong>
<p>Il CAPTCHA o l'accesso SPID/CIE vengono gestiti direttamente dal sito dell'Agenzia. Per sicurezza l'app non tenta di aggirarli.</p>
<button class="primary-command" @onclick="OpenOfficialGeoportal">Apri Geoportale catastale</button>
<button class="secondary-command" @onclick="OpenOfficialReservedArea">Apri Area riservata per download</button>
</div>
<button class="secondary-command" @onclick="ExportCadastralImage">Apri vista WMS corrente</button>
<footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer>
</section>
}
</aside>
}
<div id="mapInfo" class="map-info">Pronto</div>
<nav class="bottom-tabs" aria-label="Sezioni principali">
<button class="@(ActiveTab == "map" || ActiveTab == "search" ? "active" : null)" @onclick='() => SelectTab("map")'>Mappa</button>
<button class="@(ActiveTab == "cadastre" ? "active" : null)" @onclick='() => SelectTab("cadastre")'>Catasto</button>
<button class="@(ActiveTab == "projects" ? "active" : null)" @onclick='() => SelectTab("projects")'>Progetti</button>
<button class="@(ActiveTab == "settings" ? "active" : null)" @onclick='() => SelectTab("settings")'>Impostazioni</button>
</nav>
</section>
</div>
@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<RegionOption> Regions { get; set; } = [];
private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = [];
private IReadOnlyList<string> Sheets { get; set; } = [];
private IReadOnlyList<ParcelOption> 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<CadastralLayer> 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<CatalogProgress> CreateProgress() => new Progress<CatalogProgress>(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;
}
}