Compare commits

...
Author SHA1 Message Date
livio bc037a57d4 Prepare Pro 1.0.14 release 2026-10-04 20:23:59 +02:00
livio ce355f820c Add contextual fiducial point downloads 2026-10-04 19:28:31 +02:00
livio 85e1c93d8a Add mapped Pro fiducial point layer 2026-10-04 15:18:42 +02:00
livio 7b642afe2d Add Pro fiducial monograph lookup 2026-10-04 14:38:49 +02:00
livio 342935d99d Show cadastral sheets while navigating 2026-10-02 11:24:35 +02:00
11 changed files with 1209 additions and 34 deletions

No files matched your search

+367
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>
@@ -94,6 +96,16 @@
<small>Particelle e geometrie verdi di file e progetti</small> <small>Particelle e geometrie verdi di file e progetti</small>
</span> </span>
</label> </label>
@if (CanUse(AppFeatures.FiducialPoints))
{
<label class="map-layer-option">
<input type="checkbox" @bind="ShowFiducialPoints" @bind:after="ApplyFiducialVisibility" />
<span>
<strong>Punti fiduciali</strong>
<small>PF georiferiti caricati nella ricerca Pro</small>
</span>
</label>
}
<section class="layer-tree-section"> <section class="layer-tree-section">
<header> <header>
<strong>Ordine livelli e oggetti</strong> <strong>Ordine livelli e oggetti</strong>
@@ -201,6 +213,104 @@
<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>
<label class="toggle-line">
<input type="checkbox" @bind="KeepPreviousFiducialPoints" @bind:after="ApplyKeepPreviousFiducials" />
<span>Mantieni i PF dei fogli già caricati</span>
</label>
<small class="field-help">Ogni foglio resta un sottogruppo separato nel pannello Layer, da cui puoi nasconderlo o rimuoverlo.</small>
<label class="toggle-line">
<input type="checkbox" @bind="ShowFiducialPoints" @bind:after="ApplyFiducialVisibility" />
<span>Mostra i PF sulla mappa</span>
</label>
<label>
Colore PF
<input type="color" @bind="FiducialColor" @bind:after="ApplyFiducialColor" />
</label>
@if (!string.IsNullOrWhiteSpace(FiducialMessage))
{
<div class="catalog-progress"><span>@FiducialMessage</span></div>
}
@if (MappedFiducialPoints.Count > 0)
{
<section class="panel-section">
<h2>Punti cartografici (@MappedFiducialPoints.Count)</h2>
<button class="secondary-command" @onclick="FitFiducialPoints">Inquadra tutti i PF</button>
<div class="fiducial-list">
@foreach (var point in MappedFiducialPoints)
{
<article class="fiducial-item">
<strong>@point.Identifier</strong>
<small>Nord/X: @FormatCoordinate(point.CadastralNorth) · Est/Y: @FormatCoordinate(point.CadastralEast)</small>
@if (!string.IsNullOrWhiteSpace(point.MonographUrl))
{
<button class="secondary-command" @onclick="() => DownloadFiducialMonograph(point.MonographUrl)">Scarica PDF monografia</button>
}
</article>
}
</div>
</section>
}
<div class="service-note">
<strong>Monografie ufficiali, posizione sperimentale</strong>
<p>Elenco e PDF provengono dall'Agenzia delle Entrate; il posizionamento sperimentale usa il servizio cartografico GloboGIS. Le coordinate sono un ausilio: verificale prima di qualsiasi uso topografico o professionale.</p>
</div>
}
</section>
}
else if (ActiveTab == "data") else if (ActiveTab == "data")
{ {
<section class="panel-content"> <section class="panel-content">
@@ -745,6 +855,12 @@
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 FiducialColor { get; set; } = "#dc2626";
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;
@@ -762,6 +878,8 @@
private bool GpsOverlayCollapsed { 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 ShowFiducialPoints { get; set; } = true;
private bool KeepPreviousFiducialPoints { get; set; } = true;
private bool ShowImportedData { get; set; } = true; private bool ShowImportedData { get; set; } = true;
private bool DrawToolsVisible { get; set; } private bool DrawToolsVisible { get; set; }
private bool TrailEnabled { get; set; } private bool TrailEnabled { get; set; }
@@ -821,6 +939,11 @@
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<MappedFiducialPoint> MappedFiducialPoints { 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);
@@ -834,6 +957,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",
@@ -845,6 +969,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",
@@ -1056,6 +1181,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)
@@ -1135,6 +1264,235 @@
: 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 = [];
MappedFiducialPoints = [];
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 = [];
MappedFiducialPoints = [];
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 municipality = FiducialMunicipalities.FirstOrDefault(item => item.Code == SelectedFiducialMunicipalityCode);
if (municipality is null || string.IsNullOrWhiteSpace(SelectedFiducialSheet))
{
return;
}
await LoadFiducialsForSheetAsync(
municipality.Code,
SelectedFiducialSheet,
municipality.Name);
}
private async Task LoadFiducialsForSheetAsync(
string municipalityCode,
string sheet,
string? municipalityName = null)
{
FiducialBusy = true;
FiducialPoints = [];
MappedFiducialPoints = [];
FiducialMessage = "Recupero monografie e coordinate dei PF...";
await InvokeAsync(StateHasChanged);
try
{
string? monographError = null;
try
{
FiducialPoints = await FiducialPointService.GetFiducialsAsync(
municipalityCode,
sheet);
}
catch (Exception error)
{
monographError = error.GetBaseException().Message;
}
var mapped = await FiducialPointService.GetMappedFiducialsAsync(
municipalityCode,
sheet);
var monographs = FiducialPoints.ToDictionary(
item => item.Identifier,
item => item.MonographUrl,
StringComparer.OrdinalIgnoreCase);
var currentMappedPoints = monographs.Count == 0
? mapped
: mapped.Where(point => monographs.ContainsKey(point.Identifier)).ToArray();
MappedFiducialPoints = currentMappedPoints
.Select(point => point with
{
MonographUrl = monographs.GetValueOrDefault(point.Identifier, string.Empty)
})
.ToArray();
await JS.InvokeVoidAsync(
"lmeMap.setFiducialPoints",
MappedFiducialPoints,
FiducialColor,
ShowFiducialPoints,
KeepPreviousFiducialPoints,
municipalityCode,
sheet,
string.IsNullOrWhiteSpace(municipalityName) ? municipalityCode : municipalityName);
FiducialMessage = MappedFiducialPoints.Count == 0
? "Nessun Punto Fiduciale georiferito per il foglio selezionato."
: $"Caricati {MappedFiducialPoints.Count} PF del foglio {NormalizeSheetLabel(sheet)} e {FiducialPoints.Count} monografie ufficiali."
+ (monographError is null ? string.Empty : " Le monografie non sono al momento raggiungibili.");
await JS.InvokeVoidAsync("lmeMap.showInfo", FiducialMessage);
}
catch (Exception error)
{
FiducialMessage = $"Ricerca PF non riuscita: {error.GetBaseException().Message}";
await JS.InvokeVoidAsync("lmeMap.showInfo", FiducialMessage);
}
finally
{
FiducialBusy = false;
await InvokeAsync(StateHasChanged);
}
}
[JSInvokable]
public async Task DownloadFiducialsForMapSelection(
string municipalityCode,
string sheet,
string contextLabel)
{
if (!CanUse(AppFeatures.FiducialPoints))
{
await ShowProRequiredAsync();
return;
}
if (FiducialBusy)
{
await JS.InvokeVoidAsync("lmeMap.showInfo", "È già in corso una ricerca dei Punti Fiduciali.");
return;
}
await JS.InvokeVoidAsync("lmeMap.showInfo", $"Scarico i PF per {contextLabel}...");
var municipality = await CatalogService.FindMunicipalityAsync(municipalityCode);
await LoadFiducialsForSheetAsync(municipalityCode, sheet, municipality?.Name);
}
private static string NormalizeSheetLabel(string sheet) =>
int.TryParse(new string((sheet ?? string.Empty).Where(char.IsDigit).ToArray()), out var number)
? number.ToString(System.Globalization.CultureInfo.InvariantCulture)
: sheet ?? string.Empty;
private async Task DownloadFiducialMonograph(string monographUrl)
{
try
{
await Launcher.Default.OpenAsync(new Uri(monographUrl));
}
catch (Exception error)
{
FiducialMessage = $"Impossibile aprire la monografia: {error.GetBaseException().Message}";
}
}
private async Task ApplyFiducialVisibility()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setFiducialPointsVisible", ShowFiducialPoints);
}
private async Task ApplyFiducialColor()
{
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setFiducialColor", FiducialColor);
}
private Task ApplyKeepPreviousFiducials()
{
SaveFlagPreferences();
return Task.CompletedTask;
}
private async Task FitFiducialPoints() =>
await JS.InvokeVoidAsync("lmeMap.fitFiducialPoints");
private static string FormatCoordinate(double? value) =>
value?.ToString("0.000", System.Globalization.CultureInfo.InvariantCulture) ?? "-";
private async Task OnMunicipalityChanged(ChangeEventArgs args) private async Task OnMunicipalityChanged(ChangeEventArgs args)
{ {
SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty; SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty;
@@ -2153,6 +2511,7 @@
private Task ApplyEntitlementsToMapAsync() => JS.InvokeVoidAsync("lmeMap.setEntitlements", new private Task ApplyEntitlementsToMapAsync() => JS.InvokeVoidAsync("lmeMap.setEntitlements", new
{ {
advancedGeometry = CanUse(AppFeatures.AdvancedGeometry), advancedGeometry = CanUse(AppFeatures.AdvancedGeometry),
fiducialPoints = CanUse(AppFeatures.FiducialPoints),
maxDrawingObjects = Entitlements.HasProAccess ? (int?)null : EntitlementService.BaseDrawingObjectLimit, maxDrawingObjects = Entitlements.HasProAccess ? (int?)null : EntitlementService.BaseDrawingObjectLimit,
drawingLimitMessage = L("drawings.base.limit.reached") drawingLimitMessage = L("drawings.base.limit.reached")
}).AsTask(); }).AsTask();
@@ -2360,6 +2719,9 @@
DrawToolsVisible = GetFlagPreference("draw_tools_visible", false); DrawToolsVisible = GetFlagPreference("draw_tools_visible", false);
ShowGpsPosition = GetFlagPreference("show_gps_position", true); ShowGpsPosition = GetFlagPreference("show_gps_position", true);
ShowSelectedParcels = GetFlagPreference("show_selected_parcels", true); ShowSelectedParcels = GetFlagPreference("show_selected_parcels", true);
ShowFiducialPoints = GetFlagPreference("show_fiducial_points", true);
KeepPreviousFiducialPoints = GetFlagPreference("keep_previous_fiducials", true);
FiducialColor = Preferences.Default.Get($"{FlagPreferencePrefix}fiducial_color", "#dc2626");
TrailEnabled = GetFlagPreference("trail_enabled", false); TrailEnabled = GetFlagPreference("trail_enabled", false);
ExportImported = GetFlagPreference("export_imported", true); ExportImported = GetFlagPreference("export_imported", true);
ExportImportedSheets = GetFlagPreference("export_imported_sheets", true); ExportImportedSheets = GetFlagPreference("export_imported_sheets", true);
@@ -2400,6 +2762,9 @@
SetFlagPreference("draw_tools_visible", DrawToolsVisible); SetFlagPreference("draw_tools_visible", DrawToolsVisible);
SetFlagPreference("show_gps_position", ShowGpsPosition); SetFlagPreference("show_gps_position", ShowGpsPosition);
SetFlagPreference("show_selected_parcels", ShowSelectedParcels); SetFlagPreference("show_selected_parcels", ShowSelectedParcels);
SetFlagPreference("show_fiducial_points", ShowFiducialPoints);
SetFlagPreference("keep_previous_fiducials", KeepPreviousFiducialPoints);
Preferences.Default.Set($"{FlagPreferencePrefix}fiducial_color", FiducialColor);
SetFlagPreference("trail_enabled", TrailEnabled); SetFlagPreference("trail_enabled", TrailEnabled);
SetFlagPreference("export_imported", ExportImported); SetFlagPreference("export_imported", ExportImported);
SetFlagPreference("export_imported_sheets", ExportImportedSheets); SetFlagPreference("export_imported_sheets", ExportImportedSheets);
@@ -2437,6 +2802,8 @@
await JS.InvokeVoidAsync("lmeMap.setDrawToolsVisible", DrawToolsVisible); await JS.InvokeVoidAsync("lmeMap.setDrawToolsVisible", DrawToolsVisible);
await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition); await JS.InvokeVoidAsync("lmeMap.setGpsPositionVisible", ShowGpsPosition);
await JS.InvokeVoidAsync("lmeMap.setSelectedParcelsVisible", ShowSelectedParcels); await JS.InvokeVoidAsync("lmeMap.setSelectedParcelsVisible", ShowSelectedParcels);
await JS.InvokeVoidAsync("lmeMap.setFiducialColor", FiducialColor);
await JS.InvokeVoidAsync("lmeMap.setFiducialPointsVisible", ShowFiducialPoints);
await JS.InvokeVoidAsync("lmeMap.toggleTrail", TrailEnabled); await JS.InvokeVoidAsync("lmeMap.toggleTrail", TrailEnabled);
foreach (var layer in CadastralLayers) foreach (var layer in CadastralLayers)
+4 -2
View File
@@ -20,6 +20,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCssItems>false</EnableDefaultCssItems> <EnableDefaultCssItems>false</EnableDefaultCssItems>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<DefaultItemExcludes>$(DefaultItemExcludes);obj/**;bin/**;obj-api36*/**;artifacts/**</DefaultItemExcludes>
<!-- Display name --> <!-- Display name -->
<ApplicationTitle>LME Mappe Catastali Pro</ApplicationTitle> <ApplicationTitle>LME Mappe Catastali Pro</ApplicationTitle>
@@ -28,8 +29,8 @@
<ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId> <ApplicationId>it.liviomerola.lmemappecatastali.pro</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.0.10</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.14</ApplicationDisplayVersion>
<ApplicationVersion>110</ApplicationVersion> <ApplicationVersion>114</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 -->
@@ -61,6 +62,7 @@
<PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'"> <PropertyGroup Condition="'$(Configuration)' == 'Release' and $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<AndroidTargetSdkVersion>36</AndroidTargetSdkVersion> <AndroidTargetSdkVersion>36</AndroidTargetSdkVersion>
<RuntimeIdentifiers>android-arm;android-arm64</RuntimeIdentifiers>
<AndroidPackageFormat>aab</AndroidPackageFormat> <AndroidPackageFormat>aab</AndroidPackageFormat>
<AndroidUseAapt2>true</AndroidUseAapt2> <AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidCreatePackagePerAbi>false</AndroidCreatePackagePerAbi> <AndroidCreatePackagePerAbi>false</AndroidCreatePackagePerAbi>
+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.10` / version code `110`. - 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.
+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;
} }
} }
+6
View File
@@ -4,6 +4,12 @@ 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 ## 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. - Migliora la compatibilita del servizio catastale AdE sui dispositivi Android meno recenti con HTTP/1.1, tentativi automatici e una ricerca GPS piu tollerante.
+8
View File
@@ -40,6 +40,14 @@ public sealed class CadastralCatalogService
.ToArray(); .ToArray();
} }
public async Task<MunicipalityOption?> FindMunicipalityAsync(string municipalityCode)
{
var code = (municipalityCode ?? string.Empty).Trim();
var municipalities = await GetAllMunicipalitiesAsync();
return municipalities.FirstOrDefault(item =>
string.Equals(item.Code, code, StringComparison.OrdinalIgnoreCase));
}
public async Task<IReadOnlyList<string>> GetSheetsAsync( public async Task<IReadOnlyList<string>> GetSheetsAsync(
MunicipalityOption municipality, MunicipalityOption municipality,
IProgress<CatalogProgress>? progress = null, IProgress<CatalogProgress>? progress = null,
+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";
} }
+292
View File
@@ -0,0 +1,292 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace LMEMappeCatastaliApp.Services;
public sealed partial class FiducialPointService
{
private const string ServiceBaseUrl = "https://www1.agenziaentrate.gov.it/servizi/Monografie/";
private const string MappedPointsUrl = "https://cartografia.globogis.it/arcgis/rest/services/fiduciali/Punti_fiduciali/MapServer/2/query";
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 ParseFiducialLinks(html);
}
public async Task<IReadOnlyList<FiducialPointEntry>> GetFiducialsAsync(
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(municipalityCode);
var normalizedSheet = NormalizeSheet(sheet);
var html = await GetHtmlAsync(
$"risultato.php?co={Uri.EscapeDataString(code)}&foglio={Uri.EscapeDataString(normalizedSheet)}",
cancellationToken);
return ParseFiducialLinks(html);
}
private static IReadOnlyList<FiducialPointEntry> ParseFiducialLinks(string html) =>
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();
public async Task<IReadOnlyList<MappedFiducialPoint>> GetMappedFiducialsAsync(
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var municipality = NormalizeCode(municipalityCode);
if (!int.TryParse(NormalizeSheet(sheet), out var sheetNumber))
{
throw new ArgumentException("Numero di foglio non valido.", nameof(sheet));
}
var query = new Dictionary<string, string>
{
["f"] = "json",
["where"] = $"COMUNENAZI='{municipality}' AND FOGLIO={sheetNumber}",
["outFields"] = "ETICHETTA,PARTICELLA,NORD,EST,ATTENDIBIL,QUOTA,DATAAGGIOR,IDORIGINE,URL_AGENZI,FOGLIO,ALLEGATO,FIDUCIALE",
["returnGeometry"] = "true",
["outSR"] = "3857",
["resultRecordCount"] = "500"
};
var url = $"{MappedPointsUrl}?{string.Join('&', query.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value)}"))}";
using var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
if (document.RootElement.TryGetProperty("error", out var error))
{
throw new InvalidOperationException(error.TryGetProperty("message", out var message)
? message.GetString()
: "Servizio cartografico PF non disponibile.");
}
if (!document.RootElement.TryGetProperty("features", out var features))
{
return [];
}
return features.EnumerateArray()
.Select(feature => ParseMappedPoint(feature, municipality))
.Where(point => point is not null)
.Cast<MappedFiducialPoint>()
.DistinctBy(point => point.Identifier, StringComparer.OrdinalIgnoreCase)
.OrderBy(point => point.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static MappedFiducialPoint? ParseMappedPoint(JsonElement feature, string municipalityCode)
{
if (!feature.TryGetProperty("geometry", out var geometry)
|| !TryGetDouble(geometry, "x", out var x)
|| !TryGetDouble(geometry, "y", out var y))
{
return null;
}
var attributes = feature.GetProperty("attributes");
var label = GetString(attributes, "ETICHETTA");
var identifier = FormatIdentifier(label);
if (identifier == label)
{
var sheet = GetString(attributes, "FOGLIO").PadLeft(4, '0');
var number = GetString(attributes, "FIDUCIALE").PadLeft(2, '0');
identifier = $"PF{number}/{sheet}/{municipalityCode}";
}
const double earthRadius = 6378137d;
var longitude = x / earthRadius * 180d / Math.PI;
var latitude = (2d * Math.Atan(Math.Exp(y / earthRadius)) - Math.PI / 2d) * 180d / Math.PI;
return new MappedFiducialPoint(
identifier,
latitude,
longitude,
GetNullableDouble(attributes, "NORD"),
GetNullableDouble(attributes, "EST"),
GetString(attributes, "ATTENDIBIL"),
GetNullableDouble(attributes, "QUOTA"),
GetString(attributes, "PARTICELLA"),
GetString(attributes, "DATAAGGIOR"),
GetString(attributes, "IDORIGINE"));
}
private static bool TryGetDouble(JsonElement element, string name, out double value)
{
value = 0;
return element.TryGetProperty(name, out var property)
&& (property.ValueKind == JsonValueKind.Number
? property.TryGetDouble(out value)
: double.TryParse(property.ToString(), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out value));
}
private static double? GetNullableDouble(JsonElement element, string name) =>
TryGetDouble(element, name, out var value) ? value : null;
private static string GetString(JsonElement element, string name) =>
element.TryGetProperty(name, out var property) && property.ValueKind != JsonValueKind.Null
? property.ToString().Trim()
: string.Empty;
private static string NormalizeSheet(string sheet)
{
var digits = new string((sheet ?? string.Empty).Where(char.IsDigit).ToArray());
if (string.IsNullOrWhiteSpace(digits))
{
throw new ArgumentException("Numero di foglio non valido.", nameof(sheet));
}
return int.TryParse(digits, out var number)
? number.ToString(System.Globalization.CultureInfo.InvariantCulture)
: digits;
}
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);
public sealed record MappedFiducialPoint(
string Identifier,
double Latitude,
double Longitude,
double? CadastralNorth,
double? CadastralEast,
string Reliability,
double? Elevation,
string Parcel,
string UpdatedAt,
string OriginId,
string MonographUrl = "");
+68
View File
@@ -1185,6 +1185,17 @@ input[type="checkbox"] {
text-decoration: none; text-decoration: none;
} }
.lme-geometry-toolbar a.lme-fiducial-download {
font-size: 12px !important;
letter-spacing: 0;
}
.lme-geometry-toolbar a.is-disabled {
color: #8b949e;
background: #eef1f3;
opacity: 0.72;
}
.leaflet-help { .leaflet-help {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -1247,6 +1258,9 @@ input[type="checkbox"] {
.gps-overlay.is-collapsed { .gps-overlay.is-collapsed {
grid-template-columns: auto; grid-template-columns: auto;
right: auto;
width: 48px;
max-width: 48px;
padding: 5px; padding: 5px;
} }
@@ -1255,6 +1269,28 @@ input[type="checkbox"] {
display: none; 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;
} }
@@ -1318,6 +1354,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;
@@ -1866,6 +1917,12 @@ 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: 174px; top: 174px;
right: 0; right: 0;
@@ -1921,6 +1978,17 @@ footer {
grid-template-columns: minmax(0, 1fr) auto auto; 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-main,
.gps-center { .gps-center {
min-width: 86px; min-width: 86px;
+454 -29
View File
@@ -5,8 +5,9 @@ const lmeState = {
currentBaseName: "satellite", currentBaseName: "satellite",
layerOrder: [ layerOrder: [
"gpsPosition", "gpsPosition",
"drawings",
"gpsTrail", "gpsTrail",
"fiducials",
"drawings",
"gpsParcels", "gpsParcels",
"selectedParcels", "selectedParcels",
"importedSheets", "importedSheets",
@@ -14,8 +15,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,
@@ -29,6 +36,7 @@ const lmeState = {
drawingDeleteButton: null, drawingDeleteButton: null,
drawingScopeMenu: null, drawingScopeMenu: null,
geometryToolsControl: null, geometryToolsControl: null,
fiducialDownloadButton: null,
ignoreMapClickUntil: 0, ignoreMapClickUntil: 0,
activeDrawHandler: null, activeDrawHandler: null,
drawEventsBound: false, drawEventsBound: false,
@@ -44,6 +52,9 @@ const lmeState = {
gpsTrail: null, gpsTrail: null,
selectedParcelsLayer: null, selectedParcelsLayer: null,
selectedParcelsVisible: true, selectedParcelsVisible: true,
fiducials: null,
fiducialsVisible: true,
fiducialColor: "#dc2626",
gpsParcelLayer: null, gpsParcelLayer: null,
gpsTrailEnabled: false, gpsTrailEnabled: false,
gpsTrailPoints: [], gpsTrailPoints: [],
@@ -125,6 +136,10 @@ window.lmeMap = {
closeCadastrePreview, closeCadastrePreview,
clearSelectedParcels, clearSelectedParcels,
setSelectedParcelsVisible, setSelectedParcelsVisible,
setFiducialPoints,
setFiducialPointsVisible,
setFiducialColor,
fitFiducialPoints,
clearMapElements, clearMapElements,
fitImported, fitImported,
clearImported, clearImported,
@@ -211,6 +226,7 @@ function setLocalization(options = {}) {
function setEntitlements(options = {}) { function setEntitlements(options = {}) {
lmeState.entitlements = { ...lmeState.entitlements, ...options }; lmeState.entitlements = { ...lmeState.entitlements, ...options };
updateFiducialDownloadButton();
} }
function requireProFeature(feature) { function requireProFeature(feature) {
@@ -304,7 +320,15 @@ function init(dotNetReference) {
lmeState.importedMeasurements = L.layerGroup().addTo(lmeState.map); lmeState.importedMeasurements = L.layerGroup().addTo(lmeState.map);
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.fiducials = 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",
@@ -339,6 +363,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);
@@ -1325,6 +1350,7 @@ function managedLayerPanes() {
gpsTrail: "gpsTrailPane", gpsTrail: "gpsTrailPane",
gpsParcels: "gpsParcelsPane", gpsParcels: "gpsParcelsPane",
selectedParcels: "selectedParcelsPane", selectedParcels: "selectedParcelsPane",
fiducials: "fiducialsPane",
importedSheets: "importedSheetPane", importedSheets: "importedSheetPane",
imported: "importedPane", imported: "importedPane",
cadastre: "cadastrePane" cadastre: "cadastrePane"
@@ -1355,7 +1381,8 @@ function restoreLayerOrder() {
try { try {
const saved = JSON.parse(localStorage.getItem("lmeMapLayerOrder") || "null"); const saved = JSON.parse(localStorage.getItem("lmeMapLayerOrder") || "null");
if (Array.isArray(saved)) { if (Array.isArray(saved)) {
lmeState.layerOrder = saved; const remaining = saved.filter(key => !["gpsPosition", "gpsTrail", "fiducials"].includes(key));
lmeState.layerOrder = ["gpsPosition", "gpsTrail", "fiducials", ...remaining];
} }
} catch { } catch {
} }
@@ -1413,6 +1440,7 @@ function layerManagerCategories() {
{ key: "gpsTrail", label: "Traccia GPS", roots: [lmeState.gpsTrail].filter(Boolean) }, { key: "gpsTrail", label: "Traccia GPS", roots: [lmeState.gpsTrail].filter(Boolean) },
{ key: "gpsParcels", label: "Particella rilevata dal GPS", roots: [lmeState.gpsParcelLayer].filter(Boolean) }, { key: "gpsParcels", label: "Particella rilevata dal GPS", roots: [lmeState.gpsParcelLayer].filter(Boolean) },
{ key: "selectedParcels", label: "Particelle catastali evidenziate", roots: [lmeState.selectedParcelsLayer].filter(Boolean) }, { key: "selectedParcels", label: "Particelle catastali evidenziate", roots: [lmeState.selectedParcelsLayer].filter(Boolean) },
{ key: "fiducials", label: "Punti fiduciali", roots: [lmeState.fiducials].filter(Boolean), blocks: true },
{ key: "importedSheets", label: "Cornici e fogli importati", roots: [lmeState.importedSheets].filter(Boolean), blocks: true }, { key: "importedSheets", label: "Cornici e fogli importati", roots: [lmeState.importedSheets].filter(Boolean), blocks: true },
{ key: "imported", label: "Progetti e dati importati", roots: [lmeState.imported].filter(Boolean), blocks: true }, { key: "imported", label: "Progetti e dati importati", roots: [lmeState.imported].filter(Boolean), blocks: true },
{ key: "cadastre", label: "Layer catastali AdE", roots: [lmeState.cadastre].filter(Boolean) } { key: "cadastre", label: "Layer catastali AdE", roots: [lmeState.cadastre].filter(Boolean) }
@@ -1600,11 +1628,11 @@ async function renameManagedBlock(category, source, records) {
function managedCategoryCanRemove(category, records) { function managedCategoryCanRemove(category, records) {
return records.length > 0 return records.length > 0
&& ["drawings", "imported", "importedSheets", "selectedParcels", "gpsParcels"].includes(category.key); && ["drawings", "imported", "importedSheets", "selectedParcels", "gpsParcels", "fiducials"].includes(category.key);
} }
function managedObjectCanRemove(record) { function managedObjectCanRemove(record) {
return ["drawings", "imported", "importedSheets", "selectedParcels", "gpsParcels"].includes(record.categoryKey); return ["drawings", "imported", "importedSheets", "selectedParcels", "gpsParcels", "fiducials"].includes(record.categoryKey);
} }
function managedCategoryCanMove(categoryKey) { function managedCategoryCanMove(categoryKey) {
@@ -1866,6 +1894,7 @@ function managedObjectDetails(layer) {
} }
function refreshLayerManager() { function refreshLayerManager() {
updateFiducialDownloadButton();
const container = document.getElementById("layerManagerTree"); const container = document.getElementById("layerManagerTree");
if (!container || !lmeState.map) { if (!container || !lmeState.map) {
return; return;
@@ -2001,8 +2030,12 @@ function renderLayerManagerBlock(category, source, records) {
const blockName = managedBlockName(source, records); const blockName = managedBlockName(source, records);
const rowActions = layerManagerRowActions( const rowActions = layerManagerRowActions(
`<button type="button" class="layer-rename-button" aria-label="Rinomina gruppo" title="Rinomina">Rinomina</button>`, `<button type="button" class="layer-rename-button" aria-label="Rinomina gruppo" title="Rinomina">Rinomina</button>`,
`<button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button>`, managedCategoryCanMove(category.key)
`<button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button>`); ? `<button type="button" class="layer-membership-button" aria-label="Sposta gruppo in un altro gruppo" title="Sposta nel gruppo">Sposta nel gruppo</button>`
: "",
managedObjectCanRemove(records[0])
? `<button type="button" class="layer-remove-button" aria-label="Rimuovi gruppo dal progetto" title="Rimuovi dal progetto">Rimuovi</button>`
: "");
block.classList.add("layer-manager-with-actions"); block.classList.add("layer-manager-with-actions");
const blockLocked = records.length > 0 && records.every(record => Boolean(record.layer?.feature?.properties?._lmeLocked)); const blockLocked = records.length > 0 && records.every(record => Boolean(record.layer?.feature?.properties?._lmeLocked));
block.innerHTML = `<summary>${layerExpandButton(block.open, blockName)}<button type="button" class="layer-drag-handle" aria-label="Trascina blocco">☰</button><input type="checkbox" ${visible ? "checked" : ""} aria-label="Mostra ${escapeHtml(blockName)}"><span class="layer-manager-title">${escapeHtml(blockName)}</span><button type="button" class="layer-inline-lock ${blockLocked ? "is-locked" : ""}" aria-label="${blockLocked ? "Sblocca" : "Blocca"} gruppo" title="${blockLocked ? "Sblocca" : "Blocca"}">${blockLocked ? "🔒" : "🔓"}</button>${rowActions}${layerManagerMoveButtons("blocco")}<span class="layer-manager-count">${records.length}</span></summary><div class="layer-manager-children"></div>`; block.innerHTML = `<summary>${layerExpandButton(block.open, blockName)}<button type="button" class="layer-drag-handle" aria-label="Trascina blocco">☰</button><input type="checkbox" ${visible ? "checked" : ""} aria-label="Mostra ${escapeHtml(blockName)}"><span class="layer-manager-title">${escapeHtml(blockName)}</span><button type="button" class="layer-inline-lock ${blockLocked ? "is-locked" : ""}" aria-label="${blockLocked ? "Sblocca" : "Blocca"} gruppo" title="${blockLocked ? "Sblocca" : "Blocca"}">${blockLocked ? "🔒" : "🔓"}</button>${rowActions}${layerManagerMoveButtons("blocco")}<span class="layer-manager-count">${records.length}</span></summary><div class="layer-manager-children"></div>`;
@@ -2023,12 +2056,12 @@ function renderLayerManagerBlock(category, source, records) {
event.stopPropagation(); event.stopPropagation();
renameManagedBlock(category, source, records); renameManagedBlock(category, source, records);
}); });
block.querySelector(".layer-remove-button").addEventListener("click", event => { block.querySelector(".layer-remove-button")?.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
removeManagedRecords(records, `il gruppo '${blockName}'`); removeManagedRecords(records, `il gruppo '${blockName}'`);
}); });
block.querySelector(".layer-membership-button").addEventListener("click", event => { block.querySelector(".layer-membership-button")?.addEventListener("click", event => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
showMoveManagedRecordsDialog(records, `il gruppo '${blockName}'`); showMoveManagedRecordsDialog(records, `il gruppo '${blockName}'`);
@@ -2918,6 +2951,7 @@ function showLeafletToolbarHelp() {
<div><dt>✓</dt><dd><strong>Controlla e ripara</strong>Verifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.</dd></div> <div><dt>✓</dt><dd><strong>Controlla e ripara</strong>Verifica gli oggetti selezionati e prova a correggere coordinate duplicate o poligoni non validi. Esempio: usalo quando un contorno importato non si esporta o si riempie male; al termine compare un riepilogo.</dd></div>
<div><dt>◉</dt><dd><strong>Crea foro</strong>Seleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.</dd></div> <div><dt>◉</dt><dd><strong>Crea foro</strong>Seleziona esattamente due poligoni: prima l'area esterna, poi quella interna. Esempio: sottrai il cortile dal perimetro di un fabbricato o un'isola esclusa da un'area di lavoro.</dd></div>
<div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Seleziona esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde, mostrando le rispettive aree.</dd></div> <div><dt>⇄</dt><dd><strong>Confronta due confini</strong>Seleziona esattamente due poligoni. L'app crea il solo primo in rosso, il solo secondo in blu e la sovrapposizione in verde, mostrando le rispettive aree.</dd></div>
<div><dt>PF</dt><dd><strong>Scarica Punti Fiduciali</strong>Seleziona una particella o un foglio catastale; in alternativa usa la particella rilevata dal GPS. I PF vengono aggiunti come sottogruppo del relativo foglio.</dd></div>
<div><dt>?</dt><dd><strong>Guida</strong>Riapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra, così l'operazione non resta senza riscontro.</dd></div> <div><dt>?</dt><dd><strong>Guida</strong>Riapre questa descrizione. I risultati di riparazione e confronto vengono mostrati anche in una finestra, così l'operazione non resta senza riscontro.</dd></div>
</dl> </dl>
</div>`); </div>`);
@@ -2933,6 +2967,7 @@ function addGeometryToolsControl() {
{ icon: "✓", title: "Controlla e ripara geometrie", run: validateAndRepairSelected }, { icon: "✓", title: "Controlla e ripara geometrie", run: validateAndRepairSelected },
{ icon: "◉", title: "Crea foro da due poligoni", run: createHoleFromSelected }, { icon: "◉", title: "Crea foro da due poligoni", run: createHoleFromSelected },
{ icon: "⇄", title: "Confronta due confini", run: compareSelectedGeometries }, { icon: "⇄", title: "Confronta due confini", run: compareSelectedGeometries },
{ icon: "PF", title: "Scarica PF del foglio o della particella", run: downloadFiducialsForMapContext, fiducials: true },
{ icon: "?", title: "Guida strumenti mappa", run: showLeafletToolbarHelp } { icon: "?", title: "Guida strumenti mappa", run: showLeafletToolbarHelp }
]; ];
actions.forEach(action => { actions.forEach(action => {
@@ -2942,6 +2977,10 @@ function addGeometryToolsControl() {
button.setAttribute("role", "button"); button.setAttribute("role", "button");
button.setAttribute("aria-label", action.title); button.setAttribute("aria-label", action.title);
button.textContent = action.icon; button.textContent = action.icon;
if (action.fiducials) {
button.classList.add("lme-fiducial-download");
lmeState.fiducialDownloadButton = button;
}
L.DomEvent.on(button, "click", event => { L.DomEvent.stop(event); action.run(); }); L.DomEvent.on(button, "click", event => { L.DomEvent.stop(event); action.run(); });
}); });
L.DomEvent.disableClickPropagation(container); L.DomEvent.disableClickPropagation(container);
@@ -2949,6 +2988,88 @@ function addGeometryToolsControl() {
} }
}); });
lmeState.geometryToolsControl = new GeometryTools().addTo(lmeState.map); lmeState.geometryToolsControl = new GeometryTools().addTo(lmeState.map);
updateFiducialDownloadButton();
}
function downloadFiducialsForMapContext() {
if (!requireProFeature("fiducialPoints")) return false;
const context = currentFiducialMapContext();
if (!context) {
setInfo("Seleziona una particella o un foglio catastale. Se il GPS è attivo puoi usare anche la particella corrente.");
return false;
}
if (!lmeState.dotNetReference) {
setInfo("Servizio PF non ancora pronto. Riprova tra poco.");
return false;
}
void lmeState.dotNetReference.invokeMethodAsync(
"DownloadFiducialsForMapSelection",
context.municipalityCode,
context.sheet,
context.label);
return true;
}
function currentFiducialMapContext() {
const selected = selectedManagedRecords();
if (!selected.length && lmeState.managerHighlightedObjectId) {
const highlighted = lmeState.managerRecords?.get?.(lmeState.managerHighlightedObjectId);
if (highlighted) selected.push(highlighted);
}
const preferred = selected.filter(record => ["selectedParcels", "gpsParcels", "importedSheets", "imported"].includes(record.categoryKey));
for (const record of preferred) {
const context = fiducialContextFromLayer(record.layer);
if (context) return context;
}
const gpsLayers = managedRecordsForCategory(layerManagerCategories().find(category => category.key === "gpsParcels"));
for (const record of gpsLayers) {
const context = fiducialContextFromLayer(record.layer);
if (context) return context;
}
return null;
}
function fiducialContextFromLayer(layer) {
const properties = layer?.feature?.properties || {};
const municipalityCode = String(properties._lmeMunicipalityCode || "").trim().toUpperCase();
const sheet = normalizeFiducialSheet(properties._lmeSheet || properties.foglio || properties.FOGLIO || properties.sheet || properties.SHEET);
if (/^[A-Z][0-9A-Z]{3}$/.test(municipalityCode) && sheet) {
return { municipalityCode, sheet, label: `Comune ${municipalityCode}, foglio ${sheet}` };
}
const references = [
properties.NATIONALCADASTRALREFERENCE,
properties.NATIONALCADASTRALZONINGREFERENCE,
properties.INSPIREID_LOCALID,
properties._lmeParcelReference,
properties.LABEL
].filter(Boolean).map(value => String(value).toUpperCase());
for (const reference of references) {
const match = reference.match(/(?:^|\.)([A-Z][0-9A-Z]{3})[_-](\d{4})(?:\d{2})?(?:[._-]|$)/);
if (match) {
const parsedSheet = normalizeFiducialSheet(match[2]);
return { municipalityCode: match[1], sheet: parsedSheet, label: `Comune ${match[1]}, foglio ${parsedSheet}` };
}
}
return null;
}
function normalizeFiducialSheet(value) {
const digits = String(value ?? "").match(/\d+/)?.[0] || "";
if (!digits) return "";
return String(Number.parseInt(digits, 10));
}
function updateFiducialDownloadButton() {
const button = lmeState.fiducialDownloadButton;
if (!button) return;
const enabled = Boolean(lmeState.entitlements?.fiducialPoints && currentFiducialMapContext());
button.classList.toggle("is-disabled", !enabled);
button.setAttribute("aria-disabled", String(!enabled));
button.title = enabled
? "Scarica PF del foglio o della particella"
: "Seleziona una particella o un foglio per scaricare i PF";
} }
async function numericEditManagedRecord(record) { async function numericEditManagedRecord(record) {
@@ -3061,7 +3182,8 @@ function restoreLastTrashItem() {
if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; } if (lmeState.readOnly) { setInfo("Il progetto e' in sola lettura."); return false; }
const entry = lmeState.trash.pop(); const entry = lmeState.trash.pop();
if (!entry) { setInfo("Il cestino e' vuoto."); return false; } if (!entry) { setInfo("Il cestino e' vuoto."); return false; }
const drawingsToRestore = entry.items.filter(item => !["imported", "importedSheets"].includes(item.categoryKey)).length; const drawingsToRestore = entry.items.filter(item =>
!["imported", "importedSheets", "fiducials"].includes(item.categoryKey)).length;
if (!canAddDrawingObjects(drawingsToRestore)) { if (!canAddDrawingObjects(drawingsToRestore)) {
lmeState.trash.push(entry); lmeState.trash.push(entry);
return false; return false;
@@ -3070,6 +3192,12 @@ function restoreLastTrashItem() {
entry.items.forEach(item => { entry.items.forEach(item => {
if (item.categoryKey === "imported") addImportedFeatures({ type: "FeatureCollection", features: [item.feature] }); if (item.categoryKey === "imported") addImportedFeatures({ type: "FeatureCollection", features: [item.feature] });
else if (item.categoryKey === "importedSheets") addImportedSheetFeatures({ type: "FeatureCollection", features: [item.feature] }); else if (item.categoryKey === "importedSheets") addImportedSheetFeatures({ type: "FeatureCollection", features: [item.feature] });
else if (item.categoryKey === "fiducials") {
L.geoJSON(item.feature, {
pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng),
onEachFeature: (feature, layer) => bindFiducialPopup(feature, layer)
}).eachLayer(layer => lmeState.fiducials.addLayer(layer));
}
else addDrawingFeature(item.feature); else addDrawingFeature(item.feature);
}); });
refreshLayerManager(); refreshLayerManager();
@@ -3257,6 +3385,8 @@ function setManagedCategoryVisible(category, visible) {
lmeState.importedSheetsVisible = visible; lmeState.importedSheetsVisible = visible;
} else if (category.key === "selectedParcels") { } else if (category.key === "selectedParcels") {
lmeState.selectedParcelsVisible = visible; lmeState.selectedParcelsVisible = visible;
} else if (category.key === "fiducials") {
lmeState.fiducialsVisible = visible;
} else if (category.key === "gpsPosition") { } else if (category.key === "gpsPosition") {
lmeState.gpsPositionVisible = visible; lmeState.gpsPositionVisible = visible;
} }
@@ -3330,6 +3460,9 @@ function managedCategoryEnabled(categoryKey) {
if (categoryKey === "selectedParcels") { if (categoryKey === "selectedParcels") {
return lmeState.selectedParcelsVisible; return lmeState.selectedParcelsVisible;
} }
if (categoryKey === "fiducials") {
return lmeState.fiducialsVisible;
}
return true; return true;
} }
@@ -4707,7 +4840,7 @@ function circleToPolygon(center, radiusMeters) {
function createProjectSnapshot(includeCadastralParcels) { function createProjectSnapshot(includeCadastralParcels) {
const center = lmeState.map.getCenter(); const center = lmeState.map.getCenter();
return JSON.stringify({ return JSON.stringify({
schemaVersion: 5, schemaVersion: 6,
view: { view: {
latitude: center.lat, latitude: center.lat,
longitude: center.lng, longitude: center.lng,
@@ -4717,6 +4850,7 @@ function createProjectSnapshot(includeCadastralParcels) {
imported: managedGroupGeoJson(lmeState.imported, "imported"), imported: managedGroupGeoJson(lmeState.imported, "imported"),
importedSheets: managedGroupGeoJson(lmeState.importedSheets, "importedSheets"), importedSheets: managedGroupGeoJson(lmeState.importedSheets, "importedSheets"),
importedSheetsVisible: lmeState.importedSheetsVisible, importedSheetsVisible: lmeState.importedSheetsVisible,
fiducials: managedGroupGeoJson(lmeState.fiducials, "fiducials"),
displayOptions: captureDisplayOptions(), displayOptions: captureDisplayOptions(),
selectedParcels: includeCadastralParcels selectedParcels: includeCadastralParcels
? managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels") ? managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels")
@@ -4752,6 +4886,7 @@ function loadProjectSnapshot(raw, projectName) {
lmeState.drawingMeasurements.clearLayers(); lmeState.drawingMeasurements.clearLayers();
lmeState.importedMeasurements.clearLayers(); lmeState.importedMeasurements.clearLayers();
lmeState.selectedParcelsLayer.clearLayers(); lmeState.selectedParcelsLayer.clearLayers();
lmeState.fiducials.clearLayers();
lmeState.gpsParcelLayer.clearLayers(); lmeState.gpsParcelLayer.clearLayers();
const restoredDrawings = L.geoJSON(data.drawings || emptyFeatureCollection(), { const restoredDrawings = L.geoJSON(data.drawings || emptyFeatureCollection(), {
@@ -4812,6 +4947,10 @@ function loadProjectSnapshot(raw, projectName) {
applyStyleFromProperties(layer); applyStyleFromProperties(layer);
} }
}).addTo(lmeState.selectedParcelsLayer); }).addTo(lmeState.selectedParcelsLayer);
L.geoJSON(data.fiducials || emptyFeatureCollection(), {
pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng),
onEachFeature: (feature, layer) => bindFiducialPopup(feature, layer)
}).addTo(lmeState.fiducials);
if (lmeState.importedSheetsVisible) { if (lmeState.importedSheetsVisible) {
void ensureImportedSheetOutlines().then(() => { void ensureImportedSheetOutlines().then(() => {
applyImportedSheetVisibility(); applyImportedSheetVisibility();
@@ -4834,6 +4973,7 @@ function loadProjectSnapshot(raw, projectName) {
lmeState.drawings, lmeState.drawings,
lmeState.imported, lmeState.imported,
lmeState.selectedParcelsLayer, lmeState.selectedParcelsLayer,
lmeState.fiducials,
lmeState.gpsTrail lmeState.gpsTrail
]; ];
if (lmeState.importedSheetsVisible) { if (lmeState.importedSheetsVisible) {
@@ -4884,6 +5024,8 @@ function captureDisplayOptions() {
drawToolsVisible: Boolean(lmeState.drawControl), drawToolsVisible: Boolean(lmeState.drawControl),
gpsPositionVisible: lmeState.gpsPositionVisible, gpsPositionVisible: lmeState.gpsPositionVisible,
selectedParcelsVisible: lmeState.selectedParcelsVisible, selectedParcelsVisible: lmeState.selectedParcelsVisible,
fiducialsVisible: lmeState.fiducialsVisible,
fiducialColor: lmeState.fiducialColor,
layerOrder: [...lmeState.layerOrder], layerOrder: [...lmeState.layerOrder],
layerObjectOrder: { ...lmeState.managerObjectOrder }, layerObjectOrder: { ...lmeState.managerObjectOrder },
layerCategoryNames: { ...lmeState.managerCategoryNames }, layerCategoryNames: { ...lmeState.managerCategoryNames },
@@ -4951,6 +5093,10 @@ function applyProjectDisplayOptions(options) {
} }
setGpsPositionVisible(options.gpsPositionVisible !== false); setGpsPositionVisible(options.gpsPositionVisible !== false);
setSelectedParcelsVisible(options.selectedParcelsVisible !== false); setSelectedParcelsVisible(options.selectedParcelsVisible !== false);
if (typeof options.fiducialColor === "string") {
setFiducialColor(options.fiducialColor);
}
setFiducialPointsVisible(options.fiducialsVisible !== false);
toggleTrail(Boolean(options.trailEnabled)); toggleTrail(Boolean(options.trailEnabled));
setDrawToolsVisible(Boolean(options.drawToolsVisible)); setDrawToolsVisible(Boolean(options.drawToolsVisible));
applyStoredLayerManagerVisibility(); applyStoredLayerManagerVisibility();
@@ -5387,6 +5533,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) {
@@ -5398,6 +5547,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",
@@ -5413,29 +5569,166 @@ 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 cadastreLayer = L.tileLayer.wms(cadastreUrl(), { const zoningEnabled = lmeState.cadastreLayers.includes("CP.CadastralZoning");
pane: "cadastrePane", const layers = activeCadastreLayers().filter(layer => layer !== "CP.CadastralZoning");
layers: layers.join(","), lmeState.cadastre = null;
format: "image/png", lmeState.cadastreZoning = null;
transparent: true,
version: "1.1.1",
crs: cadastreCrs,
maxZoom: 22,
opacity: lmeState.cadastreOpacity,
attribution: "Agenzia delle Entrate - CC BY 4.0"
});
if (layers.length) {
const cadastreLayer = L.tileLayer.wms(cadastreUrl(), {
pane: "cadastrePane",
layers: layers.join(","),
format: "image/png",
transparent: true,
version: "1.1.1",
crs: cadastreCrs,
maxZoom: 22,
opacity: lmeState.cadastreOpacity,
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", event => { cadastreLayer.on("tileerror", event => {
const tile = event?.tile; const tile = event?.tile;
@@ -5459,8 +5752,6 @@ function refreshCadastreLayer() {
cadastreLayer.on("load", () => { cadastreLayer.on("load", () => {
errorShown = false; errorShown = false;
}); });
lmeState.cadastre = cadastreLayer.addTo(lmeState.map);
} }
function activeCadastreLayers() { function activeCadastreLayers() {
@@ -5475,6 +5766,138 @@ function activeCadastreLayers() {
return drawOrder.filter(layer => lmeState.cadastreLayers.includes(layer)); return drawOrder.filter(layer => lmeState.cadastreLayers.includes(layer));
} }
function setFiducialPoints(points, color, visible = true, keepPrevious = false, municipalityCode = "", sheet = "", municipalityName = "") {
if (!lmeState.fiducials) return;
const source = Array.isArray(points) ? points : [];
if (source.length || lmeState.fiducials.getLayers().length) rememberMapChange();
const normalizedMunicipality = String(municipalityCode || "").trim().toUpperCase();
const normalizedSheet = normalizeFiducialSheet(sheet);
const sheetKey = normalizedMunicipality && normalizedSheet
? `${normalizedMunicipality}:${normalizedSheet}`
: "fiducials-current";
const sheetLabel = `Foglio ${normalizedSheet || sheet}${municipalityName ? ` · ${municipalityName}` : ""}`;
if (!keepPrevious) {
managedRecordsForCategory(layerManagerCategories().find(category => category.key === "fiducials"))
.forEach(record => lmeState.managerSelectedObjectIds.delete(record.id));
lmeState.fiducials.clearLayers();
clearManagedCategoryState("fiducials");
} else {
managedRecordsForCategory(layerManagerCategories().find(category => category.key === "fiducials"))
.filter(record => record.layer?.feature?.properties?._lmeFiducialSheetKey === sheetKey)
.forEach(record => {
record.parent?.removeLayer?.(record.layer);
clearManagedObjectState(record.id);
lmeState.managerSelectedObjectIds.delete(record.id);
});
}
if (typeof color === "string" && /^#[0-9a-f]{6}$/i.test(color)) {
lmeState.fiducialColor = color;
}
source.forEach((point, index) => {
const latitude = Number(point.latitude ?? point.Latitude);
const longitude = Number(point.longitude ?? point.Longitude);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return;
const feature = {
type: "Feature",
geometry: { type: "Point", coordinates: [longitude, latitude] },
properties: {
name: String(point.identifier ?? point.Identifier ?? `PF ${index + 1}`),
_lmeDisplayName: String(point.identifier ?? point.Identifier ?? `PF ${index + 1}`),
_lmeSource: sheetKey,
_lmeGroupName: sheetLabel,
_lmeFiducialSheetKey: sheetKey,
_lmeMunicipalityCode: normalizedMunicipality,
_lmeSheet: normalizedSheet,
_lmeColor: lmeState.fiducialColor,
_lmeFillColor: lmeState.fiducialColor,
_lmeWeight: 3,
_lmeOpacity: 1,
_lmeFillOpacity: 0.72,
cadastralNorth: point.cadastralNorth ?? point.CadastralNorth ?? null,
cadastralEast: point.cadastralEast ?? point.CadastralEast ?? null,
reliability: String(point.reliability ?? point.Reliability ?? ""),
elevation: point.elevation ?? point.Elevation ?? null,
parcel: String(point.parcel ?? point.Parcel ?? ""),
updatedAt: String(point.updatedAt ?? point.UpdatedAt ?? ""),
originId: String(point.originId ?? point.OriginId ?? ""),
monographUrl: String(point.monographUrl ?? point.MonographUrl ?? "")
}
};
const marker = createFiducialMarker(feature, L.latLng(latitude, longitude));
marker.feature = feature;
bindFiducialPopup(feature, marker);
lmeState.fiducials.addLayer(marker);
});
setFiducialPointsVisible(visible);
refreshLayerManager();
if (source.length) finishMapChange();
}
function createFiducialMarker(feature, latlng) {
const properties = feature?.properties || {};
const color = properties._lmeColor || lmeState.fiducialColor;
return L.circleMarker(latlng, {
pane: "fiducialsPane",
radius: 8,
color,
fillColor: properties._lmeFillColor || color,
weight: Number(properties._lmeWeight) || 3,
opacity: Number.isFinite(Number(properties._lmeOpacity)) ? Number(properties._lmeOpacity) : 1,
fillOpacity: Number.isFinite(Number(properties._lmeFillOpacity)) ? Number(properties._lmeFillOpacity) : 0.72
});
}
function bindFiducialPopup(feature, layer) {
const properties = feature?.properties || {};
const rows = [];
if (properties.cadastralNorth != null) rows.push(`Nord/X catastale: ${escapeHtml(properties.cadastralNorth)}`);
if (properties.cadastralEast != null) rows.push(`Est/Y catastale: ${escapeHtml(properties.cadastralEast)}`);
if (properties.reliability) rows.push(`Attendibilita: ${escapeHtml(properties.reliability)}`);
if (properties.elevation != null) rows.push(`Quota: ${escapeHtml(properties.elevation)}`);
if (properties.parcel) rows.push(`Particella: ${escapeHtml(properties.parcel)}`);
const url = String(properties.monographUrl || "");
const monograph = /^https:\/\//i.test(url)
? `<p><a href="${escapeHtml(url)}" target="_blank" rel="noopener">Apri monografia ufficiale</a></p>`
: "";
layer.bindPopup(`<strong>${escapeHtml(properties._lmeDisplayName || properties.name || "Punto fiduciale")}</strong><p>${rows.join("<br>")}</p>${monograph}<small>Posizione cartografica sperimentale GloboGIS: verificare prima dell'uso topografico.</small>`);
applyStyleFromProperties(layer);
}
function setFiducialPointsVisible(enabled) {
lmeState.fiducialsVisible = Boolean(enabled);
if (!lmeState.map || !lmeState.fiducials) return;
if (lmeState.fiducialsVisible) {
lmeState.managerHiddenCategories.delete("fiducials");
if (!lmeState.map.hasLayer(lmeState.fiducials)) lmeState.fiducials.addTo(lmeState.map);
lmeState.fiducials.bringToFront?.();
} else {
lmeState.managerHiddenCategories.add("fiducials");
if (lmeState.map.hasLayer(lmeState.fiducials)) lmeState.map.removeLayer(lmeState.fiducials);
}
refreshLayerManager();
}
function setFiducialColor(color) {
if (!/^#[0-9a-f]{6}$/i.test(String(color))) return;
lmeState.fiducialColor = color;
lmeState.fiducials?.eachLayer(layer => {
if (layer.feature?.properties) {
layer.feature.properties._lmeColor = color;
layer.feature.properties._lmeFillColor = color;
}
applyStyleFromProperties(layer);
});
refreshLayerManager();
}
function fitFiducialPoints() {
if (!lmeState.fiducials?.getLayers().length) {
setInfo("Nessun Punto Fiduciale da inquadrare.");
return;
}
fitLayer(lmeState.fiducials);
}
function clearSelectedParcels() { function clearSelectedParcels() {
const count = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length; const count = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length;
if (count) rememberMapChange(); if (count) rememberMapChange();
@@ -5513,6 +5936,7 @@ function setSelectedParcelsVisible(enabled) {
function clearMapElements() { function clearMapElements() {
const count = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length const count = managedGroupGeoJson(lmeState.selectedParcelsLayer, "selectedParcels").features.length
+ managedGroupGeoJson(lmeState.gpsParcelLayer, "gpsParcels").features.length + managedGroupGeoJson(lmeState.gpsParcelLayer, "gpsParcels").features.length
+ managedGroupGeoJson(lmeState.fiducials, "fiducials").features.length
+ managedGroupGeoJson(lmeState.imported, "imported").features.length + managedGroupGeoJson(lmeState.imported, "imported").features.length
+ managedGroupGeoJson(lmeState.importedSheets, "importedSheets").features.length + managedGroupGeoJson(lmeState.importedSheets, "importedSheets").features.length
+ managedGroupGeoJson(lmeState.drawings, "drawings").features.length + managedGroupGeoJson(lmeState.drawings, "drawings").features.length
@@ -5521,7 +5945,7 @@ function clearMapElements() {
setInfo("La mappa non contiene elementi da pulire."); setInfo("La mappa non contiene elementi da pulire.");
return; return;
} }
if (!window.confirm("Rimuovere particelle evidenziate, file importati, disegni, POI e traccia GPS dalla mappa?")) { if (!window.confirm("Rimuovere particelle evidenziate, Punti Fiduciali, file importati, disegni, POI e traccia GPS dalla mappa?")) {
return; return;
} }
@@ -5529,6 +5953,7 @@ function clearMapElements() {
resetLayerManagerProjectState(); resetLayerManagerProjectState();
lmeState.selectedParcelsLayer.clearLayers(); lmeState.selectedParcelsLayer.clearLayers();
lmeState.gpsParcelLayer.clearLayers(); lmeState.gpsParcelLayer.clearLayers();
lmeState.fiducials.clearLayers();
lmeState.imported.clearLayers(); lmeState.imported.clearLayers();
lmeState.importedSheets.clearLayers(); lmeState.importedSheets.clearLayers();
lmeState.drawings.clearLayers(); lmeState.drawings.clearLayers();