Compare commits

...
12 changed files with 908 additions and 45 deletions

No files matched your search

+261 -4
View File
@@ -3,6 +3,7 @@
@inject PreciseLocationService LocationService @inject PreciseLocationService LocationService
@inject CadastralCatalogService CatalogService @inject CadastralCatalogService CatalogService
@inject OfficialCadastreService OfficialCadastreService @inject OfficialCadastreService OfficialCadastreService
@inject FiducialPointService FiducialPointService
@inject LocationSearchService LocationSearchService @inject LocationSearchService LocationSearchService
@inject NetworkLocationService NetworkLocationService @inject NetworkLocationService NetworkLocationService
@inject AzureMapsService AzureMapsService @inject AzureMapsService AzureMapsService
@@ -46,6 +47,7 @@
<button @onclick='() => SelectTab("map")'>@L("menu.map")</button> <button @onclick='() => SelectTab("map")'>@L("menu.map")</button>
<button @onclick='() => SelectTab("search")'>@L("menu.search")</button> <button @onclick='() => SelectTab("search")'>@L("menu.search")</button>
<button @onclick='() => SelectTab("cadastre")'>@L("menu.cadastre")</button> <button @onclick='() => SelectTab("cadastre")'>@L("menu.cadastre")</button>
<button @onclick='() => SelectTab("fiducials")'>Punti fiduciali</button>
<button @onclick='() => SelectTab("data")'>@L("menu.data")</button> <button @onclick='() => SelectTab("data")'>@L("menu.data")</button>
<button @onclick='() => SelectTab("projects")'>@L("menu.projects")</button> <button @onclick='() => SelectTab("projects")'>@L("menu.projects")</button>
<button @onclick='() => SelectTab("save")'>@L("menu.export")</button> <button @onclick='() => SelectTab("save")'>@L("menu.export")</button>
@@ -109,13 +111,18 @@
</section> </section>
} }
<div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null)"> <div class="gps-overlay @(ActiveTab == "search" ? "search-open" : null) @(GpsOverlayCollapsed ? "is-collapsed" : null)">
<div class="gps-state"> <div class="gps-state">
<span id="gpsState">GPS non attivo</span> <span id="gpsState">GPS non attivo</span>
<strong id="parcelState">Nessuna particella selezionata</strong> <strong id="parcelState">Nessuna particella selezionata</strong>
</div> </div>
<div class="gps-actions">
<button class="gps-main" @onclick="ToggleGps">@(GpsActive ? "Ferma GPS" : "Avvia GPS")</button> <button class="gps-main" @onclick="ToggleGps">@(GpsActive ? "Ferma GPS" : "Avvia GPS")</button>
<button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione">Centra</button> <button class="gps-center" @onclick="CenterOnGps" aria-label="Centra posizione GPS">Centra</button>
</div>
<button class="gps-collapse" @onclick="ToggleGpsOverlay" aria-label="@(GpsOverlayCollapsed ? "Espandi pannello GPS" : "Riduci pannello GPS")" title="@(GpsOverlayCollapsed ? "Espandi pannello GPS" : "Riduci pannello GPS")">
<span aria-hidden="true">@(GpsOverlayCollapsed ? "+" : "−")</span>
</button>
</div> </div>
@if (ActiveTab is not "map" and not "search") @if (ActiveTab is not "map" and not "search")
@@ -196,6 +203,84 @@
<MenuHelp Topic="cadastre" /> <MenuHelp Topic="cadastre" />
</section> </section>
} }
else if (ActiveTab == "fiducials")
{
<section class="panel-content">
@if (!CanUse(AppFeatures.FiducialPoints))
{
<div class="service-note protected-service">
<strong>Funzione LME Pro</strong>
<p>La ricerca dei Punti Fiduciali e delle monografie ufficiali è disponibile con abbonamento Pro o chiave manuale valida.</p>
<button class="primary-command" @onclick='() => SelectTab("settings")'>Apri abbonamento Pro</button>
</div>
}
else
{
<label>
Provincia
<select value="@SelectedFiducialProvinceCode" @onchange="OnFiducialProvinceChanged" disabled="@FiducialBusy">
<option value="">Seleziona provincia</option>
@foreach (var province in FiducialProvinces)
{
<option value="@province.Code">@province.Name</option>
}
</select>
</label>
<label>
Comune catastale
<select value="@SelectedFiducialMunicipalityCode" @onchange="OnFiducialMunicipalityChanged" disabled="@(FiducialMunicipalities.Count == 0 || FiducialBusy)">
<option value="">Seleziona comune</option>
@foreach (var municipality in FiducialMunicipalities)
{
<option value="@municipality.Code">@municipality.Name (@municipality.Code)</option>
}
</select>
</label>
<label>
Foglio
<select @bind="SelectedFiducialSheet" disabled="@(FiducialSheets.Count == 0 || FiducialBusy)">
<option value="">Seleziona foglio</option>
@foreach (var sheet in FiducialSheets)
{
<option value="@sheet.Code">@sheet.Name</option>
}
</select>
</label>
<button class="primary-command" @onclick="SearchFiducials" disabled="@(string.IsNullOrWhiteSpace(SelectedFiducialSheet) || FiducialBusy)">
@(FiducialBusy ? "Ricerca..." : "Cerca Punti Fiduciali")
</button>
@if (!string.IsNullOrWhiteSpace(FiducialMessage))
{
<div class="catalog-progress"><span>@FiducialMessage</span></div>
}
@if (FiducialPoints.Count > 0)
{
<section class="panel-section">
<h2>Punti trovati (@FiducialPoints.Count)</h2>
<div class="fiducial-list">
@foreach (var point in FiducialPoints)
{
<article class="fiducial-item">
<strong>@point.Identifier</strong>
<button class="secondary-command" @onclick="() => DownloadFiducialMonograph(point)">Scarica PDF monografia</button>
</article>
}
</div>
</section>
}
<div class="service-note">
<strong>Fonte ufficiale Agenzia delle Entrate</strong>
<p>L'elenco e i PDF arrivano dal servizio pubblico delle monografie. Il layer sulla mappa sarà abilitato soltanto quando sono disponibili coordinate geografiche trasformate e verificabili.</p>
</div>
}
</section>
}
else if (ActiveTab == "data") else if (ActiveTab == "data")
{ {
<section class="panel-content"> <section class="panel-content">
@@ -553,6 +638,11 @@
Trasparenza catasto Trasparenza catasto
<input type="range" min="0" max="1" step="0.05" value="@CadastralOpacity.ToString(System.Globalization.CultureInfo.InvariantCulture)" @onchange="ChangeCadastreOpacity" /> <input type="range" min="0" max="1" step="0.05" value="@CadastralOpacity.ToString(System.Globalization.CultureInfo.InvariantCulture)" @onchange="ChangeCadastreOpacity" />
</label> </label>
<label>
Durata messaggi mappa (secondi)
<input type="number" min="0" max="30" step="1" @bind="MapMessageTimeoutSeconds" @bind:after="ApplyMapMessageOptions" />
</label>
<small class="field-help">Imposta 0 per lasciare i messaggi visibili fino alla chiusura manuale.</small>
</section> </section>
<section class="panel-section"> <section class="panel-section">
@@ -689,7 +779,10 @@
</aside> </aside>
} }
<div id="mapInfo" class="map-info">Pronto</div> <div id="mapInfo" class="map-info" role="status" aria-live="polite">
<span id="mapInfoText" class="map-info-text">Pronto</span>
<button type="button" class="map-info-close" onclick="window.lmeMap.hideInfo()" aria-label="Chiudi messaggio" title="Chiudi messaggio">×</button>
</div>
<div id="wmsPreview" class="wms-preview" hidden> <div id="wmsPreview" class="wms-preview" hidden>
<section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle"> <section class="wms-preview-dialog" role="dialog" aria-modal="true" aria-labelledby="wmsPreviewTitle">
@@ -732,6 +825,11 @@
private string MonthlyPrice { get; set; } = string.Empty; private string MonthlyPrice { get; set; } = string.Empty;
private string YearlyPrice { get; set; } = string.Empty; private string YearlyPrice { get; set; } = string.Empty;
private bool BillingBusy { get; set; } private bool BillingBusy { get; set; }
private bool FiducialBusy { get; set; }
private string SelectedFiducialProvinceCode { get; set; } = string.Empty;
private string SelectedFiducialMunicipalityCode { get; set; } = string.Empty;
private string SelectedFiducialSheet { get; set; } = string.Empty;
private string FiducialMessage { get; set; } = string.Empty;
private string Status { get; set; } = "online"; private string Status { get; set; } = "online";
private string SelectedBaseLayer { get; set; } = "satellite"; private string SelectedBaseLayer { get; set; } = "satellite";
private string GoogleMapsApiKey { get; set; } = string.Empty; private string GoogleMapsApiKey { get; set; } = string.Empty;
@@ -743,8 +841,10 @@
private string CatalogMessage { get; set; } = string.Empty; private string CatalogMessage { get; set; } = string.Empty;
private double? CatalogFraction { get; set; } private double? CatalogFraction { get; set; }
private double CadastralOpacity { get; set; } = 0.75; private double CadastralOpacity { get; set; } = 0.75;
private int MapMessageTimeoutSeconds { get; set; } = 3;
private bool CatalogBusy { get; set; } private bool CatalogBusy { get; set; }
private bool GpsActive { get; set; } private bool GpsActive { get; set; }
private bool GpsOverlayCollapsed { get; set; }
private bool ShowGpsPosition { get; set; } = true; private bool ShowGpsPosition { get; set; } = true;
private bool ShowSelectedParcels { get; set; } = true; private bool ShowSelectedParcels { get; set; } = true;
private bool ShowImportedData { get; set; } = true; private bool ShowImportedData { get; set; } = true;
@@ -806,6 +906,10 @@
private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = []; private IReadOnlyList<MunicipalityOption> Municipalities { get; set; } = [];
private IReadOnlyList<string> Sheets { get; set; } = []; private IReadOnlyList<string> Sheets { get; set; } = [];
private IReadOnlyList<ParcelOption> Parcels { get; set; } = []; private IReadOnlyList<ParcelOption> Parcels { get; set; } = [];
private IReadOnlyList<FiducialAreaOption> FiducialProvinces { get; set; } = [];
private IReadOnlyList<FiducialAreaOption> FiducialMunicipalities { get; set; } = [];
private IReadOnlyList<FiducialAreaOption> FiducialSheets { get; set; } = [];
private IReadOnlyList<FiducialPointEntry> FiducialPoints { get; set; } = [];
private IReadOnlyList<MapProjectSummary> Projects { get; set; } = []; private IReadOnlyList<MapProjectSummary> Projects { get; set; } = [];
private IReadOnlyList<MapProjectVersion> ProjectVersions { get; set; } = []; private IReadOnlyList<MapProjectVersion> ProjectVersions { get; set; } = [];
private MapProjectSummary? SelectedProject => Projects.FirstOrDefault(item => item.Id == SelectedProjectId); private MapProjectSummary? SelectedProject => Projects.FirstOrDefault(item => item.Id == SelectedProjectId);
@@ -819,6 +923,7 @@
private string PanelTitle => ActiveTab switch private string PanelTitle => ActiveTab switch
{ {
"cadastre" => "Ricerca catastale", "cadastre" => "Ricerca catastale",
"fiducials" => "Punti fiduciali",
"data" => "Dati e disegni", "data" => "Dati e disegni",
"projects" => "Archivio progetti", "projects" => "Archivio progetti",
"save" => "Salvataggio ed esportazione", "save" => "Salvataggio ed esportazione",
@@ -830,6 +935,7 @@
private string PanelEyebrow => ActiveTab switch private string PanelEyebrow => ActiveTab switch
{ {
"cadastre" => "Regione, Comune, Foglio, Particella", "cadastre" => "Regione, Comune, Foglio, Particella",
"fiducials" => "Monografie ufficiali AdE",
"data" => "File GIS, misure e POI", "data" => "File GIS, misure e POI",
"projects" => "Salva, apri e visualizza i tuoi lavori", "projects" => "Salva, apri e visualizza i tuoi lavori",
"save" => "Esporta file GIS", "save" => "Esporta file GIS",
@@ -1041,6 +1147,10 @@
{ {
await RefreshProjectsAsync(); await RefreshProjectsAsync();
} }
if (tab == "fiducials" && CanUse(AppFeatures.FiducialPoints) && FiducialProvinces.Count == 0)
{
await LoadFiducialProvincesAsync();
}
await Task.Delay(30); await Task.Delay(30);
await JS.InvokeVoidAsync("lmeMap.resize"); await JS.InvokeVoidAsync("lmeMap.resize");
if (LayerPanelOpen) if (LayerPanelOpen)
@@ -1120,6 +1230,132 @@
: await CatalogService.GetMunicipalitiesAsync(SelectedRegionFile); : await CatalogService.GetMunicipalitiesAsync(SelectedRegionFile);
} }
private async Task LoadFiducialProvincesAsync()
{
FiducialBusy = true;
FiducialMessage = "Carico l'elenco delle province...";
try
{
FiducialProvinces = await FiducialPointService.GetProvincesAsync();
FiducialMessage = string.Empty;
}
catch (Exception error)
{
FiducialMessage = $"Servizio monografie non disponibile: {error.GetBaseException().Message}";
}
finally
{
FiducialBusy = false;
}
}
private async Task OnFiducialProvinceChanged(ChangeEventArgs args)
{
SelectedFiducialProvinceCode = args.Value?.ToString() ?? string.Empty;
SelectedFiducialMunicipalityCode = string.Empty;
SelectedFiducialSheet = string.Empty;
FiducialMunicipalities = [];
FiducialSheets = [];
FiducialPoints = [];
if (string.IsNullOrWhiteSpace(SelectedFiducialProvinceCode))
{
FiducialMessage = string.Empty;
return;
}
FiducialBusy = true;
FiducialMessage = "Carico i Comuni catastali...";
try
{
FiducialMunicipalities = await FiducialPointService.GetMunicipalitiesAsync(SelectedFiducialProvinceCode);
FiducialMessage = string.Empty;
}
catch (Exception error)
{
FiducialMessage = $"Comuni non disponibili: {error.GetBaseException().Message}";
}
finally
{
FiducialBusy = false;
}
}
private async Task OnFiducialMunicipalityChanged(ChangeEventArgs args)
{
SelectedFiducialMunicipalityCode = args.Value?.ToString() ?? string.Empty;
SelectedFiducialSheet = string.Empty;
FiducialSheets = [];
FiducialPoints = [];
if (string.IsNullOrWhiteSpace(SelectedFiducialMunicipalityCode))
{
FiducialMessage = string.Empty;
return;
}
FiducialBusy = true;
FiducialMessage = "Carico i fogli disponibili...";
try
{
FiducialSheets = await FiducialPointService.GetSheetsAsync(
SelectedFiducialProvinceCode,
SelectedFiducialMunicipalityCode);
FiducialMessage = string.Empty;
}
catch (Exception error)
{
FiducialMessage = $"Fogli non disponibili: {error.GetBaseException().Message}";
}
finally
{
FiducialBusy = false;
}
}
private async Task SearchFiducials()
{
var province = FiducialProvinces.FirstOrDefault(item => item.Code == SelectedFiducialProvinceCode);
var municipality = FiducialMunicipalities.FirstOrDefault(item => item.Code == SelectedFiducialMunicipalityCode);
if (province is null || municipality is null || string.IsNullOrWhiteSpace(SelectedFiducialSheet))
{
return;
}
FiducialBusy = true;
FiducialPoints = [];
FiducialMessage = "Interrogo il servizio ufficiale delle monografie...";
try
{
FiducialPoints = await FiducialPointService.GetFiducialsAsync(
province.Name,
municipality.Name,
municipality.Code,
SelectedFiducialSheet);
FiducialMessage = FiducialPoints.Count == 0
? "Nessun Punto Fiduciale pubblicato per il foglio selezionato."
: $"Trovati {FiducialPoints.Count} Punti Fiduciali.";
}
catch (Exception error)
{
FiducialMessage = $"Ricerca PF non riuscita: {error.GetBaseException().Message}";
}
finally
{
FiducialBusy = false;
}
}
private async Task DownloadFiducialMonograph(FiducialPointEntry point)
{
try
{
await Launcher.Default.OpenAsync(new Uri(point.MonographUrl));
}
catch (Exception error)
{
FiducialMessage = $"Impossibile aprire la monografia: {error.GetBaseException().Message}";
}
}
private async Task OnMunicipalityChanged(ChangeEventArgs args) private async Task OnMunicipalityChanged(ChangeEventArgs args)
{ {
SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty; SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty;
@@ -1305,7 +1541,7 @@
Interlocked.Exchange(ref LastGpsLookupAt, now); Interlocked.Exchange(ref LastGpsLookupAt, now);
try try
{ {
var radius = Math.Clamp(location.Accuracy ?? 5, 2, 25); var radius = Math.Clamp(location.Accuracy ?? 12, 12, 30);
var xml = await OfficialCadastreService.GetParcelsNearAsync( var xml = await OfficialCadastreService.GetParcelsNearAsync(
location.Latitude, location.Latitude,
location.Longitude, location.Longitude,
@@ -2094,6 +2330,12 @@
private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask(); private Task CenterOnGps() => JS.InvokeVoidAsync("lmeMap.centerOnGps").AsTask();
private void ToggleGpsOverlay()
{
GpsOverlayCollapsed = !GpsOverlayCollapsed;
SaveFlagPreferences();
}
private string L(string key) => L10n.T(key); private string L(string key) => L10n.T(key);
private bool CanUse(string feature) => Entitlements.CanUse(feature); private bool CanUse(string feature) => Entitlements.CanUse(feature);
@@ -2356,6 +2598,11 @@
OfflineExtraZoomLevels = Preferences.Default.Get($"{FlagPreferencePrefix}offline_zoom_levels", 2); OfflineExtraZoomLevels = Preferences.Default.Get($"{FlagPreferencePrefix}offline_zoom_levels", 2);
GpsProximityEnabled = GetFlagPreference("gps_proximity", true); GpsProximityEnabled = GetFlagPreference("gps_proximity", true);
GpsBoundaryAlertEnabled = GetFlagPreference("gps_boundary_alert", false); GpsBoundaryAlertEnabled = GetFlagPreference("gps_boundary_alert", false);
GpsOverlayCollapsed = GetFlagPreference("gps_overlay_collapsed", false);
MapMessageTimeoutSeconds = Math.Clamp(
Preferences.Default.Get($"{FlagPreferencePrefix}map_message_timeout", 3),
0,
30);
foreach (var layer in CadastralLayers) foreach (var layer in CadastralLayers)
{ {
@@ -2391,6 +2638,8 @@
Preferences.Default.Set($"{FlagPreferencePrefix}offline_zoom_levels", OfflineExtraZoomLevels); Preferences.Default.Set($"{FlagPreferencePrefix}offline_zoom_levels", OfflineExtraZoomLevels);
SetFlagPreference("gps_proximity", GpsProximityEnabled); SetFlagPreference("gps_proximity", GpsProximityEnabled);
SetFlagPreference("gps_boundary_alert", GpsBoundaryAlertEnabled); SetFlagPreference("gps_boundary_alert", GpsBoundaryAlertEnabled);
SetFlagPreference("gps_overlay_collapsed", GpsOverlayCollapsed);
Preferences.Default.Set($"{FlagPreferencePrefix}map_message_timeout", MapMessageTimeoutSeconds);
foreach (var layer in CadastralLayers) foreach (var layer in CadastralLayers)
{ {
@@ -2400,6 +2649,7 @@
private async Task ApplyFlagPreferencesToMapAsync() private async Task ApplyFlagPreferencesToMapAsync()
{ {
await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds);
await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer); await JS.InvokeVoidAsync("lmeMap.setIncludeKmlSheetLayer", IncludeKmlSheetLayer);
await JS.InvokeVoidAsync("lmeMap.setImportedLayerVisible", ShowImportedData); await JS.InvokeVoidAsync("lmeMap.setImportedLayerVisible", ShowImportedData);
await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer); await JS.InvokeVoidAsync("lmeMap.setImportedSheetLayerVisible", ShowImportedSheetLayer);
@@ -2416,6 +2666,13 @@
} }
} }
private async Task ApplyMapMessageOptions()
{
MapMessageTimeoutSeconds = Math.Clamp(MapMessageTimeoutSeconds, 0, 30);
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds);
}
private async Task ApplyAdvancedOptions() private async Task ApplyAdvancedOptions()
{ {
SaveFlagPreferences(); SaveFlagPreferences();
+2 -2
View File
@@ -28,8 +28,8 @@
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId> <ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0.6</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.12</ApplicationDisplayVersion>
<ApplicationVersion>106</ApplicationVersion> <ApplicationVersion>112</ApplicationVersion>
<Version>$(ApplicationDisplayVersion)</Version> <Version>$(ApplicationDisplayVersion)</Version>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
+1
View File
@@ -19,6 +19,7 @@ public static class MauiProgram
builder.Services.AddSingleton<PreciseLocationService>(); builder.Services.AddSingleton<PreciseLocationService>();
builder.Services.AddSingleton<CadastralCatalogService>(); builder.Services.AddSingleton<CadastralCatalogService>();
builder.Services.AddSingleton<OfficialCadastreService>(); builder.Services.AddSingleton<OfficialCadastreService>();
builder.Services.AddSingleton<FiducialPointService>();
builder.Services.AddSingleton<LocationSearchService>(); builder.Services.AddSingleton<LocationSearchService>();
builder.Services.AddSingleton<NetworkLocationService>(); builder.Services.AddSingleton<NetworkLocationService>();
builder.Services.AddSingleton<AzureMapsService>(); builder.Services.AddSingleton<AzureMapsService>();
+1 -1
View File
@@ -4,7 +4,7 @@
- Nome: LME Mappe Catastali Pro. - Nome: LME Mappe Catastali Pro.
- Package name Android: `it.liviomerola.lmemappecatastali.pro`. - Package name Android: `it.liviomerola.lmemappecatastali.pro`.
- Versione corrente da pubblicare: `1.0.6` / version code `106`. - Versione corrente da pubblicare: `1.0.11` / version code `111`.
- Formato Release Android: Android App Bundle (`.aab`). - Formato Release Android: Android App Bundle (`.aab`).
- Target SDK Release: Android 16 / API 36. - Target SDK Release: Android 16 / API 36.
+36 -8
View File
@@ -9,6 +9,7 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
private readonly SemaphoreSlim _connectionLock = new(1, 1); private readonly SemaphoreSlim _connectionLock = new(1, 1);
private readonly BillingClient _client; private readonly BillingClient _client;
private TaskCompletionSource<PlayBillingResult>? _purchaseCompletion; private TaskCompletionSource<PlayBillingResult>? _purchaseCompletion;
private string? _lastCatalogFailure;
public AndroidPlayBillingService() public AndroidPlayBillingService()
{ {
@@ -31,7 +32,12 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
{ {
var details = await GetProductDetailsAsync(cancellationToken); var details = await GetProductDetailsAsync(cancellationToken);
return details?.GetSubscriptionOfferDetails()? return details?.GetSubscriptionOfferDetails()?
.Where(item => string.IsNullOrEmpty(item.OfferId)) .Where(item => !string.IsNullOrWhiteSpace(item.BasePlanId)
&& !string.IsNullOrWhiteSpace(item.OfferToken))
.GroupBy(item => item.BasePlanId, StringComparer.Ordinal)
.Select(group => group
.OrderBy(item => string.IsNullOrEmpty(item.OfferId) ? 0 : 1)
.First())
.Select(item => .Select(item =>
{ {
var phase = item.PricingPhases.PricingPhaseList.LastOrDefault(); var phase = item.PricingPhases.PricingPhaseList.LastOrDefault();
@@ -99,13 +105,25 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
var details = await GetProductDetailsAsync(cancellationToken); var details = await GetProductDetailsAsync(cancellationToken);
var offer = details?.GetSubscriptionOfferDetails()? var offer = details?.GetSubscriptionOfferDetails()?
.FirstOrDefault(item => item.BasePlanId == basePlanId .Where(item => item.BasePlanId == basePlanId
&& string.IsNullOrEmpty(item.OfferId) && !string.IsNullOrWhiteSpace(item.OfferToken))
&& !string.IsNullOrWhiteSpace(item.OfferToken)); .OrderBy(item => string.IsNullOrEmpty(item.OfferId) ? 0 : 1)
if (details is null || offer is null) .FirstOrDefault();
if (details is null)
{ {
return PlayBillingResult.Failed( return PlayBillingResult.Failed(_lastCatalogFailure
$"Piano {basePlanId} non disponibile su Google Play. Verifica che l'abbonamento lme_pro e il piano siano attivi per il tuo Paese."); ?? "Google Play non ha restituito l'abbonamento lme_pro.");
}
if (offer is null)
{
var availablePlans = string.Join(", ", details.GetSubscriptionOfferDetails()?
.Select(item => item.BasePlanId)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.Ordinal) ?? []);
return PlayBillingResult.Failed(string.IsNullOrWhiteSpace(availablePlans)
? $"Google Play ha restituito lme_pro senza offerte acquistabili per il piano {basePlanId}."
: $"Piano {basePlanId} non restituito da Google Play. Piani ricevuti: {availablePlans}.");
} }
var productParams = BillingFlowParams.ProductDetailsParams.NewBuilder() var productParams = BillingFlowParams.ProductDetailsParams.NewBuilder()
@@ -191,7 +209,17 @@ public sealed class AndroidPlayBillingService : Java.Lang.Object, IPlayBillingSe
throw new InvalidOperationException(ToMessage(result.Result)); throw new InvalidOperationException(ToMessage(result.Result));
} }
return result.ProductDetailsList?.FirstOrDefault(); // The .NET async wrapper copies the Java result into the managed
// ProductDetails property; ProductDetailsList has no Java handle here.
var details = result.ProductDetails?.FirstOrDefault();
if (details is not null)
{
_lastCatalogFailure = null;
return details;
}
_lastCatalogFailure = "Google Play ha restituito un catalogo vuoto per lme_pro. Verifica account Play Store, adesione al test e origine dell'installazione.";
return null;
} }
private async Task EnsureConnectedAsync(CancellationToken cancellationToken) private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
+5 -1
View File
@@ -102,7 +102,11 @@ public class MainActivity : MauiAppCompatActivity
{ {
if (view is null || insets is null) return insets; if (view is null || insets is null) return insets;
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars()); var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
view.SetPadding(_left, _top, _right, systemBars?.Bottom ?? 0); view.SetPadding(
_left,
systemBars?.Top ?? _top,
_right,
systemBars?.Bottom ?? 0);
return insets; return insets;
} }
} }
+26
View File
@@ -4,6 +4,32 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md). Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md).
## Versione Pro 1.0.11 test
- Il flag Fogli catastali mostra il WMS di tutti i fogli agli zoom panoramici e, in dettaglio, il confine ufficiale WFS del foglio corrente anche senza dati caricati.
- Se il foglio corrente e gia presente in un progetto o in un'importazione, l'overlay di navigazione non ne crea un duplicato.
- La toolbar superiore rispetta l'area occupata dalla barra di stato e dalle notifiche Android.
## Versione Pro 1.0.10 test
- Migliora la compatibilita del servizio catastale AdE sui dispositivi Android meno recenti con HTTP/1.1, tentativi automatici e una ricerca GPS piu tollerante.
- I messaggi sulla mappa si chiudono automaticamente dopo un intervallo configurabile, predefinito a 3 secondi, e includono un pulsante di chiusura.
- La barra GPS mantiene sempre disponibili i comandi Avvia/Ferma e Centra, impilati sugli schermi stretti, e puo essere ridotta.
## Versione Pro 1.0.9 test
- Corregge la lettura del catalogo dal wrapper asincrono .NET di Google Play Billing, usando la raccolta managed `ProductDetails` popolata dal binding.
## Versione Pro 1.0.8 test
- Google Play Billing accetta tutte le offerte idonee restituite per i piani `monthly` e `yearly`, preferendo il piano base quando disponibile.
- In caso di catalogo vuoto o prodotto non caricato, il messaggio mostra una diagnostica specifica restituita da Google Play.
## Versione Pro 1.0.7 test
- I messaggi informativi su telefono vengono mostrati sopra i controlli zoom e possono occupare fino a tre righe senza sovrapporsi.
- Durante il rilevamento GPS la mappa si ricentra automaticamente quando la nuova posizione esce dall'area visibile, mantenendo lo zoom corrente.
## Versione Pro 1.0.6 test ## Versione Pro 1.0.6 test
- Il pannello Layer usa tutta la larghezza disponibile sui telefoni e le righe si adattano ai testi su piu linee senza sovrapporsi. - Il pannello Layer usa tutta la larghezza disponibile sui telefoni e le righe si adattano ai testi su piu linee senza sovrapporsi.
+3 -1
View File
@@ -21,7 +21,8 @@ public sealed class EntitlementService(
AppFeatures.CloudSync, AppFeatures.CloudSync,
AppFeatures.OfflineMaps, AppFeatures.OfflineMaps,
AppFeatures.AdvancedGeometry, AppFeatures.AdvancedGeometry,
AppFeatures.GpsTrail AppFeatures.GpsTrail,
AppFeatures.FiducialPoints
}; };
public bool IsGoogleProActive { get; private set; } public bool IsGoogleProActive { get; private set; }
@@ -185,4 +186,5 @@ public static class AppFeatures
public const string OfflineMaps = "offline-maps"; public const string OfflineMaps = "offline-maps";
public const string AdvancedGeometry = "advanced-geometry"; public const string AdvancedGeometry = "advanced-geometry";
public const string GpsTrail = "gps-trail"; public const string GpsTrail = "gps-trail";
public const string FiducialPoints = "fiducial-points";
} }
+148
View File
@@ -0,0 +1,148 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
namespace LMEMappeCatastaliApp.Services;
public sealed partial class FiducialPointService
{
private const string ServiceBaseUrl = "https://www1.agenziaentrate.gov.it/servizi/Monografie/";
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) };
public FiducialPointService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)");
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetProvincesAsync(
CancellationToken cancellationToken = default)
{
var html = await GetHtmlAsync("ricerca.php", cancellationToken);
return ParseOptions(html, "provincia");
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetMunicipalitiesAsync(
string provinceCode,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(provinceCode);
var html = await GetHtmlAsync($"ricerca.php?level=1&prov={Uri.EscapeDataString(code)}", cancellationToken);
return ParseOptions(html, "co");
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetSheetsAsync(
string provinceCode,
string municipalityCode,
CancellationToken cancellationToken = default)
{
var province = NormalizeCode(provinceCode);
var municipality = NormalizeCode(municipalityCode);
var path = $"ricerca.php?level=2&prov={Uri.EscapeDataString(province)}";
var html = await PostFormAsync(path, new Dictionary<string, string> { ["co"] = municipality }, cancellationToken);
return ParseOptions(html, "foglio");
}
public async Task<IReadOnlyList<FiducialPointEntry>> GetFiducialsAsync(
string provinceName,
string municipalityName,
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(municipalityCode);
var normalizedSheet = sheet.Trim();
var query = string.Join("&",
[
$"provincia={Uri.EscapeDataString(provinceName.Trim())}",
$"comune={Uri.EscapeDataString(municipalityName.Trim())}",
$"co={Uri.EscapeDataString(code)}"
]);
var html = await PostFormAsync(
$"risultato.php?{query}",
new Dictionary<string, string> { ["foglio"] = normalizedSheet },
cancellationToken);
return DownloadLinkRegex().Matches(html)
.Select(match =>
{
var relativeUrl = WebUtility.HtmlDecode(match.Groups["url"].Value);
var fileName = WebUtility.HtmlDecode(match.Groups["name"].Value);
return new FiducialPointEntry(
FormatIdentifier(fileName),
new Uri(new Uri(ServiceBaseUrl), relativeUrl).AbsoluteUri);
})
.DistinctBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private async Task<string> GetHtmlAsync(string relativeUrl, CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(ServiceBaseUrl + relativeUrl, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
private async Task<string> PostFormAsync(
string relativeUrl,
IReadOnlyDictionary<string, string> values,
CancellationToken cancellationToken)
{
using var content = new FormUrlEncodedContent(values);
using var response = await _httpClient.PostAsync(ServiceBaseUrl + relativeUrl, content, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
private static IReadOnlyList<FiducialAreaOption> ParseOptions(string html, string selectId)
{
var select = SelectRegex().Matches(html)
.LastOrDefault(match => string.Equals(match.Groups["id"].Value, selectId, StringComparison.OrdinalIgnoreCase));
if (select is null)
{
return [];
}
return OptionRegex().Matches(select.Groups["body"].Value)
.Select(match => new FiducialAreaOption(
WebUtility.HtmlDecode(match.Groups["value"].Value).Trim(),
WebUtility.HtmlDecode(StripTagsRegex().Replace(match.Groups["label"].Value, string.Empty)).Trim()))
.Where(item => !string.IsNullOrWhiteSpace(item.Code) && !string.IsNullOrWhiteSpace(item.Name))
.DistinctBy(item => item.Code, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string NormalizeCode(string value)
{
var normalized = value.Trim().ToUpperInvariant();
if (normalized.Length is < 2 or > 4 || normalized.Any(character => !char.IsLetterOrDigit(character)))
{
throw new ArgumentException("Codice territoriale non valido.", nameof(value));
}
return normalized;
}
private static string FormatIdentifier(string fileName)
{
var parts = fileName.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length == 3 ? $"PF{parts[2]}/{parts[1]}/{parts[0]}" : fileName;
}
[GeneratedRegex("<select\\b[^>]*(?:id|name)=[\\\"'](?<id>[^\\\"']+)[\\\"'][^>]*>(?<body>[\\s\\S]*?)</select>", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex SelectRegex();
[GeneratedRegex("<option\\b[^>]*value=[\\\"'](?<value>[^\\\"']*)[\\\"'][^>]*>(?<label>[\\s\\S]*?)</option>", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex OptionRegex();
[GeneratedRegex("href=[\\\"'](?<url>download\\.php\\?[^\\\"']*namefile=(?<name>[A-Z0-9-]+))[^\\\"']*[\\\"']", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex DownloadLinkRegex();
[GeneratedRegex("<[^>]+>", RegexOptions.None, 2000)]
private static partial Regex StripTagsRegex();
}
public sealed record FiducialAreaOption(string Code, string Name);
public sealed record FiducialPointEntry(string Identifier, string MonographUrl);
+77 -8
View File
@@ -1,4 +1,6 @@
using System.Globalization; using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Xml.Linq; using System.Xml.Linq;
namespace LMEMappeCatastaliApp.Services; namespace LMEMappeCatastaliApp.Services;
@@ -13,8 +15,9 @@ public sealed class OfficialCadastreService
public OfficialCadastreService() public OfficialCadastreService()
{ {
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/0.2"); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)");
_httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/gml+xml"); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/gml+xml"));
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
} }
public async Task<string> GetParcelsNearAsync( public async Task<string> GetParcelsNearAsync(
@@ -22,14 +25,30 @@ public sealed class OfficialCadastreService
double longitude, double longitude,
double radiusMeters, double radiusMeters,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
=> await GetFeaturesNearAsync( {
var firstRadius = Math.Clamp(radiusMeters, 12, 30);
var xml = await GetFeaturesNearAsync(
latitude, latitude,
longitude, longitude,
radiusMeters, firstRadius,
"CP:CadastralParcel", "CP:CadastralParcel",
100, 100,
cancellationToken); cancellationToken);
if (firstRadius >= 30 || ContainsFeatures(xml))
{
return xml;
}
return await GetFeaturesNearAsync(
latitude,
longitude,
30,
"CP:CadastralParcel",
100,
cancellationToken);
}
public async Task<string> GetSheetsNearAsync( public async Task<string> GetSheetsNearAsync(
double latitude, double latitude,
double longitude, double longitude,
@@ -78,17 +97,67 @@ public sealed class OfficialCadastreService
var url = WfsEndpoint + "?" + string.Join("&", query.Select(item => var url = WfsEndpoint + "?" + string.Join("&", query.Select(item =>
$"{Uri.EscapeDataString(item.Key)}={Uri.EscapeDataString(item.Value)}")); $"{Uri.EscapeDataString(item.Key)}={Uri.EscapeDataString(item.Value)}"));
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseContentRead, cancellationToken); for (var attempt = 0; attempt < 3; attempt++)
if (!response.IsSuccessStatusCode)
{ {
throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}."); try
} {
using var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Version = HttpVersion.Version11,
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true };
using var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseContentRead,
cancellationToken);
if (response.IsSuccessStatusCode)
{
var xml = await response.Content.ReadAsStringAsync(cancellationToken); var xml = await response.Content.ReadAsStringAsync(cancellationToken);
ThrowIfExceptionReport(xml); ThrowIfExceptionReport(xml);
return xml; return xml;
} }
if (attempt < 2 && IsTransient(response.StatusCode))
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
continue;
}
throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}.");
}
catch (HttpRequestException) when (attempt < 2)
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
}
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested && attempt < 2)
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken);
}
}
throw new HttpRequestException("Il servizio WFS AdE non ha risposto dopo tre tentativi.");
}
private static bool IsTransient(HttpStatusCode statusCode) =>
statusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests
|| (int)statusCode >= 500;
private static bool ContainsFeatures(string xml)
{
try
{
var document = XDocument.Parse(xml);
return document.Descendants().Any(element =>
element.Name.LocalName is "member" or "featureMember");
}
catch (System.Xml.XmlException)
{
return false;
}
}
private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture); private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture);
private static void ThrowIfExceptionReport(string xml) private static void ThrowIfExceptionReport(string xml)
+128 -9
View File
@@ -1245,6 +1245,41 @@ input[type="checkbox"] {
transition: top 140ms ease; transition: top 140ms ease;
} }
.gps-overlay.is-collapsed {
grid-template-columns: auto;
right: auto;
width: 48px;
max-width: 48px;
padding: 5px;
}
.gps-overlay.is-collapsed .gps-state,
.gps-overlay.is-collapsed .gps-actions {
display: none;
}
.fiducial-list {
display: grid;
gap: 8px;
}
.fiducial-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #d9e0e3;
}
.fiducial-item strong {
overflow-wrap: anywhere;
}
.fiducial-item .secondary-command {
margin: 0;
}
.gps-overlay.search-open { .gps-overlay.search-open {
top: 140px; top: 140px;
} }
@@ -1267,8 +1302,14 @@ input[type="checkbox"] {
color: #a33227; color: #a33227;
} }
.gps-actions {
display: grid;
gap: 5px;
}
.gps-main, .gps-main,
.gps-center { .gps-center,
.gps-collapse {
min-height: 38px; min-height: 38px;
padding: 0 10px; padding: 0 10px;
white-space: nowrap; white-space: nowrap;
@@ -1279,6 +1320,15 @@ input[type="checkbox"] {
color: var(--ink); color: var(--ink);
} }
.gps-collapse {
width: 38px;
padding: 0;
background: #e7ecef;
color: var(--ink);
font-size: 22px;
line-height: 1;
}
.leaflet-top.leaflet-right { .leaflet-top.leaflet-right {
top: 68px; top: 68px;
right: 2px; right: 2px;
@@ -1293,6 +1343,21 @@ input[type="checkbox"] {
height: 38px; height: 38px;
} }
.cadastral-sheet-label {
width: auto !important;
height: auto !important;
border: 1px solid #9a5b00;
border-radius: 4px;
padding: 3px 6px;
background: rgba(255, 251, 235, 0.94);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28);
color: #713f12;
font-size: 11px;
font-weight: 800;
line-height: 1.2;
white-space: nowrap;
}
.lme-drawing-scope-actions a[aria-disabled="true"] { .lme-drawing-scope-actions a[aria-disabled="true"] {
opacity: 0.45; opacity: 0.45;
pointer-events: none; pointer-events: none;
@@ -1566,11 +1631,36 @@ label {
z-index: 820; z-index: 820;
max-width: min(620px, calc(100% - 460px)); max-width: min(620px, calc(100% - 460px));
border-radius: 8px; border-radius: 8px;
padding: 9px 11px; display: flex;
align-items: flex-start;
gap: 8px;
padding: 7px 7px 7px 11px;
background: rgba(22, 31, 38, 0.88); background: rgba(22, 31, 38, 0.88);
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: auto;
}
.map-info[hidden] {
display: none;
}
.map-info-text {
min-width: 0;
padding: 2px 0;
overflow-wrap: anywhere;
}
.map-info-close {
flex: 0 0 28px;
width: 28px;
min-height: 28px;
padding: 0;
border: 0;
background: transparent;
color: #fff;
font-size: 20px;
line-height: 1;
} }
.wms-preview[hidden] { .wms-preview[hidden] {
@@ -1816,13 +1906,19 @@ footer {
top: 136px; top: 136px;
} }
.gps-overlay.is-collapsed {
right: auto;
width: 48px;
max-width: 48px;
}
.leaflet-top.leaflet-right { .leaflet-top.leaflet-right {
top: 122px; top: 174px;
right: 0; right: 0;
} }
.map-stage.search-active .leaflet-top.leaflet-right { .map-stage.search-active .leaflet-top.leaflet-right {
top: 190px; top: 242px;
} }
.gps-main, .gps-main,
@@ -1833,12 +1929,19 @@ footer {
.map-info { .map-info {
right: 8px; right: 8px;
bottom: 72px; bottom: calc(var(--nav-height) + 116px);
left: 8px; left: 8px;
max-width: none; max-width: none;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; line-height: 1.35;
white-space: nowrap; white-space: normal;
}
.map-info-text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
} }
.bottom-tabs button { .bottom-tabs button {
@@ -1860,8 +1963,24 @@ footer {
min-width: 0; min-width: 0;
} }
.gps-overlay {
grid-template-columns: minmax(0, 1fr) auto auto;
}
.gps-overlay.is-collapsed {
grid-template-columns: auto;
right: auto;
width: 48px;
max-width: 48px;
}
.fiducial-item {
grid-template-columns: minmax(0, 1fr);
}
.gps-main,
.gps-center { .gps-center {
display: none; min-width: 86px;
} }
.bottom-tabs button { .bottom-tabs button {
+220 -11
View File
@@ -14,8 +14,14 @@ const lmeState = {
"cadastre" "cadastre"
], ],
cadastre: null, cadastre: null,
cadastreZoning: null,
cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"], cadastreLayers: ["CP.CadastralParcel", "codice_plla", "CP.CadastralZoning", "fabbricati"],
cadastreOpacity: 0.75, cadastreOpacity: 0.75,
navigationSheetLayer: null,
navigationSheetFeature: null,
navigationSheetLabel: null,
navigationSheetRefreshTimer: null,
navigationSheetRequestId: 0,
imported: null, imported: null,
importedVisible: true, importedVisible: true,
importedSheets: null, importedSheets: null,
@@ -50,6 +56,8 @@ const lmeState = {
gpsTrailStartedAt: null, gpsTrailStartedAt: null,
gpsWatchId: null, gpsWatchId: null,
lastPosition: null, lastPosition: null,
infoTimeoutSeconds: 3,
infoTimer: null,
fileImportBound: false, fileImportBound: false,
resizeBound: false, resizeBound: false,
googleMapsLoading: null, googleMapsLoading: null,
@@ -185,7 +193,9 @@ window.lmeMap = {
setLocalization, setLocalization,
setEntitlements, setEntitlements,
importExternalGisFile, importExternalGisFile,
showInfo: setInfo showInfo: setInfo,
hideInfo,
setInfoTimeout
}; };
function setLocalization(options = {}) { function setLocalization(options = {}) {
@@ -301,6 +311,13 @@ function init(dotNetReference) {
lmeState.drawingMeasurements = L.layerGroup().addTo(lmeState.map); lmeState.drawingMeasurements = L.layerGroup().addTo(lmeState.map);
lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map); lmeState.selectedParcelsLayer = L.featureGroup().addTo(lmeState.map);
lmeState.gpsParcelLayer = L.featureGroup().addTo(lmeState.map); lmeState.gpsParcelLayer = L.featureGroup().addTo(lmeState.map);
const navigationSheetPane = lmeState.map.createPane("navigationSheetPane");
navigationSheetPane.style.zIndex = "465";
navigationSheetPane.style.pointerEvents = "none";
const cadastreZoningPane = lmeState.map.createPane("cadastreZoningPane");
cadastreZoningPane.style.zIndex = "455";
cadastreZoningPane.style.pointerEvents = "none";
lmeState.navigationSheetLayer = L.layerGroup().addTo(lmeState.map);
lmeState.gpsTrail = L.polyline([], { lmeState.gpsTrail = L.polyline([], {
pane: "gpsTrailPane", pane: "gpsTrailPane",
color: "#f97316", color: "#f97316",
@@ -335,6 +352,7 @@ function init(dotNetReference) {
lmeState.map.on("moveend", () => { lmeState.map.on("moveend", () => {
void updateAzureAttribution(); void updateAzureAttribution();
scheduleMeasurementRefresh(); scheduleMeasurementRefresh();
scheduleNavigationSheetRefresh();
}); });
document.getElementById("closeWmsPreview")?.addEventListener("click", closeCadastrePreview); document.getElementById("closeWmsPreview")?.addEventListener("click", closeCadastrePreview);
@@ -5383,6 +5401,9 @@ function setCadastreOpacity(value) {
if (lmeState.cadastre) { if (lmeState.cadastre) {
lmeState.cadastre.setOpacity(lmeState.cadastreOpacity); lmeState.cadastre.setOpacity(lmeState.cadastreOpacity);
} }
if (lmeState.cadastreZoning) {
lmeState.cadastreZoning.setOpacity(Math.max(0.8, lmeState.cadastreOpacity));
}
} }
function toggleCadastreLayer(layer, enabled) { function toggleCadastreLayer(layer, enabled) {
@@ -5394,6 +5415,13 @@ function toggleCadastreLayer(layer, enabled) {
lmeState.cadastreLayers = lmeState.cadastreLayers.filter(item => item !== layer); lmeState.cadastreLayers = lmeState.cadastreLayers.filter(item => item !== layer);
} }
refreshCadastreLayer(); refreshCadastreLayer();
if (layer === "CP.CadastralZoning") {
if (enabled) {
scheduleNavigationSheetRefresh(true);
} else {
clearNavigationSheetOutline();
}
}
const layerNames = { const layerNames = {
"CP.CadastralParcel": "Confini particelle", "CP.CadastralParcel": "Confini particelle",
codice_plla: "Numeri particelle", codice_plla: "Numeri particelle",
@@ -5409,17 +5437,131 @@ function toggleCadastreLayer(layer, enabled) {
setInfo(`${layerNames[layer] || layer}: ${enabled ? "attivo" : "nascosto"}.`); setInfo(`${layerNames[layer] || layer}: ${enabled ? "attivo" : "nascosto"}.`);
} }
function scheduleNavigationSheetRefresh(immediate = false) {
window.clearTimeout(lmeState.navigationSheetRefreshTimer);
if (!lmeState.map
|| !lmeState.dotNetReference
|| !lmeState.cadastreLayers.includes("CP.CadastralZoning")) {
return;
}
if (lmeState.map.getZoom() < 15) {
clearNavigationSheetOutline();
return;
}
lmeState.navigationSheetRefreshTimer = window.setTimeout(
() => void refreshNavigationSheetOutline(),
immediate ? 0 : 650);
}
async function refreshNavigationSheetOutline() {
if (!lmeState.map
|| !lmeState.dotNetReference
|| !lmeState.cadastreLayers.includes("CP.CadastralZoning")) {
return;
}
const center = lmeState.map.getCenter();
const point = [center.lng, center.lat];
if (loadedCadastralSheetAt(point)) {
clearNavigationSheetOutline();
return;
}
if (lmeState.navigationSheetFeature?.geometry
&& geometryContainsPoint(lmeState.navigationSheetFeature.geometry, point)) {
lmeState.navigationSheetLabel?.setLatLng(center);
return;
}
const requestId = ++lmeState.navigationSheetRequestId;
try {
const xml = await lmeState.dotNetReference.invokeMethodAsync(
"GetOfficialCadastralSheetsNear",
center.lat,
center.lng);
if (requestId !== lmeState.navigationSheetRequestId
|| !lmeState.cadastreLayers.includes("CP.CadastralZoning")) {
return;
}
const candidates = gmlParcelsToGeoJson(xml).features || [];
const sheet = findContainingPolygon(point, { type: "FeatureCollection", features: candidates })
|| candidates[0];
if (!sheet) {
return;
}
const reference = String(sheet.properties?.NATIONALCADASTRALZONINGREFERENCE
|| sheet.properties?.LABEL
|| "").trim();
const previousReference = String(
lmeState.navigationSheetFeature?.properties?.NATIONALCADASTRALZONINGREFERENCE
|| lmeState.navigationSheetFeature?.properties?.LABEL
|| "").trim();
lmeState.navigationSheetFeature = sheet;
lmeState.navigationSheetLayer.clearLayers();
L.geoJSON(sheet, {
pane: "navigationSheetPane",
interactive: false,
style: {
color: "#f59e0b",
weight: 4,
opacity: 0.95,
dashArray: "10 6",
fillColor: "#fbbf24",
fillOpacity: 0.055
}
}).addTo(lmeState.navigationSheetLayer);
lmeState.navigationSheetLabel = L.marker(center, {
pane: "navigationSheetPane",
interactive: false,
icon: L.divIcon({
className: "cadastral-sheet-label",
html: escapeHtml(reference ? `Foglio ${reference}` : "Foglio catastale")
})
}).addTo(lmeState.navigationSheetLayer);
if (reference && reference !== previousReference) {
setInfo(`Confine del foglio catastale ${reference} caricato.`);
}
} catch {
if (requestId === lmeState.navigationSheetRequestId) {
setInfo("Confine del foglio catastale non disponibile dal servizio AdE. Riprovo al prossimo spostamento.");
}
}
}
function clearNavigationSheetOutline() {
window.clearTimeout(lmeState.navigationSheetRefreshTimer);
lmeState.navigationSheetRequestId++;
lmeState.navigationSheetFeature = null;
lmeState.navigationSheetLabel = null;
lmeState.navigationSheetLayer?.clearLayers();
}
function loadedCadastralSheetAt(point) {
const features = [
...(lmeState.importedSheets?.toGeoJSON?.()?.features || []),
...(lmeState.imported?.toGeoJSON?.()?.features || [])
].filter(feature => ["Polygon", "MultiPolygon"].includes(feature?.geometry?.type)
&& (feature.properties?._lmeOfficialSheet
|| feature.properties?.NATIONALCADASTRALZONINGREFERENCE
|| isSheetOrOverlayFeature(feature)));
return findContainingPolygon(point, { type: "FeatureCollection", features });
}
function refreshCadastreLayer() { function refreshCadastreLayer() {
if (lmeState.cadastre) { if (lmeState.cadastre) {
lmeState.map.removeLayer(lmeState.cadastre); lmeState.map.removeLayer(lmeState.cadastre);
} }
if (lmeState.cadastreZoning) {
const layers = activeCadastreLayers(); lmeState.map.removeLayer(lmeState.cadastreZoning);
lmeState.cadastre = null;
if (!layers.length) {
return;
} }
const zoningEnabled = lmeState.cadastreLayers.includes("CP.CadastralZoning");
const layers = activeCadastreLayers().filter(layer => layer !== "CP.CadastralZoning");
lmeState.cadastre = null;
lmeState.cadastreZoning = null;
if (layers.length) {
const cadastreLayer = L.tileLayer.wms(cadastreUrl(), { const cadastreLayer = L.tileLayer.wms(cadastreUrl(), {
pane: "cadastrePane", pane: "cadastrePane",
layers: layers.join(","), layers: layers.join(","),
@@ -5431,21 +5573,53 @@ function refreshCadastreLayer() {
opacity: lmeState.cadastreOpacity, opacity: lmeState.cadastreOpacity,
attribution: "Agenzia delle Entrate - CC BY 4.0" attribution: "Agenzia delle Entrate - CC BY 4.0"
}); });
attachCadastreTileRecovery(cadastreLayer);
lmeState.cadastre = cadastreLayer.addTo(lmeState.map);
}
if (zoningEnabled) {
const zoningLayer = L.tileLayer.wms(cadastreUrl(), {
pane: "cadastreZoningPane",
layers: "CP.CadastralZoning",
format: "image/png",
transparent: true,
version: "1.1.1",
crs: cadastreCrs,
minZoom: 8,
maxZoom: 14,
opacity: Math.max(0.8, lmeState.cadastreOpacity),
attribution: "Agenzia delle Entrate - CC BY 4.0"
});
attachCadastreTileRecovery(zoningLayer);
lmeState.cadastreZoning = zoningLayer.addTo(lmeState.map);
scheduleNavigationSheetRefresh(true);
}
}
function attachCadastreTileRecovery(cadastreLayer) {
let errorShown = false; let errorShown = false;
cadastreLayer.on("tileerror", () => { cadastreLayer.on("tileerror", event => {
const tile = event?.tile;
const retryCount = Number(tile?.dataset?.lmeRetry || 0);
if (tile && retryCount < 2) {
tile.dataset.lmeRetry = String(retryCount + 1);
window.setTimeout(() => {
const separator = tile.src.includes("?") ? "&" : "?";
tile.src = `${tile.src}${separator}_lme_retry=${Date.now()}`;
}, 450 * (retryCount + 1));
return;
}
if (errorShown) { if (errorShown) {
return; return;
} }
errorShown = true; errorShown = true;
setInfo("Layer catastali non caricati dal servizio AdE. Controlla la connessione e riprova."); setInfo("Il servizio cartografico AdE non ha risposto. La connessione internet puo essere attiva: attendi qualche secondo e riprova.");
}); });
cadastreLayer.on("load", () => { cadastreLayer.on("load", () => {
errorShown = false; errorShown = false;
}); });
lmeState.cadastre = cadastreLayer.addTo(lmeState.map);
} }
function activeCadastreLayers() { function activeCadastreLayers() {
@@ -5637,6 +5811,12 @@ function onGpsPosition(position) {
updateGpsPositionLayers(latlng, lmeState.lastAccuracy); updateGpsPositionLayers(latlng, lmeState.lastAccuracy);
if (firstLiveFix) { if (firstLiveFix) {
lmeState.map.setView(latlng, 19); lmeState.map.setView(latlng, 19);
} else if (!lmeState.map.getBounds().contains(latlng)) {
lmeState.map.panTo(latlng, {
animate: true,
duration: 0.5,
easeLinearity: 0.25
});
} }
if (lmeState.gpsTrailEnabled) { if (lmeState.gpsTrailEnabled) {
@@ -5899,7 +6079,36 @@ function cadastreUrl() {
} }
function setInfo(text) { function setInfo(text) {
setText("mapInfo", text); const container = document.getElementById("mapInfo");
if (!container) return;
setText("mapInfoText", text);
container.hidden = false;
if (lmeState.infoTimer) {
window.clearTimeout(lmeState.infoTimer);
lmeState.infoTimer = null;
}
if (lmeState.infoTimeoutSeconds > 0) {
lmeState.infoTimer = window.setTimeout(hideInfo, lmeState.infoTimeoutSeconds * 1000);
}
}
function hideInfo() {
const container = document.getElementById("mapInfo");
if (container) container.hidden = true;
if (lmeState.infoTimer) {
window.clearTimeout(lmeState.infoTimer);
lmeState.infoTimer = null;
}
}
function setInfoTimeout(seconds) {
const value = Number(seconds);
lmeState.infoTimeoutSeconds = Number.isFinite(value) ? Math.min(30, Math.max(0, value)) : 3;
const container = document.getElementById("mapInfo");
if (container && !container.hidden) {
setInfo(document.getElementById("mapInfoText")?.textContent || "");
}
} }
function setText(id, text) { function setText(id, text) {