Files
LMEMappeCatastaliProApp/Components/Pages/Home.razor
T

506 lines
21 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
@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>
<div class="button-row">
<button @onclick="EnableDrawTools">Disegna</button>
<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 @onchange="ChangeBaseLayer">
<option value="satellite">Satellite</option>
<option value="osm">Strade</option>
</select>
</label>
<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 @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 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 CancellationTokenSource? CatalogCancellation { get; set; }
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
{
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()
{
try
{
await JS.InvokeVoidAsync("lmeMap.search", SearchText);
ActiveTab = "map";
}
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.";
await JS.InvokeVoidAsync("lmeMap.search", $"{municipality.Name}, Italia");
}
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}";
await JS.InvokeVoidAsync("lmeMap.locateParcelByPoint", parcel.Latitude, parcel.Longitude, parcel.Number, label);
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);
}
}
private async Task ToggleGps()
{
if (GpsActive)
{
await StopGps();
}
else
{
await StartGps();
}
}
private async Task StartGps()
{
GpsActive = true;
try
{
await LocationService.StartTrackingAsync(
async location => await InvokeAsync(() => JS.InvokeVoidAsync("lmeMap.setNativeGpsPosition", location)),
async error => await InvokeAsync(() => JS.InvokeVoidAsync("lmeMap.setGpsError", error)));
}
catch (Exception error)
{
GpsActive = false;
await JS.InvokeVoidAsync("lmeMap.setGpsError", error.Message);
}
}
private async Task StopGps()
{
LocationService.StopTracking();
GpsActive = false;
await JS.InvokeVoidAsync("lmeMap.stopGps");
}
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 EnableDrawTools()
{
await JS.InvokeVoidAsync("lmeMap.enableDrawTools");
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 Task ToggleTrail(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.toggleTrail", (bool?)args.Value == true).AsTask();
private Task ChangeBaseLayer(ChangeEventArgs args) => JS.InvokeVoidAsync("lmeMap.setBaseLayer", args.Value?.ToString()).AsTask();
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();
return ValueTask.CompletedTask;
}
}