Files
LMEMappeCatastaliApp/Components/Pages/Home.razor
T

1527 lines
66 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
@inject NetworkLocationService NetworkLocationService
@inject AzureMapsService AzureMapsService
@inject ProjectStorageService ProjectStorageService
@inject MapExportService MapExportService
@implements IAsyncDisposable
<PageTitle>LME Mappe Catastali</PageTitle>
<div class="map-app">
<section class="map-stage @(ActiveTab == "search" ? "search-active" : null)">
<div id="map"></div>
<header class="map-toolbar">
<div class="app-identity">
<strong>LME Catasto</strong>
<span>@Status</span>
</div>
<div class="toolbar-actions">
<button class="toolbar-button layer-button @(LayerPanelOpen ? "is-active" : null)"
title="Layer catastali"
aria-expanded="@LayerPanelOpen"
@onclick="ToggleLayerPanel">
<span class="layer-symbol" aria-hidden="true"></span>
<span>Layer</span>
</button>
<button class="toolbar-button" @onclick='() => SelectTab(ActiveTab == "search" ? "map" : "search")'>Cerca</button>
</div>
</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>
@if (LayerPanelOpen)
{
<section class="map-layer-panel" aria-label="Layer catastali">
<header>
<strong>Layer catastali</strong>
<button class="layer-panel-close" aria-label="Chiudi layer" @onclick="ToggleLayerPanel">×</button>
</header>
@foreach (var layer in CadastralLayers)
{
<label class="map-layer-option">
<input type="checkbox" checked="@layer.Enabled" @onchange="(args) => ToggleCadastreLayer(layer, args)" />
<span>
<strong>@layer.Name</strong>
@if (!string.IsNullOrWhiteSpace(layer.Detail))
{
<small>@layer.Detail</small>
}
</span>
</label>
}
<label class="map-layer-option">
<input type="checkbox" @bind="ShowSelectedParcels" @bind:after="ApplySelectedParcelsVisibility" />
<span>
<strong>Particelle catastali evidenziate</strong>
<small>Contorni rossi ottenuti da ricerca o GPS</small>
</span>
</label>
<label class="map-layer-option">
<input type="checkbox" @bind="ShowImportedData" @bind:after="ApplyImportedDataVisibility" />
<span>
<strong>Dati importati</strong>
<small>Particelle e geometrie verdi di file e progetti</small>
</span>
</label>
<section class="layer-tree-section">
<header>
<strong>Ordine livelli e oggetti</strong>
<small>Trascina dall'alto verso il basso</small>
</header>
<div id="layerManagerTree" class="layer-manager-tree"></div>
</section>
<div class="layer-panel-actions">
<button class="danger-command" @onclick="ClearSelectedParcels">Elimina definitivamente le particelle selezionate</button>
<button class="danger-command" @onclick="ClearMapElements">Pulisci elementi mappa</button>
</div>
</section>
}
<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 == "data")
{
<section class="panel-content">
<section class="panel-section">
<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>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="IncludeKmlSheetLayer" @bind:after="ApplyKmlSheetLayerOption" />
<span>KML/KMZ: includi anche il layer del foglio</span>
</label>
<small class="field-help">
@(IncludeKmlSheetLayer
? "Attivato: importa foglio, particelle e altri elementi KML/KMZ."
: "Disattivato: importa solo i poligoni delle particelle riconosciute.")
</small>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedData" @bind:after="ApplyImportedDataVisibility" />
<span>Mostra particelle/dati importati in verde</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedSheetLayer" @bind:after="ApplyImportedSheetLayerVisibility" />
<span>Mostra cornice/layer del foglio importato</span>
</label>
<small class="field-help">La cornice grande resta separata dalle particelle e puo essere nascosta anche nei progetti salvati.</small>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedMeasurements" @bind:after="ApplyImportedMeasurementsVisibility" />
<span>Mostra misure dei file caricati</span>
</label>
<div class="button-row">
<button @onclick="FitImportedLayers">Inquadra dati</button>
<button class="secondary-command" @onclick="ClearImportedLayers">Rimuovi dati</button>
</div>
</section>
<section class="panel-section">
<h2>Disegni e misure</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowDrawingMeasurements" @bind:after="ApplyDrawingMeasurementsVisibility" />
<span>Mostra superficie, perimetro e lati</span>
</label>
<button class="secondary-command" aria-pressed="@DrawToolsVisible" @onclick="ToggleDrawTools">
@(DrawToolsVisible ? "Nascondi strumenti sulla mappa" : "Mostra strumenti sulla mappa")
</button>
<button class="danger-command" @onclick="ClearDrawings">Elimina disegni e POI</button>
</section>
<section class="panel-section">
<h2>Punti di interesse</h2>
<label>
Nome POI
<input @bind="PoiName" @bind:event="oninput" placeholder="Nome del punto" />
</label>
<button class="primary-command" @onclick="AddProjectMarker">Aggiungi POI al centro mappa</button>
</section>
</section>
}
else if (ActiveTab == "save")
{
<section class="panel-content">
<section class="panel-section">
<h2>Esporta contenuto mappa</h2>
<label>
Nome file
<input @bind="ExportFileName" @bind:event="oninput" placeholder="Es. Rilievo San Nicola" maxlength="80" />
</label>
<label>
Formato
<select @bind="ExportFormat">
<option value="geojson">GeoJSON</option>
<option value="kml">KML</option>
<option value="kmz">KMZ compresso</option>
</select>
</label>
<div class="export-options" role="group" aria-label="Oggetti da esportare">
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportImported" @bind:after="SaveFlagPreferences" />
<span>Particelle e dati GIS importati</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportImportedSheets" @bind:after="SaveFlagPreferences" />
<span>Cornici, fogli e riquadri KMZ</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportCadastral" @bind:after="SaveFlagPreferences" />
<span>Particelle catastali rosse e GPS</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportDrawings" @bind:after="SaveFlagPreferences" />
<span>Disegni, aree, linee e cerchi</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportPoi" @bind:after="SaveFlagPreferences" />
<span>Punti di interesse</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ExportTrail" @bind:after="SaveFlagPreferences" />
<span>Traccia GPS registrata</span>
</label>
</div>
<button class="primary-command" @onclick="ExportMapFile" disabled="@(ExportBusy || string.IsNullOrWhiteSpace(ExportFileName))">
@(ExportBusy ? "Preparazione file..." : "Esporta e salva file")
</button>
<small class="field-help">KML e KMZ sono compatibili con Google Earth. Gli sfondi satellitari e il WMS non vengono incorporati nel file vettoriale.</small>
</section>
</section>
}
else if (ActiveTab == "projects")
{
<section class="panel-content">
<section class="panel-section">
<h2>Salva progetto</h2>
<label>
Nome progetto
<input @bind="NewProjectName" @bind:event="oninput" placeholder="Es. Rilievo San Nicola" maxlength="80" />
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="IncludeCadastralParcels" @bind:after="SaveFlagPreferences" />
<span>Includi le particelle catastali rosse</span>
</label>
<button class="primary-command" @onclick="SaveProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(NewProjectName))">Salva progetto</button>
</section>
<section class="panel-section">
<h2>Apri progetto</h2>
<label>
Progetto salvato
<select value="@SelectedProjectId" @onchange="SelectProject" disabled="@(Projects.Count == 0 || ProjectBusy)">
<option value="">Seleziona un progetto</option>
@foreach (var project in Projects)
{
<option value="@project.Id">@project.Name - @project.UpdatedAt.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</option>
}
</select>
</label>
<div class="button-row">
<button @onclick="LoadProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Apri</button>
<button class="danger-command" @onclick="DeleteProject" disabled="@(ProjectBusy || string.IsNullOrWhiteSpace(SelectedProjectId))">Elimina</button>
</div>
<div class="project-archive-state">@ProjectArchiveMessage</div>
</section>
<section class="panel-section">
<h2>Visualizzazione progetto</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedData" @bind:after="ApplyImportedDataVisibility" />
<span>Mostra particelle/dati importati in verde</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedSheetLayer" @bind:after="ApplyImportedSheetLayerVisibility" />
<span>Mostra cornice/layer del foglio importato</span>
</label>
<small class="field-help">Il comando agisce subito sulla mappa e sui dati del progetto caricato.</small>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowImportedMeasurements" @bind:after="ApplyImportedMeasurementsVisibility" />
<span>Mostra misure dei file caricati</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowDrawingMeasurements" @bind:after="ApplyDrawingMeasurementsVisibility" />
<span>Mostra misure dei disegni</span>
</label>
</section>
</section>
}
else if (ActiveTab == "settings")
{
<section class="panel-content">
<section class="panel-section">
<h2>Mappa e sfondo</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="azure-road">Strade - Azure Maps</option>
<option value="azure-satellite">Satellite - Azure Maps</option>
<option value="google-satellite">Satellite - Google Maps</option>
<option value="google-roadmap">Strade - Google Maps</option>
</select>
</label>
<label>
Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="@CadastralOpacity.ToString(System.Globalization.CultureInfo.InvariantCulture)" @onchange="ChangeCadastreOpacity" />
</label>
</section>
<section class="panel-section">
<h2>Servizi mappa con chiave</h2>
<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>
Chiave sottoscrizione Azure Maps Gen2
<input type="password" @bind="AzureMapsSubscriptionKey" placeholder="Subscription key" autocomplete="off" />
</label>
<button class="secondary-command" @onclick="SaveAzureMapsKey">Salva chiave Azure Maps</button>
<div class="service-note">
<strong>Quota gratuita Azure Maps Gen2</strong>
<p>Include ogni mese 5.000 transazioni per le mappe stradali e 1.000 per le immagini satellitari. Ogni transazione comprende fino a 15 tessere.</p>
</div>
</section>
<section class="panel-section">
<h2>GPS e percorso</h2>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowGpsPosition" @bind:after="ApplyGpsPositionVisibility" />
<span>Mostra punto e cerchio GPS</span>
</label>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="TrailEnabled" @bind:after="ApplyTrailOption" />
<span>Registra la traccia mentre cammini</span>
</label>
</section>
<section class="panel-section">
<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">Mostra vista WMS corrente</button>
<footer>Dati catastali: Agenzia delle Entrate, licenza CC BY 4.0. App indipendente.</footer>
</section>
</section>
}
</aside>
}
<div id="mapInfo" class="map-info">Pronto</div>
<div id="wmsPreview" class="wms-preview" hidden>
<section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle">
<header>
<div>
<strong id="wmsPreviewTitle">Vista WMS catastale</strong>
<small>Immagine ufficiale AdE con le particelle evidenziate dall'app</small>
</div>
<button id="closeWmsPreview" class="close-button" aria-label="Chiudi vista WMS">×</button>
</header>
<div class="wms-preview-body">
<span id="wmsPreviewLoading">Caricamento...</span>
<div id="wmsPreviewMap" class="wms-preview-map" hidden>
<img id="wmsPreviewImage" alt="Vista WMS catastale dell'area corrente" />
<svg id="wmsPreviewOverlay" aria-hidden="true" preserveAspectRatio="none"></svg>
</div>
</div>
</section>
</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 == "data" ? "active" : null)" @onclick='() => SelectTab("data")'>Dati</button>
<button class="@(ActiveTab == "projects" ? "active" : null)" @onclick='() => SelectTab("projects")'>Progetti</button>
<button class="@(ActiveTab == "save" ? "active" : null)" @onclick='() => SelectTab("save")'>Salvataggio</button>
<button class="@(ActiveTab == "settings" ? "active" : null)" @onclick='() => SelectTab("settings")'>Impostazioni</button>
</nav>
</section>
</div>
@code {
private const string FlagPreferencePrefix = "lme_flag_";
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 ShowSelectedParcels { get; set; } = true;
private bool ShowImportedData { get; set; } = true;
private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; }
private bool LayerPanelOpen { get; set; }
private DotNetObjectReference<Home>? MapCallbackReference { 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<RegionOption> Regions { get; set; } = [];
private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = [];
private IReadOnlyList<string> Sheets { get; set; } = [];
private IReadOnlyList<ParcelOption> Parcels { get; set; } = [];
private IReadOnlyList<MapProjectSummary> 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<CadastralLayer> 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()
{
LoadFlagPreferences();
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)
{
MapCallbackReference = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("lmeMap.init", MapCallbackReference);
await ApplyFlagPreferencesToMapAsync();
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<string?>("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");
if (LayerPanelOpen)
{
await JS.InvokeVoidAsync("lmeMap.refreshLayerManager");
}
}
private async Task ToggleLayerPanel()
{
LayerPanelOpen = !LayerPanelOpen;
if (!LayerPanelOpen)
{
await JS.InvokeVoidAsync("lmeMap.clearLayerManagerSelection");
}
if (LayerPanelOpen)
{
ActiveTab = "map";
}
await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize");
if (LayerPanelOpen)
{
await JS.InvokeVoidAsync("lmeMap.refreshLayerManager");
}
}
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 ApplyGpsPositionVisibility()
{
SaveFlagPreferences();
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<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 async Task ClearImportedLayers()
{
ShowImportedSheetLayer = false;
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.clearImported");
}
private Task AddProjectMarker() => JS.InvokeVoidAsync("lmeMap.addProjectMarker", PoiName).AsTask();
private async Task ApplyKmlSheetLayerOption()
{
ShowImportedSheetLayer = IncludeKmlSheetLayer;
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer);
}
private Task ApplyImportedSheetLayerVisibility()
{
SaveFlagPreferences();
return JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer).AsTask();
}
private async Task ApplyImportedMeasurementsVisibility()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setImportedMeasurementsVisible", ShowImportedMeasurements);
}
private async Task ApplyDrawingMeasurementsVisibility()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements);
}
[JSInvokable]
public Task SetMeasurementsFromLayerManager(string category, bool enabled)
{
if (category == "imported")
{
ShowImportedMeasurements = enabled;
}
else if (category == "drawings")
{
ShowDrawingMeasurements = enabled;
}
SaveFlagPreferences();
return InvokeAsync(StateHasChanged);
}
private async Task ToggleDrawTools()
{
DrawToolsVisible = !DrawToolsVisible;
SaveFlagPreferences();
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<string>("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<bool>("confirm", $"Il progetto '{existing.Name}' esiste gia. Sovrascriverlo?"))
{
return;
}
ProjectBusy = true;
try
{
var snapshot = await JS.InvokeAsync<string>("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<ProjectDisplayOptions>("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;
ShowImportedData = options.ImportedVisible;
ShowImportedSheetLayer = options.ImportedSheetsVisible;
ShowImportedMeasurements = options.ShowImportedMeasurements;
ShowDrawingMeasurements = options.ShowDrawingMeasurements;
DrawToolsVisible = options.DrawToolsVisible;
ShowGpsPosition = options.GpsPositionVisible;
ShowSelectedParcels = options.SelectedParcelsVisible;
TrailEnabled = options.TrailEnabled;
var activeLayers = (options.CadastreLayers ?? []).ToHashSet(StringComparer.Ordinal);
foreach (var layer in CadastralLayers)
{
layer.Enabled = activeLayers.Contains(layer.Key);
}
SaveFlagPreferences();
}
private async Task DeleteProject()
{
var project = Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
if (project is null)
{
return;
}
if (!await JS.InvokeAsync<bool>("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 ApplyTrailOption()
{
SaveFlagPreferences();
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;
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer.Key, layer.Enabled);
}
private void LoadFlagPreferences()
{
IncludeCadastralParcels = GetFlagPreference("include_cadastral_parcels", true);
IncludeKmlSheetLayer = GetFlagPreference("include_kml_sheet_layer", false);
ShowImportedData = GetFlagPreference("show_imported_data", true);
ShowImportedSheetLayer = GetFlagPreference("show_imported_sheet_layer", false);
ShowImportedMeasurements = GetFlagPreference("show_imported_measurements", false);
ShowDrawingMeasurements = GetFlagPreference("show_drawing_measurements", true);
DrawToolsVisible = GetFlagPreference("draw_tools_visible", false);
ShowGpsPosition = GetFlagPreference("show_gps_position", true);
ShowSelectedParcels = GetFlagPreference("show_selected_parcels", true);
TrailEnabled = GetFlagPreference("trail_enabled", false);
ExportImported = GetFlagPreference("export_imported", true);
ExportImportedSheets = GetFlagPreference("export_imported_sheets", true);
ExportCadastral = GetFlagPreference("export_cadastral", true);
ExportDrawings = GetFlagPreference("export_drawings", true);
ExportPoi = GetFlagPreference("export_poi", true);
ExportTrail = GetFlagPreference("export_trail", true);
foreach (var layer in CadastralLayers)
{
layer.Enabled = GetFlagPreference($"cadastre_layer_{layer.Key}", layer.Enabled);
}
}
private void SaveFlagPreferences()
{
SetFlagPreference("include_cadastral_parcels", IncludeCadastralParcels);
SetFlagPreference("include_kml_sheet_layer", IncludeKmlSheetLayer);
SetFlagPreference("show_imported_data", ShowImportedData);
SetFlagPreference("show_imported_sheet_layer", ShowImportedSheetLayer);
SetFlagPreference("show_imported_measurements", ShowImportedMeasurements);
SetFlagPreference("show_drawing_measurements", ShowDrawingMeasurements);
SetFlagPreference("draw_tools_visible", DrawToolsVisible);
SetFlagPreference("show_gps_position", ShowGpsPosition);
SetFlagPreference("show_selected_parcels", ShowSelectedParcels);
SetFlagPreference("trail_enabled", TrailEnabled);
SetFlagPreference("export_imported", ExportImported);
SetFlagPreference("export_imported_sheets", ExportImportedSheets);
SetFlagPreference("export_cadastral", ExportCadastral);
SetFlagPreference("export_drawings", ExportDrawings);
SetFlagPreference("export_poi", ExportPoi);
SetFlagPreference("export_trail", ExportTrail);
foreach (var layer in CadastralLayers)
{
SetFlagPreference($"cadastre_layer_{layer.Key}", layer.Enabled);
}
}
private async Task ApplyFlagPreferencesToMapAsync()
{
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
await JS.InvokeVoidAsync("lmeMap.setImportedLayerVisible", ShowImportedData);
await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer);
await JS.InvokeVoidAsync("lmeMap.setImportedMeasurementsVisible", ShowImportedMeasurements);
await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements);
await JS.InvokeVoidAsync("lmeMap.setDrawToolsVisible", DrawToolsVisible);
await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition);
await JS.InvokeVoidAsync("lmeMap.setSelectedParcelsVisible", ShowSelectedParcels);
await JS.InvokeVoidAsync("lmeMap.toggleTrail", TrailEnabled);
foreach (var layer in CadastralLayers)
{
await JS.InvokeVoidAsync("lmeMap.toggleCadastreLayer", layer.Key, layer.Enabled);
}
}
private static bool GetFlagPreference(string name, bool defaultValue) =>
Preferences.Default.Get($"{FlagPreferencePrefix}{name}", defaultValue);
private static void SetFlagPreference(string name, bool value) =>
Preferences.Default.Set($"{FlagPreferencePrefix}{name}", value);
private async Task ApplySelectedParcelsVisibility()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setSelectedParcelsVisible", ShowSelectedParcels);
}
private async Task ApplyImportedDataVisibility()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setImportedLayerVisible", ShowImportedData);
}
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 ImportedVisible { get; init; } = true;
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 SelectedParcelsVisible { get; init; } = true;
public bool TrailEnabled { get; init; }
}
public ValueTask DisposeAsync()
{
CancelCatalogWork();
LocationService.StopTracking();
GpsLookupCancellation?.Cancel();
GpsLookupCancellation?.Dispose();
MapCallbackReference?.Dispose();
return ValueTask.CompletedTask;
}
}