Compare commits

...
12 changed files with 1619 additions and 28 deletions

No files matched your search

+473
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">
@@ -563,6 +673,17 @@
<input type="number" min="0" max="30" step="1" @bind="MapMessageTimeoutSeconds" @bind:after="ApplyMapMessageOptions" /> <input type="number" min="0" max="30" step="1" @bind="MapMessageTimeoutSeconds" @bind:after="ApplyMapMessageOptions" />
</label> </label>
<small class="field-help">Imposta 0 per lasciare i messaggi visibili fino alla chiusura manuale.</small> <small class="field-help">Imposta 0 per lasciare i messaggi visibili fino alla chiusura manuale.</small>
<label class="toggle-line project-save-option">
<input type="checkbox" @bind="ShowCompass" @bind:after="ApplyCompassOptions" />
<span>Mostra bussola sulla mappa</span>
</label>
<label>
Orientamento bussola
<select @bind="CompassMode" @bind:after="ApplyCompassOptions" disabled="@(!ShowCompass)">
<option value="fixed">Mappa: nord fisso in alto</option>
<option value="heading">Navigatore: direzione del dispositivo in alto</option>
</select>
</label>
</section> </section>
<section class="panel-section"> <section class="panel-section">
@@ -745,6 +866,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;
@@ -761,7 +888,12 @@
private bool GpsActive { get; set; } private bool GpsActive { get; set; }
private bool GpsOverlayCollapsed { get; set; } private bool GpsOverlayCollapsed { get; set; }
private bool ShowGpsPosition { get; set; } = true; private bool ShowGpsPosition { get; set; } = true;
private bool ShowCompass { get; set; }
private string CompassMode { get; set; } = "fixed";
private bool CompassSensorSubscribed { get; set; }
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 +953,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 +971,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 +983,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 +1195,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 +1278,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;
@@ -1430,6 +1802,15 @@
return InvokeAsync(StateHasChanged); return InvokeAsync(StateHasChanged);
} }
[JSInvokable]
public async Task SetCompassFromLayerManager(bool enabled)
{
ShowCompass = enabled;
SaveFlagPreferences();
await ConfigureCompassSensorAsync();
await InvokeAsync(StateHasChanged);
}
[JSInvokable] [JSInvokable]
public Task<string> GetOfficialCadastralSheetsNear(double latitude, double longitude) => public Task<string> GetOfficialCadastralSheetsNear(double latitude, double longitude) =>
OfficialCadastreService.GetSheetsNearAsync(latitude, longitude); OfficialCadastreService.GetSheetsNearAsync(latitude, longitude);
@@ -1940,6 +2321,8 @@
ShowDrawingMeasurements = options.ShowDrawingMeasurements; ShowDrawingMeasurements = options.ShowDrawingMeasurements;
DrawToolsVisible = options.DrawToolsVisible; DrawToolsVisible = options.DrawToolsVisible;
ShowGpsPosition = options.GpsPositionVisible; ShowGpsPosition = options.GpsPositionVisible;
ShowCompass = options.CompassVisible;
CompassMode = options.CompassMode is "heading" ? "heading" : "fixed";
ShowSelectedParcels = options.SelectedParcelsVisible; ShowSelectedParcels = options.SelectedParcelsVisible;
TrailEnabled = options.TrailEnabled; TrailEnabled = options.TrailEnabled;
@@ -1949,6 +2332,7 @@
layer.Enabled = activeLayers.Contains(layer.Key); layer.Enabled = activeLayers.Contains(layer.Key);
} }
SaveFlagPreferences(); SaveFlagPreferences();
_ = ConfigureCompassSensorAsync();
} }
private async Task DeleteProject() private async Task DeleteProject()
@@ -2153,6 +2537,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();
@@ -2359,7 +2744,12 @@
ShowDrawingMeasurements = GetFlagPreference("show_drawing_measurements", true); ShowDrawingMeasurements = GetFlagPreference("show_drawing_measurements", true);
DrawToolsVisible = GetFlagPreference("draw_tools_visible", false); DrawToolsVisible = GetFlagPreference("draw_tools_visible", false);
ShowGpsPosition = GetFlagPreference("show_gps_position", true); ShowGpsPosition = GetFlagPreference("show_gps_position", true);
ShowCompass = GetFlagPreference("show_compass", false);
CompassMode = Preferences.Default.Get($"{FlagPreferencePrefix}compass_mode", "fixed") is "heading" ? "heading" : "fixed";
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);
@@ -2399,7 +2789,12 @@
SetFlagPreference("show_drawing_measurements", ShowDrawingMeasurements); SetFlagPreference("show_drawing_measurements", ShowDrawingMeasurements);
SetFlagPreference("draw_tools_visible", DrawToolsVisible); SetFlagPreference("draw_tools_visible", DrawToolsVisible);
SetFlagPreference("show_gps_position", ShowGpsPosition); SetFlagPreference("show_gps_position", ShowGpsPosition);
SetFlagPreference("show_compass", ShowCompass);
Preferences.Default.Set($"{FlagPreferencePrefix}compass_mode", CompassMode);
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);
@@ -2436,7 +2831,11 @@
await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements); await JS.InvokeVoidAsync("lmeMap.setDrawingMeasurementsVisible", ShowDrawingMeasurements);
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.setCompassOptions", ShowCompass, CompassMode);
await ConfigureCompassSensorAsync();
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)
@@ -2452,6 +2851,68 @@
await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds); await JS.InvokeVoidAsync("lmeMap.setInfoTimeout", MapMessageTimeoutSeconds);
} }
private async Task ApplyCompassOptions()
{
CompassMode = CompassMode is "heading" ? "heading" : "fixed";
SaveFlagPreferences();
await JS.InvokeVoidAsync("lmeMap.setCompassOptions", ShowCompass, CompassMode);
await ConfigureCompassSensorAsync();
}
private Task ConfigureCompassSensorAsync()
{
var compass = Microsoft.Maui.Devices.Sensors.Compass.Default;
var shouldMonitor = ShowCompass && compass.IsSupported;
try
{
if (shouldMonitor && !CompassSensorSubscribed)
{
compass.ReadingChanged += OnCompassReadingChanged;
CompassSensorSubscribed = true;
compass.Start(Microsoft.Maui.Devices.Sensors.SensorSpeed.UI);
}
else if (!shouldMonitor && CompassSensorSubscribed)
{
compass.ReadingChanged -= OnCompassReadingChanged;
CompassSensorSubscribed = false;
if (compass.IsMonitoring)
{
compass.Stop();
}
}
}
catch
{
if (CompassSensorSubscribed)
{
compass.ReadingChanged -= OnCompassReadingChanged;
CompassSensorSubscribed = false;
}
}
return Task.CompletedTask;
}
private async void OnCompassReadingChanged(
object? sender,
Microsoft.Maui.Devices.Sensors.CompassChangedEventArgs args)
{
if (!MapReady || !ShowCompass)
{
return;
}
try
{
await InvokeAsync(() => JS.InvokeVoidAsync(
"lmeMap.setCompassHeading",
args.Reading.HeadingMagneticNorth).AsTask());
}
catch
{
}
}
private async Task ApplyAdvancedOptions() private async Task ApplyAdvancedOptions()
{ {
SaveFlagPreferences(); SaveFlagPreferences();
@@ -2526,6 +2987,8 @@
public bool ShowDrawingMeasurements { get; init; } = true; public bool ShowDrawingMeasurements { get; init; } = true;
public bool DrawToolsVisible { get; init; } public bool DrawToolsVisible { get; init; }
public bool GpsPositionVisible { get; init; } = true; public bool GpsPositionVisible { get; init; } = true;
public bool CompassVisible { get; init; }
public string CompassMode { get; init; } = "fixed";
public bool SelectedParcelsVisible { get; init; } = true; public bool SelectedParcelsVisible { get; init; } = true;
public bool TrailEnabled { get; init; } public bool TrailEnabled { get; init; }
} }
@@ -2555,6 +3018,16 @@
GpsLookupCancellation?.Dispose(); GpsLookupCancellation?.Dispose();
ProjectAutoSaveCancellation?.Cancel(); ProjectAutoSaveCancellation?.Cancel();
ProjectAutoSaveCancellation?.Dispose(); ProjectAutoSaveCancellation?.Dispose();
var compass = Microsoft.Maui.Devices.Sensors.Compass.Default;
if (CompassSensorSubscribed)
{
compass.ReadingChanged -= OnCompassReadingChanged;
if (compass.IsMonitoring)
{
compass.Stop();
}
CompassSensorSubscribed = false;
}
MapCallbackReference?.Dispose(); MapCallbackReference?.Dispose();
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
+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.11</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.18</ApplicationDisplayVersion>
<ApplicationVersion>111</ApplicationVersion> <ApplicationVersion>118</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>();
+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 = "");
+104
View File
@@ -0,0 +1,104 @@
param(
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $PSScriptRoot
$dotnet = Join-Path $projectRoot "artifacts\tooling\dotnet10\dotnet.exe"
$androidSdk = Join-Path $env:LOCALAPPDATA "Android\Sdk"
$documentsDirectory = Split-Path -Parent (Split-Path -Parent $projectRoot)
$privateDirectory = Join-Path $documentsDirectory "LMEMappeCatastali-Private\GooglePlay"
$keyStore = Join-Path $privateDirectory "lme-mappe-catastali-pro-upload.jks"
$recoveryFile = Join-Path $privateDirectory "UPLOAD-KEY-RECOVERY.txt"
$outputDirectory = Join-Path $projectRoot "artifacts\play-store"
$projectPath = Join-Path $projectRoot "LMEMappeCatastaliApp.csproj"
foreach ($requiredPath in @($dotnet, $androidSdk, $keyStore, $recoveryFile, $projectPath)) {
if (-not (Test-Path -LiteralPath $requiredPath)) {
throw "Componente richiesto non trovato: $requiredPath"
}
}
[xml]$projectXml = Get-Content -Raw -LiteralPath $projectPath
$propertyGroup = $projectXml.Project.PropertyGroup |
Where-Object { $_.ApplicationDisplayVersion -and $_.ApplicationVersion } |
Select-Object -First 1
$displayVersion = [string]$propertyGroup.ApplicationDisplayVersion
$versionCode = [string]$propertyGroup.ApplicationVersion
if ([string]::IsNullOrWhiteSpace($displayVersion) -or [string]::IsNullOrWhiteSpace($versionCode)) {
throw "Versione applicazione non trovata nel progetto."
}
$recovery = Get-Content -LiteralPath $recoveryFile
function Read-RecoveryValue([string]$label) {
$line = $recovery | Where-Object { $_ -like "${label}:*" } | Select-Object -First 1
if ([string]::IsNullOrWhiteSpace($line)) {
throw "Valore $label non trovato nel file di recupero."
}
return ($line -split ":", 2)[1].Trim()
}
$storePassword = Read-RecoveryValue "Store password"
$keyPassword = Read-RecoveryValue "Key password"
$env:LME_ANDROID_UPLOAD_STORE_PASS = $storePassword
$env:LME_ANDROID_UPLOAD_KEY_PASS = $keyPassword
$packages = [System.Collections.Generic.List[System.IO.FileInfo]]::new()
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
& $dotnet restore $projectPath `
-p:Configuration=$Configuration `
-p:TargetFrameworks=net10.0-android `
-p:AndroidSdkDirectory=$androidSdk `
-p:UseDefaultPublishRuntimeIdentifier=false
if ($LASTEXITCODE -ne 0) {
throw "Ripristino delle dipendenze Android non riuscito."
}
try {
foreach ($format in @("aab", "apk")) {
& $dotnet publish $projectPath `
-f net10.0-android `
-c $Configuration `
--no-restore `
-p:AndroidSdkDirectory=$androidSdk `
-p:UseDefaultPublishRuntimeIdentifier=false `
-p:AndroidPackageFormat=$format `
-p:AndroidKeyStore=true `
-p:AndroidSigningKeyStore=$keyStore `
-p:AndroidSigningKeyAlias=lme-upload `
-p:AndroidSigningStorePass=env:LME_ANDROID_UPLOAD_STORE_PASS `
-p:AndroidSigningKeyPass=env:LME_ANDROID_UPLOAD_KEY_PASS
if ($LASTEXITCODE -ne 0) {
throw "Pubblicazione Android $format non riuscita."
}
$publishDirectory = Join-Path $projectRoot "bin\$Configuration\net10.0-android\publish"
$package = Get-ChildItem -LiteralPath $publishDirectory -Filter "*-Signed.$format" -File |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($null -eq $package) {
throw "Nessun pacchetto $format firmato prodotto in $publishDirectory"
}
$destination = Join-Path $outputDirectory "LME-Mappe-Catastali-Pro-$displayVersion-$versionCode.$format"
Copy-Item -LiteralPath $package.FullName -Destination $destination -Force
$packages.Add((Get-Item -LiteralPath $destination))
}
}
finally {
Remove-Item Env:LME_ANDROID_UPLOAD_STORE_PASS -ErrorAction SilentlyContinue
Remove-Item Env:LME_ANDROID_UPLOAD_KEY_PASS -ErrorAction SilentlyContinue
$storePassword = $null
$keyPassword = $null
}
$hashLines = $packages | ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
"$($hash.Hash.ToLowerInvariant()) $($_.Name)"
}
$hashFile = Join-Path $outputDirectory "LME-Mappe-Catastali-Pro-$displayVersion-$versionCode-SHA256SUMS.txt"
$hashLines | Set-Content -LiteralPath $hashFile
$packages | Select-Object Name, Length, LastWriteTime
Get-Item -LiteralPath $hashFile | Select-Object Name, Length, LastWriteTime
+159
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;
} }
@@ -1638,6 +1674,105 @@ label {
line-height: 1; line-height: 1;
} }
.map-compass {
--compass-rotation: 0deg;
--compass-radius: -31px;
position: absolute;
top: 50%;
left: 12px;
z-index: 810;
width: 82px;
height: 82px;
transform: translateY(-50%);
border: 2px solid rgba(255, 255, 255, 0.92);
border-radius: 50%;
background: rgba(22, 31, 38, 0.86);
box-shadow: 0 2px 9px rgba(0, 0, 0, 0.3);
color: #fff;
cursor: grab;
pointer-events: auto;
touch-action: none;
user-select: none;
}
.map-compass.is-dragging {
cursor: grabbing;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.42);
}
.map-compass-rose {
position: absolute;
inset: 0;
}
.map-compass-direction {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 12px;
transform: translate(-50%, -50%) rotate(var(--direction-angle)) translateY(var(--compass-radius));
color: #f8fafc;
font-size: 9px;
font-weight: 800;
line-height: 1;
text-align: center;
}
.map-compass-direction > span {
display: block;
transform: rotate(var(--label-counter));
}
.map-compass-direction.is-intermediate {
color: #cbd5e1;
font-size: 7px;
}
.map-compass-direction.is-north {
color: #fca5a5;
font-size: 11px;
font-weight: 900;
}
.map-compass-needle {
position: absolute;
bottom: 50%;
left: 50%;
width: 4px;
height: 23px;
transform: translateX(-50%) rotate(var(--compass-rotation));
transform-origin: 50% 100%;
border-radius: 4px 4px 1px 1px;
background: #ef4444;
transition: transform 120ms linear;
}
.map-compass-needle::after {
position: absolute;
top: -5px;
left: 50%;
width: 0;
height: 0;
transform: translateX(-50%);
border-right: 5px solid transparent;
border-bottom: 7px solid #ef4444;
border-left: 5px solid transparent;
content: "";
}
.map-compass-center {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
transform: translate(-50%, -50%);
border: 2px solid #fff;
border-radius: 50%;
background: #17212b;
}
.wms-preview[hidden] { .wms-preview[hidden] {
display: none; display: none;
} }
@@ -1881,6 +2016,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;
@@ -1906,6 +2047,13 @@ footer {
white-space: normal; white-space: normal;
} }
.map-compass {
--compass-radius: -27px;
left: 8px;
width: 72px;
height: 72px;
}
.map-info-text { .map-info-text {
display: -webkit-box; display: -webkit-box;
overflow: hidden; overflow: hidden;
@@ -1936,6 +2084,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;
+1
View File
@@ -27,6 +27,7 @@
<script src="_framework/blazor.webview.js" autostart="false"></script> <script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="lib/leaflet/leaflet.js"></script> <script src="lib/leaflet/leaflet.js"></script>
<script src="lib/leaflet-rotate/leaflet-rotate.umd.min.js"></script>
<script src="lib/leaflet-draw/leaflet.draw.js"></script> <script src="lib/leaflet-draw/leaflet.draw.js"></script>
<script src="lib/googlemutant/Leaflet.GoogleMutant.js"></script> <script src="lib/googlemutant/Leaflet.GoogleMutant.js"></script>
<script src="lib/jszip/jszip.min.js"></script> <script src="lib/jszip/jszip.min.js"></script>
+551 -25
View File
@@ -5,12 +5,14 @@ const lmeState = {
currentBaseName: "satellite", currentBaseName: "satellite",
layerOrder: [ layerOrder: [
"gpsPosition", "gpsPosition",
"drawings",
"gpsTrail", "gpsTrail",
"fiducials",
"drawings",
"gpsParcels", "gpsParcels",
"selectedParcels", "selectedParcels",
"importedSheets", "importedSheets",
"imported", "imported",
"compass",
"cadastre" "cadastre"
], ],
cadastre: null, cadastre: null,
@@ -35,6 +37,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,
@@ -50,12 +53,23 @@ 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: [],
gpsTrailStartedAt: null, gpsTrailStartedAt: null,
gpsWatchId: null, gpsWatchId: null,
lastPosition: null, lastPosition: null,
compassLayer: null,
compassElement: null,
compassVisible: false,
compassMode: "fixed",
compassHeading: 0,
compassPosition: null,
compassDragCleanup: null,
compassResizeHandler: null,
infoTimeoutSeconds: 3, infoTimeoutSeconds: 3,
infoTimer: null, infoTimer: null,
fileImportBound: false, fileImportBound: false,
@@ -131,6 +145,10 @@ window.lmeMap = {
closeCadastrePreview, closeCadastrePreview,
clearSelectedParcels, clearSelectedParcels,
setSelectedParcelsVisible, setSelectedParcelsVisible,
setFiducialPoints,
setFiducialPointsVisible,
setFiducialColor,
fitFiducialPoints,
clearMapElements, clearMapElements,
fitImported, fitImported,
clearImported, clearImported,
@@ -176,6 +194,8 @@ window.lmeMap = {
getGpsTrailStats, getGpsTrailStats,
setNativeGpsPosition, setNativeGpsPosition,
setGpsPositionVisible, setGpsPositionVisible,
setCompassOptions,
setCompassHeading,
setGpsError, setGpsError,
saveGpsMeasurementPoint, saveGpsMeasurementPoint,
restoreLastTrashItem, restoreLastTrashItem,
@@ -217,6 +237,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) {
@@ -249,7 +270,13 @@ function init(dotNetReference) {
lmeState.map = L.map("map", { lmeState.map = L.map("map", {
zoomControl: false, zoomControl: false,
attributionControl: false attributionControl: false,
rotate: true,
bearing: 0,
dragRotate: false,
touchRotate: false,
shiftKeyRotate: false,
rotateControl: false
}).setView([41.9028, 12.4964], 17); }).setView([41.9028, 12.4964], 17);
L.control.zoom({ position: "bottomright" }).addTo(lmeState.map); L.control.zoom({ position: "bottomright" }).addTo(lmeState.map);
@@ -293,13 +320,19 @@ function init(dotNetReference) {
lmeState.baseLayers.offline = createOfflineTileLayer(); lmeState.baseLayers.offline = createOfflineTileLayer();
lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map); lmeState.currentBase = lmeState.baseLayers.satellite.addTo(lmeState.map);
lmeState.compassLayer = createCompassLayer();
restoreLayerOrder(); restoreLayerOrder();
Object.values(managedLayerPanes()).forEach(paneName => { Object.values(managedLayerPanes()).forEach(paneName => {
if (!lmeState.map.getPane(paneName)) { if (!lmeState.map.getPane(paneName)) {
lmeState.map.createPane(paneName); const noRotation = paneName === "gpsPositionPane";
lmeState.map.createPane(paneName, noRotation
? (lmeState.map._norotatePane || lmeState.map._mapPane)
: (lmeState.map._rotatePane || lmeState.map._mapPane));
} }
}); });
const managerHighlightPane = lmeState.map.createPane("managerHighlightPane"); lmeState.map.createPane("drawingsMarkerPane", lmeState.map._norotatePane || lmeState.map._mapPane);
lmeState.map.createPane("gpsAccuracyPane", lmeState.map._rotatePane || lmeState.map._mapPane);
const managerHighlightPane = lmeState.map.createPane("managerHighlightPane", lmeState.map._rotatePane || lmeState.map._mapPane);
managerHighlightPane.style.zIndex = "650"; managerHighlightPane.style.zIndex = "650";
lmeState.drawingsRenderer = L.canvas({ pane: "drawingsPane", tolerance: 14 }); lmeState.drawingsRenderer = L.canvas({ pane: "drawingsPane", tolerance: 14 });
setLayerOrder(lmeState.layerOrder); setLayerOrder(lmeState.layerOrder);
@@ -310,11 +343,12 @@ 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"); const navigationSheetPane = lmeState.map.createPane("navigationSheetPane", lmeState.map._rotatePane || lmeState.map._mapPane);
navigationSheetPane.style.zIndex = "465"; navigationSheetPane.style.zIndex = "465";
navigationSheetPane.style.pointerEvents = "none"; navigationSheetPane.style.pointerEvents = "none";
const cadastreZoningPane = lmeState.map.createPane("cadastreZoningPane"); const cadastreZoningPane = lmeState.map.createPane("cadastreZoningPane", lmeState.map._rotatePane || lmeState.map._mapPane);
cadastreZoningPane.style.zIndex = "455"; cadastreZoningPane.style.zIndex = "455";
cadastreZoningPane.style.pointerEvents = "none"; cadastreZoningPane.style.pointerEvents = "none";
lmeState.navigationSheetLayer = L.layerGroup().addTo(lmeState.map); lmeState.navigationSheetLayer = L.layerGroup().addTo(lmeState.map);
@@ -405,6 +439,199 @@ function init(dotNetReference) {
setInfo("Mappa pronta: satellite + catasto AdE"); setInfo("Mappa pronta: satellite + catasto AdE");
} }
function createCompassLayer() {
return new (L.Layer.extend({
onAdd(map) {
const element = L.DomUtil.create("div", "map-compass");
element.setAttribute("role", "img");
element.setAttribute("aria-label", "Bussola: nord");
element.setAttribute("title", "Trascina per spostare la bussola");
element.innerHTML = `<span class="map-compass-rose" aria-hidden="true">
<span class="map-compass-direction is-north" style="--direction-angle:0deg;--label-counter:0deg"><span>N</span></span>
<span class="map-compass-direction is-intermediate" style="--direction-angle:45deg;--label-counter:-45deg"><span>NE</span></span>
<span class="map-compass-direction" style="--direction-angle:90deg;--label-counter:-90deg"><span>E</span></span>
<span class="map-compass-direction is-intermediate" style="--direction-angle:135deg;--label-counter:-135deg"><span>SE</span></span>
<span class="map-compass-direction" style="--direction-angle:180deg;--label-counter:-180deg"><span>S</span></span>
<span class="map-compass-direction is-intermediate" style="--direction-angle:225deg;--label-counter:-225deg"><span>SW</span></span>
<span class="map-compass-direction" style="--direction-angle:270deg;--label-counter:-270deg"><span>W</span></span>
<span class="map-compass-direction is-intermediate" style="--direction-angle:315deg;--label-counter:-315deg"><span>NW</span></span>
<span class="map-compass-needle"></span>
</span><span class="map-compass-center" aria-hidden="true"></span>`;
L.DomEvent.disableClickPropagation(element);
L.DomEvent.disableScrollPropagation(element);
map.getContainer().appendChild(element);
lmeState.compassElement = element;
restoreCompassPosition();
bindCompassDragging(element);
lmeState.compassResizeHandler = () => applyCompassPosition();
map.on("resize", lmeState.compassResizeHandler);
requestAnimationFrame(() => applyCompassPosition());
updateCompassDisplay();
},
onRemove(map) {
lmeState.compassDragCleanup?.();
lmeState.compassDragCleanup = null;
if (lmeState.compassResizeHandler) {
map.off("resize", lmeState.compassResizeHandler);
lmeState.compassResizeHandler = null;
}
lmeState.compassElement?.remove();
lmeState.compassElement = null;
}
}))();
}
function restoreCompassPosition() {
if (lmeState.compassPosition) return;
try {
const saved = JSON.parse(localStorage.getItem("lmeCompassPosition") || "null");
const x = Number(saved?.x);
const y = Number(saved?.y);
if (Number.isFinite(x) && Number.isFinite(y)) {
lmeState.compassPosition = {
x: Math.max(0, Math.min(1, x)),
y: Math.max(0, Math.min(1, y))
};
}
} catch {
}
}
function applyCompassPosition() {
const element = lmeState.compassElement;
const container = lmeState.map?.getContainer();
if (!element || !container) return;
const width = container.clientWidth;
const height = container.clientHeight;
const elementWidth = element.offsetWidth;
const elementHeight = element.offsetHeight;
if (!width || !height || !elementWidth || !elementHeight) return;
const halfWidth = elementWidth / 2;
const halfHeight = elementHeight / 2;
const position = lmeState.compassPosition || {
x: (12 + halfWidth) / width,
y: 0.5
};
const centerX = Math.max(halfWidth + 4, Math.min(width - halfWidth - 4, position.x * width));
const centerY = Math.max(halfHeight + 4, Math.min(height - halfHeight - 4, position.y * height));
element.style.left = `${centerX - halfWidth}px`;
element.style.top = `${centerY - halfHeight}px`;
element.style.transform = "none";
}
function bindCompassDragging(element) {
let pointerId = null;
let offsetX = 0;
let offsetY = 0;
const move = event => {
if (event.pointerId !== pointerId) return;
event.preventDefault();
event.stopPropagation();
const container = lmeState.map?.getContainer();
if (!container) return;
const containerRect = container.getBoundingClientRect();
const elementWidth = element.offsetWidth;
const elementHeight = element.offsetHeight;
const left = event.clientX - containerRect.left - offsetX;
const top = event.clientY - containerRect.top - offsetY;
const centerX = Math.max(elementWidth / 2 + 4,
Math.min(containerRect.width - elementWidth / 2 - 4, left + elementWidth / 2));
const centerY = Math.max(elementHeight / 2 + 4,
Math.min(containerRect.height - elementHeight / 2 - 4, top + elementHeight / 2));
lmeState.compassPosition = {
x: centerX / containerRect.width,
y: centerY / containerRect.height
};
applyCompassPosition();
};
const finish = event => {
if (event.pointerId !== pointerId) return;
event.preventDefault();
event.stopPropagation();
element.classList.remove("is-dragging");
if (element.hasPointerCapture?.(pointerId)) {
element.releasePointerCapture(pointerId);
}
pointerId = null;
try {
localStorage.setItem("lmeCompassPosition", JSON.stringify(lmeState.compassPosition));
} catch {
}
};
const start = event => {
if (event.pointerType === "mouse" && event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
const rect = element.getBoundingClientRect();
pointerId = event.pointerId;
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
element.classList.add("is-dragging");
element.setPointerCapture?.(pointerId);
};
element.addEventListener("pointerdown", start);
element.addEventListener("pointermove", move);
element.addEventListener("pointerup", finish);
element.addEventListener("pointercancel", finish);
lmeState.compassDragCleanup = () => {
element.removeEventListener("pointerdown", start);
element.removeEventListener("pointermove", move);
element.removeEventListener("pointerup", finish);
element.removeEventListener("pointercancel", finish);
};
}
function setCompassOptions(visible, mode = "fixed") {
lmeState.compassVisible = Boolean(visible);
lmeState.compassMode = mode === "heading" ? "heading" : "fixed";
if (!lmeState.map || !lmeState.compassLayer) return;
if (lmeState.compassVisible) {
lmeState.managerHiddenCategories.delete("compass");
if (!lmeState.map.hasLayer(lmeState.compassLayer)) {
lmeState.compassLayer.addTo(lmeState.map);
}
} else {
lmeState.managerHiddenCategories.add("compass");
if (lmeState.map.hasLayer(lmeState.compassLayer)) {
lmeState.map.removeLayer(lmeState.compassLayer);
}
}
if (!lmeState.compassVisible || lmeState.compassMode === "fixed") {
lmeState.map.setHeading?.(null);
lmeState.map.setBearing?.(0);
} else if (Number.isFinite(lmeState.compassHeading)) {
lmeState.map.setHeading?.(lmeState.compassHeading, { ease: 0.2, deadzone: 1 });
}
updateCompassDisplay();
refreshLayerManager();
}
function setCompassHeading(heading) {
const parsed = Number(heading);
if (!Number.isFinite(parsed)) return;
lmeState.compassHeading = ((parsed % 360) + 360) % 360;
if (lmeState.compassVisible && lmeState.compassMode === "heading") {
lmeState.map?.setHeading?.(lmeState.compassHeading, { ease: 0.2, deadzone: 1 });
}
updateCompassDisplay();
}
function updateCompassDisplay() {
const element = lmeState.compassElement;
if (!element) return;
const rotation = -lmeState.compassHeading;
element.classList.toggle("is-fixed", lmeState.compassMode === "fixed");
element.style.setProperty("--compass-rotation", `${rotation}deg`);
element.setAttribute("aria-label", lmeState.compassMode === "fixed"
? `Bussola: mappa con nord fisso, orientamento ${Math.round(lmeState.compassHeading)} gradi`
: `Bussola: nord, orientamento ${Math.round(lmeState.compassHeading)} gradi`);
}
function showSearchResult(latitude, longitude, label) { function showSearchResult(latitude, longitude, label) {
const lat = Number(latitude); const lat = Number(latitude);
const lon = Number(longitude); const lon = Number(longitude);
@@ -1339,8 +1566,10 @@ function managedLayerPanes() {
gpsTrail: "gpsTrailPane", gpsTrail: "gpsTrailPane",
gpsParcels: "gpsParcelsPane", gpsParcels: "gpsParcelsPane",
selectedParcels: "selectedParcelsPane", selectedParcels: "selectedParcelsPane",
fiducials: "fiducialsPane",
importedSheets: "importedSheetPane", importedSheets: "importedSheetPane",
imported: "importedPane", imported: "importedPane",
compass: "compassPane",
cadastre: "cadastrePane" cadastre: "cadastrePane"
}; };
} }
@@ -1354,10 +1583,14 @@ function setLayerOrder(order) {
return; return;
} }
normalized.forEach((key, index) => { normalized.forEach((key, index) => {
const pane = lmeState.map.getPane(panes[key]); const zIndex = String(590 - index * 20);
if (pane) { const paneNames = [panes[key]];
pane.style.zIndex = String(590 - index * 20); if (key === "drawings") paneNames.push("drawingsMarkerPane");
} if (key === "gpsPosition") paneNames.push("gpsAccuracyPane");
paneNames.forEach(paneName => {
const pane = lmeState.map.getPane(paneName);
if (pane) pane.style.zIndex = zIndex;
});
}); });
try { try {
localStorage.setItem("lmeMapLayerOrder", JSON.stringify(normalized)); localStorage.setItem("lmeMapLayerOrder", JSON.stringify(normalized));
@@ -1369,7 +1602,12 @@ 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));
if (!remaining.includes("compass")) {
const cadastreIndex = remaining.indexOf("cadastre");
remaining.splice(cadastreIndex >= 0 ? cadastreIndex : remaining.length, 0, "compass");
}
lmeState.layerOrder = ["gpsPosition", "gpsTrail", "fiducials", ...remaining];
} }
} catch { } catch {
} }
@@ -1427,8 +1665,10 @@ 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: "compass", label: "Bussola", roots: [lmeState.compassLayer].filter(Boolean), uiOnly: true },
{ key: "cadastre", label: "Layer catastali AdE", roots: [lmeState.cadastre].filter(Boolean) } { key: "cadastre", label: "Layer catastali AdE", roots: [lmeState.cadastre].filter(Boolean) }
]; ];
} }
@@ -1466,6 +1706,9 @@ function collectManagedLeafLayers(layer, categoryKey, parent, records) {
} }
function managedRecordsForCategory(category) { function managedRecordsForCategory(category) {
if (category.uiOnly) {
return [];
}
const records = []; const records = [];
category.roots.forEach(root => collectManagedLeafLayers(root, category.key, lmeState.map, records)); category.roots.forEach(root => collectManagedLeafLayers(root, category.key, lmeState.map, records));
lmeState.managerHiddenObjects.forEach(record => { lmeState.managerHiddenObjects.forEach(record => {
@@ -1614,11 +1857,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) {
@@ -1880,6 +2123,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;
@@ -2015,8 +2259,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>`;
@@ -2037,12 +2285,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}'`);
@@ -2693,7 +2941,7 @@ function addDrawingFeature(feature, select = false) {
style: storedFeatureStyle, style: storedFeatureStyle,
pointToLayer: (item, latlng) => item?.properties?._lmeShape === "circle" && Number(item.properties._lmeRadius) > 0 pointToLayer: (item, latlng) => item?.properties?._lmeShape === "circle" && Number(item.properties._lmeRadius) > 0
? L.circle(latlng, { pane: "drawingsPane", renderer: lmeState.drawingsRenderer, radius: Number(item.properties._lmeRadius) }) ? L.circle(latlng, { pane: "drawingsPane", renderer: lmeState.drawingsRenderer, radius: Number(item.properties._lmeRadius) })
: L.marker(latlng, { pane: "drawingsPane", draggable: false }), : L.marker(latlng, { pane: "drawingsMarkerPane", draggable: false }),
onEachFeature: (item, layer) => { onEachFeature: (item, layer) => {
annotateDrawingLayer(layer, item?.properties?._lmeShape); annotateDrawingLayer(layer, item?.properties?._lmeShape);
applyStyleFromProperties(layer); applyStyleFromProperties(layer);
@@ -2932,6 +3180,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>`);
@@ -2947,6 +3196,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 => {
@@ -2956,6 +3206,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);
@@ -2963,6 +3217,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) {
@@ -3075,7 +3411,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;
@@ -3084,6 +3421,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();
@@ -3271,8 +3614,17 @@ 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;
} else if (category.key === "compass") {
lmeState.compassVisible = visible;
void lmeState.dotNetReference?.invokeMethodAsync("SetCompassFromLayerManager", visible);
if (!visible) {
lmeState.map.setHeading?.(null);
lmeState.map.setBearing?.(0);
}
} }
if (visible) { if (visible) {
lmeState.managerHiddenCategories.delete(category.key); lmeState.managerHiddenCategories.delete(category.key);
@@ -3344,6 +3696,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;
} }
@@ -3873,7 +4228,7 @@ function addProjectMarker(name) {
rememberMapChange(); rememberMapChange();
const center = lmeState.map.getCenter(); const center = lmeState.map.getCenter();
const label = (name || "Nuovo POI").trim(); const label = (name || "Nuovo POI").trim();
const marker = L.marker(center, { pane: "drawingsPane", draggable: false }).addTo(lmeState.drawings); const marker = L.marker(center, { pane: "drawingsMarkerPane", draggable: false }).addTo(lmeState.drawings);
marker.lmeManagerLabel = `POI: ${label}`; marker.lmeManagerLabel = `POI: ${label}`;
marker.feature = { marker.feature = {
type: "Feature", type: "Feature",
@@ -4578,7 +4933,7 @@ function addMeasurementLabel(targetGroup, latLng, html, labelType, context) {
return; return;
} }
L.marker(latLng, { L.marker(latLng, {
pane: context.sourceType === "drawing" ? "drawingsPane" : "importedPane", pane: "tooltipPane",
interactive: false, interactive: false,
keyboard: false, keyboard: false,
icon: L.divIcon({ icon: L.divIcon({
@@ -4721,7 +5076,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: 7,
view: { view: {
latitude: center.lat, latitude: center.lat,
longitude: center.lng, longitude: center.lng,
@@ -4731,6 +5086,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")
@@ -4766,6 +5122,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(), {
@@ -4784,7 +5141,7 @@ function loadProjectSnapshot(raw, projectName) {
fillOpacity: 0.2 fillOpacity: 0.2
}); });
} }
return L.marker(latlng, { pane: "drawingsPane", draggable: false }); return L.marker(latlng, { pane: "drawingsMarkerPane", draggable: false });
}, },
onEachFeature: (feature, layer) => { onEachFeature: (feature, layer) => {
bindStoredFeaturePopup(feature, layer); bindStoredFeaturePopup(feature, layer);
@@ -4826,6 +5183,14 @@ function loadProjectSnapshot(raw, projectName) {
applyStyleFromProperties(layer); applyStyleFromProperties(layer);
} }
}).addTo(lmeState.selectedParcelsLayer); }).addTo(lmeState.selectedParcelsLayer);
const restoredFiducials = L.geoJSON(data.fiducials || emptyFeatureCollection(), {
pointToLayer: (feature, latlng) => createFiducialMarker(feature, latlng),
onEachFeature: (feature, layer) => {
bindFiducialPopup(feature, layer);
bindManagedMapSelection(layer, "fiducials", lmeState.fiducials);
}
});
restoredFiducials.eachLayer(layer => lmeState.fiducials.addLayer(layer));
if (lmeState.importedSheetsVisible) { if (lmeState.importedSheetsVisible) {
void ensureImportedSheetOutlines().then(() => { void ensureImportedSheetOutlines().then(() => {
applyImportedSheetVisibility(); applyImportedSheetVisibility();
@@ -4848,6 +5213,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) {
@@ -4897,7 +5263,11 @@ function captureDisplayOptions() {
showDrawingMeasurements: lmeState.showDrawingMeasurements, showDrawingMeasurements: lmeState.showDrawingMeasurements,
drawToolsVisible: Boolean(lmeState.drawControl), drawToolsVisible: Boolean(lmeState.drawControl),
gpsPositionVisible: lmeState.gpsPositionVisible, gpsPositionVisible: lmeState.gpsPositionVisible,
compassVisible: lmeState.compassVisible,
compassMode: lmeState.compassMode,
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 },
@@ -4964,7 +5334,14 @@ function applyProjectDisplayOptions(options) {
setBaseLayer(options.baseLayer); setBaseLayer(options.baseLayer);
} }
setGpsPositionVisible(options.gpsPositionVisible !== false); setGpsPositionVisible(options.gpsPositionVisible !== false);
const compassVisible = Boolean(options.compassVisible)
&& !lmeState.managerHiddenCategories.has("compass");
setCompassOptions(compassVisible, options.compassMode);
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();
@@ -5013,6 +5390,19 @@ function emptyFeatureCollection() {
function managedGroupGeoJson(group, categoryKey) { function managedGroupGeoJson(group, categoryKey) {
const collection = group?.toGeoJSON?.() || emptyFeatureCollection(); const collection = group?.toGeoJSON?.() || emptyFeatureCollection();
const features = [...(collection.features || [])]; const features = [...(collection.features || [])];
const knownIds = new Set(features.map(feature => feature?.properties?._lmeManagerId).filter(Boolean));
lmeState.managerHiddenObjects.forEach(record => {
if (record.categoryKey !== categoryKey || typeof record.layer?.toGeoJSON !== "function") return;
const stored = record.layer.toGeoJSON();
const hiddenFeatures = stored?.type === "FeatureCollection" ? stored.features : [stored];
hiddenFeatures.filter(feature => feature?.type === "Feature").forEach(feature => {
const id = feature?.properties?._lmeManagerId;
if (!id || !knownIds.has(id)) {
features.push(feature);
if (id) knownIds.add(id);
}
});
});
const categoryName = lmeState.managerCategoryNames[categoryKey]; const categoryName = lmeState.managerCategoryNames[categoryKey];
if (categoryName) { if (categoryName) {
features.forEach(feature => { features.forEach(feature => {
@@ -5634,6 +6024,140 @@ 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;
const category = layerManagerCategories().find(item => item.key === "fiducials");
managedRecordsForCategory(category).forEach(record => {
const layer = record.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();
@@ -5672,6 +6196,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
@@ -5680,7 +6205,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;
} }
@@ -5688,6 +6213,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();
@@ -5836,7 +6362,7 @@ function updateGpsPositionLayers(latlng, accuracy) {
lmeState.gpsMarker.lmeManagerId = "gps-position-marker"; lmeState.gpsMarker.lmeManagerId = "gps-position-marker";
lmeState.gpsMarker.lmeManagerLabel = "Posizione GPS"; lmeState.gpsMarker.lmeManagerLabel = "Posizione GPS";
lmeState.gpsAccuracy = L.circle(latlng, { lmeState.gpsAccuracy = L.circle(latlng, {
pane: "gpsPositionPane", pane: "gpsAccuracyPane",
radius: accuracy, radius: accuracy,
color: "#2563eb", color: "#2563eb",
weight: 2, weight: 2,
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Grzegorz Tomicki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2
View File
@@ -0,0 +1,2 @@
/*! @tomickigrzegorz/leaflet-rotate v0.3.0 | MIT */
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(require("leaflet")):"function"==typeof define&&define.amd?define(["leaflet"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).L)}(this,function(t){"use strict";t.Point.prototype.rotate=function(i){var e=Math.cos(i),o=Math.sin(i);return new t.Point(this.x*e-this.y*o,this.x*o+this.y*e)},t.Point.prototype.rotateFrom=function(t,i){return i?this.subtract(i).rotate(t).add(i):this.rotate(t)};const i=Math.PI/180,e=180/Math.PI;function o(t){return(t%360+360)%360}function a(t){return o(t+180)-180}function n(t){var i=2*Math.PI;return((t+Math.PI)%i+i)%i-Math.PI}const s=1e3/60;function r(t,i){return 1-Math.pow(1-t,i/s)}function h(){return"undefined"!=typeof performance&&performance.now?performance.now():Date.now()}var _=t.Map.prototype;t.Map.mergeOptions({rotate:!1,bearing:0,touchRotate:!1,shiftKeyRotate:!1,dragRotate:!0,rotateControl:!1,rotateClockwise:!0});var p=_.initialize;_.initialize=function(t,i){i&&i.rotate&&(this._rotate=!0,this._bearing=0,this._bearingRad=0),p.call(this,t,i),this._rotate&&this.setBearing(i.bearing||0)};var l=_._initPanes;_._initPanes=function(){if(l.call(this),this._rotate){var i=this._mapPane;this._rotatePane=t.DomUtil.create("div","leaflet-rotate-pane",i),this._norotatePane=t.DomUtil.create("div","leaflet-norotate-pane",i),this._rotatePane.appendChild(this._panes.tilePane),this._rotatePane.appendChild(this._panes.overlayPane),this._norotatePane.appendChild(this._panes.shadowPane),this._norotatePane.appendChild(this._panes.markerPane),this._norotatePane.appendChild(this._panes.tooltipPane),this._norotatePane.appendChild(this._panes.popupPane),t.DomUtil.addClass(this._rotatePane,"leaflet-proxy leaflet-zoom-animated")}},_.setBearing=function(e){if(!this._rotate||!isFinite(e))return this;var a=this._bearing||0,n=o(e);if(n===a)return this;if(this._commitRotatePan(),this._bearing=n,this._bearingRad=n*i,this._updateRotatePaneTransform(),0===a!=(0===n))for(var s in this._layers){var r=this._layers[s];r instanceof t.Renderer&&r._update()}return this.fire("rotate")},_.getBearing=function(){return this._bearing||0},_._updateRotatePaneTransform=function(){if(this._rotatePane){if(!this._bearing)return this._rotatePane.style[t.DomUtil.TRANSFORM]="",void(this._rotatePane.style[t.DomUtil.TRANSFORM+"Origin"]="");var i=this.getSize().divideBy(2);this._rotatePane.style[t.DomUtil.TRANSFORM+"Origin"]=i.x+"px "+i.y+"px",this._rotatePane.style[t.DomUtil.TRANSFORM]="rotate("+this._bearingRad+"rad)"}},_._commitRotatePan=function(){if(this._rotate&&!this._committingRotatePan){var t=this._getMapPanePos();!t||0===t.x&&0===t.y||(this._committingRotatePan=!0,this._resetView(this.getCenter(),this.getZoom(),!0),this._committingRotatePan=!1)}};var m=_.containerPointToLayerPoint;_.containerPointToLayerPoint=function(i){if(!this._rotate||!this._bearing)return m.call(this,i);var e=t.point(i),o=this._getMapPanePos(),a=this.getSize().divideBy(2);return e.subtract(o).subtract(a).rotate(-this._bearingRad).add(a)};var d=_.layerPointToContainerPoint;_.layerPointToContainerPoint=function(i){if(!this._rotate||!this._bearing)return d.call(this,i);var e=t.point(i),o=this._getMapPanePos(),a=this.getSize().divideBy(2);return e.subtract(a).rotate(this._bearingRad).add(a).add(o)},_.rotatedPointToMapPanePoint=function(i){if(!this._bearing)return t.point(i);var e=this.getSize().divideBy(2);return t.point(i).rotateFrom(this._bearingRad,e)},_.mapPanePointToRotatedPoint=function(i){if(!this._bearing)return t.point(i);var e=this.getSize().divideBy(2);return t.point(i).rotateFrom(-this._bearingRad,e)};var c=_._getCenterOffset;_._getCenterOffset=function(t){return this._rotate&&this._bearing?this.project(t).subtract(this.project(this.getCenter())).rotate(this._bearingRad):c.call(this,t)};var u=_.getBounds;_.getBounds=function(){if(!this._rotate||!this._bearing)return u.call(this);var i=this.getSize(),e=t.latLngBounds();return e.extend(this.containerPointToLatLng(t.point(0,0))),e.extend(this.containerPointToLatLng(t.point(i.x,0))),e.extend(this.containerPointToLatLng(t.point(i.x,i.y))),e.extend(this.containerPointToLatLng(t.point(0,i.y))),e},_.mapBoundsToContainerBounds=function(i){return t.bounds([this.latLngToContainerPoint(i.getNorthWest()),this.latLngToContainerPoint(i.getNorthEast()),this.latLngToContainerPoint(i.getSouthEast()),this.latLngToContainerPoint(i.getSouthWest())])};var g=_.getBoundsZoom;_.getBoundsZoom=function(i,e,o){if(!this._rotate||!this._bearing)return g.call(this,i,e,o);i=t.latLngBounds(i),o=t.point(o||[0,0]);var a=this.getZoom()||0,n=this.getMinZoom(),s=this.getMaxZoom(),r=this.getSize().subtract(o);if(r.x<=0||r.y<=0)return a;var h=this.mapBoundsToContainerBounds(i).getSize(),_=this.options.zoomSnap,p=r.x/h.x,l=r.y/h.y,m=e?Math.max(p,l):Math.min(p,l);return a=this.getScaleZoom(m,a),_&&(a=Math.round(a/_)*_),Math.max(n,Math.min(s,a))},_._animateZoomNoDelay=function(t,i,e){this._mapPane&&(e&&(this._animatingZoom=!0,this._animateToCenter=t,this._animLine truncated