Compare commits

...
10 Commits
15 changed files with 1821 additions and 46 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.10</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0.18</ApplicationDisplayVersion>
<ApplicationVersion>110</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>();
+1 -1
View File
@@ -4,7 +4,7 @@
- Nome: LME Mappe Catastali Pro. - Nome: LME Mappe Catastali Pro.
- Package name Android: `it.liviomerola.lmemappecatastali.pro`. - Package name Android: `it.liviomerola.lmemappecatastali.pro`.
- Versione corrente da pubblicare: `1.0.10` / version code `110`. - Versione corrente da pubblicare: `1.0.11` / version code `111`.
- Formato Release Android: Android App Bundle (`.aab`). - Formato Release Android: Android App Bundle (`.aab`).
- Target SDK Release: Android 16 / API 36. - Target SDK Release: Android 16 / API 36.
+5 -1
View File
@@ -102,7 +102,11 @@ public class MainActivity : MauiAppCompatActivity
{ {
if (view is null || insets is null) return insets; if (view is null || insets is null) return insets;
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars()); var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
view.SetPadding(_left, _top, _right, systemBars?.Bottom ?? 0); view.SetPadding(
_left,
systemBars?.Top ?? _top,
_right,
systemBars?.Bottom ?? 0);
return insets; return insets;
} }
} }
+6
View File
@@ -4,6 +4,12 @@ App .NET MAUI Blazor Hybrid per Android, iOS, Mac Catalyst e Windows, pensata pe
Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md). Informativa privacy: [PRIVACY_POLICY.md](PRIVACY_POLICY.md).
## Versione Pro 1.0.11 test
- Il flag Fogli catastali mostra il WMS di tutti i fogli agli zoom panoramici e, in dettaglio, il confine ufficiale WFS del foglio corrente anche senza dati caricati.
- Se il foglio corrente e gia presente in un progetto o in un'importazione, l'overlay di navigazione non ne crea un duplicato.
- La toolbar superiore rispetta l'area occupata dalla barra di stato e dalle notifiche Android.
## Versione Pro 1.0.10 test ## Versione Pro 1.0.10 test
- Migliora la compatibilita del servizio catastale AdE sui dispositivi Android meno recenti con HTTP/1.1, tentativi automatici e una ricerca GPS piu tollerante. - Migliora la compatibilita del servizio catastale AdE sui dispositivi Android meno recenti con HTTP/1.1, tentativi automatici e una ricerca GPS piu tollerante.
+8
View File
@@ -40,6 +40,14 @@ public sealed class CadastralCatalogService
.ToArray(); .ToArray();
} }
public async Task<MunicipalityOption?> FindMunicipalityAsync(string municipalityCode)
{
var code = (municipalityCode ?? string.Empty).Trim();
var municipalities = await GetAllMunicipalitiesAsync();
return municipalities.FirstOrDefault(item =>
string.Equals(item.Code, code, StringComparison.OrdinalIgnoreCase));
}
public async Task<IReadOnlyList<string>> GetSheetsAsync( public async Task<IReadOnlyList<string>> GetSheetsAsync(
MunicipalityOption municipality, MunicipalityOption municipality,
IProgress<CatalogProgress>? progress = null, IProgress<CatalogProgress>? progress = null,
+3 -1
View File
@@ -21,7 +21,8 @@ public sealed class EntitlementService(
AppFeatures.CloudSync, AppFeatures.CloudSync,
AppFeatures.OfflineMaps, AppFeatures.OfflineMaps,
AppFeatures.AdvancedGeometry, AppFeatures.AdvancedGeometry,
AppFeatures.GpsTrail AppFeatures.GpsTrail,
AppFeatures.FiducialPoints
}; };
public bool IsGoogleProActive { get; private set; } public bool IsGoogleProActive { get; private set; }
@@ -185,4 +186,5 @@ public static class AppFeatures
public const string OfflineMaps = "offline-maps"; public const string OfflineMaps = "offline-maps";
public const string AdvancedGeometry = "advanced-geometry"; public const string AdvancedGeometry = "advanced-geometry";
public const string GpsTrail = "gps-trail"; public const string GpsTrail = "gps-trail";
public const string FiducialPoints = "fiducial-points";
} }
+292
View File
@@ -0,0 +1,292 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace LMEMappeCatastaliApp.Services;
public sealed partial class FiducialPointService
{
private const string ServiceBaseUrl = "https://www1.agenziaentrate.gov.it/servizi/Monografie/";
private const string MappedPointsUrl = "https://cartografia.globogis.it/arcgis/rest/services/fiduciali/Punti_fiduciali/MapServer/2/query";
private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) };
public FiducialPointService()
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)");
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetProvincesAsync(
CancellationToken cancellationToken = default)
{
var html = await GetHtmlAsync("ricerca.php", cancellationToken);
return ParseOptions(html, "provincia");
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetMunicipalitiesAsync(
string provinceCode,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(provinceCode);
var html = await GetHtmlAsync($"ricerca.php?level=1&prov={Uri.EscapeDataString(code)}", cancellationToken);
return ParseOptions(html, "co");
}
public async Task<IReadOnlyList<FiducialAreaOption>> GetSheetsAsync(
string provinceCode,
string municipalityCode,
CancellationToken cancellationToken = default)
{
var province = NormalizeCode(provinceCode);
var municipality = NormalizeCode(municipalityCode);
var path = $"ricerca.php?level=2&prov={Uri.EscapeDataString(province)}";
var html = await PostFormAsync(path, new Dictionary<string, string> { ["co"] = municipality }, cancellationToken);
return ParseOptions(html, "foglio");
}
public async Task<IReadOnlyList<FiducialPointEntry>> GetFiducialsAsync(
string provinceName,
string municipalityName,
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(municipalityCode);
var normalizedSheet = sheet.Trim();
var query = string.Join("&",
[
$"provincia={Uri.EscapeDataString(provinceName.Trim())}",
$"comune={Uri.EscapeDataString(municipalityName.Trim())}",
$"co={Uri.EscapeDataString(code)}"
]);
var html = await PostFormAsync(
$"risultato.php?{query}",
new Dictionary<string, string> { ["foglio"] = normalizedSheet },
cancellationToken);
return ParseFiducialLinks(html);
}
public async Task<IReadOnlyList<FiducialPointEntry>> GetFiducialsAsync(
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var code = NormalizeCode(municipalityCode);
var normalizedSheet = NormalizeSheet(sheet);
var html = await GetHtmlAsync(
$"risultato.php?co={Uri.EscapeDataString(code)}&foglio={Uri.EscapeDataString(normalizedSheet)}",
cancellationToken);
return ParseFiducialLinks(html);
}
private static IReadOnlyList<FiducialPointEntry> ParseFiducialLinks(string html) =>
DownloadLinkRegex().Matches(html)
.Select(match =>
{
var relativeUrl = WebUtility.HtmlDecode(match.Groups["url"].Value);
var fileName = WebUtility.HtmlDecode(match.Groups["name"].Value);
return new FiducialPointEntry(
FormatIdentifier(fileName),
new Uri(new Uri(ServiceBaseUrl), relativeUrl).AbsoluteUri);
})
.DistinctBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray();
public async Task<IReadOnlyList<MappedFiducialPoint>> GetMappedFiducialsAsync(
string municipalityCode,
string sheet,
CancellationToken cancellationToken = default)
{
var municipality = NormalizeCode(municipalityCode);
if (!int.TryParse(NormalizeSheet(sheet), out var sheetNumber))
{
throw new ArgumentException("Numero di foglio non valido.", nameof(sheet));
}
var query = new Dictionary<string, string>
{
["f"] = "json",
["where"] = $"COMUNENAZI='{municipality}' AND FOGLIO={sheetNumber}",
["outFields"] = "ETICHETTA,PARTICELLA,NORD,EST,ATTENDIBIL,QUOTA,DATAAGGIOR,IDORIGINE,URL_AGENZI,FOGLIO,ALLEGATO,FIDUCIALE",
["returnGeometry"] = "true",
["outSR"] = "3857",
["resultRecordCount"] = "500"
};
var url = $"{MappedPointsUrl}?{string.Join('&', query.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value)}"))}";
using var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
if (document.RootElement.TryGetProperty("error", out var error))
{
throw new InvalidOperationException(error.TryGetProperty("message", out var message)
? message.GetString()
: "Servizio cartografico PF non disponibile.");
}
if (!document.RootElement.TryGetProperty("features", out var features))
{
return [];
}
return features.EnumerateArray()
.Select(feature => ParseMappedPoint(feature, municipality))
.Where(point => point is not null)
.Cast<MappedFiducialPoint>()
.DistinctBy(point => point.Identifier, StringComparer.OrdinalIgnoreCase)
.OrderBy(point => point.Identifier, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static MappedFiducialPoint? ParseMappedPoint(JsonElement feature, string municipalityCode)
{
if (!feature.TryGetProperty("geometry", out var geometry)
|| !TryGetDouble(geometry, "x", out var x)
|| !TryGetDouble(geometry, "y", out var y))
{
return null;
}
var attributes = feature.GetProperty("attributes");
var label = GetString(attributes, "ETICHETTA");
var identifier = FormatIdentifier(label);
if (identifier == label)
{
var sheet = GetString(attributes, "FOGLIO").PadLeft(4, '0');
var number = GetString(attributes, "FIDUCIALE").PadLeft(2, '0');
identifier = $"PF{number}/{sheet}/{municipalityCode}";
}
const double earthRadius = 6378137d;
var longitude = x / earthRadius * 180d / Math.PI;
var latitude = (2d * Math.Atan(Math.Exp(y / earthRadius)) - Math.PI / 2d) * 180d / Math.PI;
return new MappedFiducialPoint(
identifier,
latitude,
longitude,
GetNullableDouble(attributes, "NORD"),
GetNullableDouble(attributes, "EST"),
GetString(attributes, "ATTENDIBIL"),
GetNullableDouble(attributes, "QUOTA"),
GetString(attributes, "PARTICELLA"),
GetString(attributes, "DATAAGGIOR"),
GetString(attributes, "IDORIGINE"));
}
private static bool TryGetDouble(JsonElement element, string name, out double value)
{
value = 0;
return element.TryGetProperty(name, out var property)
&& (property.ValueKind == JsonValueKind.Number
? property.TryGetDouble(out value)
: double.TryParse(property.ToString(), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out value));
}
private static double? GetNullableDouble(JsonElement element, string name) =>
TryGetDouble(element, name, out var value) ? value : null;
private static string GetString(JsonElement element, string name) =>
element.TryGetProperty(name, out var property) && property.ValueKind != JsonValueKind.Null
? property.ToString().Trim()
: string.Empty;
private static string NormalizeSheet(string sheet)
{
var digits = new string((sheet ?? string.Empty).Where(char.IsDigit).ToArray());
if (string.IsNullOrWhiteSpace(digits))
{
throw new ArgumentException("Numero di foglio non valido.", nameof(sheet));
}
return int.TryParse(digits, out var number)
? number.ToString(System.Globalization.CultureInfo.InvariantCulture)
: digits;
}
private async Task<string> GetHtmlAsync(string relativeUrl, CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(ServiceBaseUrl + relativeUrl, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
private async Task<string> PostFormAsync(
string relativeUrl,
IReadOnlyDictionary<string, string> values,
CancellationToken cancellationToken)
{
using var content = new FormUrlEncodedContent(values);
using var response = await _httpClient.PostAsync(ServiceBaseUrl + relativeUrl, content, cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
private static IReadOnlyList<FiducialAreaOption> ParseOptions(string html, string selectId)
{
var select = SelectRegex().Matches(html)
.LastOrDefault(match => string.Equals(match.Groups["id"].Value, selectId, StringComparison.OrdinalIgnoreCase));
if (select is null)
{
return [];
}
return OptionRegex().Matches(select.Groups["body"].Value)
.Select(match => new FiducialAreaOption(
WebUtility.HtmlDecode(match.Groups["value"].Value).Trim(),
WebUtility.HtmlDecode(StripTagsRegex().Replace(match.Groups["label"].Value, string.Empty)).Trim()))
.Where(item => !string.IsNullOrWhiteSpace(item.Code) && !string.IsNullOrWhiteSpace(item.Name))
.DistinctBy(item => item.Code, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string NormalizeCode(string value)
{
var normalized = value.Trim().ToUpperInvariant();
if (normalized.Length is < 2 or > 4 || normalized.Any(character => !char.IsLetterOrDigit(character)))
{
throw new ArgumentException("Codice territoriale non valido.", nameof(value));
}
return normalized;
}
private static string FormatIdentifier(string fileName)
{
var parts = fileName.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length == 3 ? $"PF{parts[2]}/{parts[1]}/{parts[0]}" : fileName;
}
[GeneratedRegex("<select\\b[^>]*(?:id|name)=[\\\"'](?<id>[^\\\"']+)[\\\"'][^>]*>(?<body>[\\s\\S]*?)</select>", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex SelectRegex();
[GeneratedRegex("<option\\b[^>]*value=[\\\"'](?<value>[^\\\"']*)[\\\"'][^>]*>(?<label>[\\s\\S]*?)</option>", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex OptionRegex();
[GeneratedRegex("href=[\\\"'](?<url>download\\.php\\?[^\\\"']*namefile=(?<name>[A-Z0-9-]+))[^\\\"']*[\\\"']", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)]
private static partial Regex DownloadLinkRegex();
[GeneratedRegex("<[^>]+>", RegexOptions.None, 2000)]
private static partial Regex StripTagsRegex();
}
public sealed record FiducialAreaOption(string Code, string Name);
public sealed record FiducialPointEntry(string Identifier, string MonographUrl);
public sealed record MappedFiducialPoint(
string Identifier,
double Latitude,
double Longitude,
double? CadastralNorth,
double? CadastralEast,
string Reliability,
double? Elevation,
string Parcel,
string UpdatedAt,
string OriginId,
string MonographUrl = "");
+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
+174
View File
@@ -1185,6 +1185,17 @@ input[type="checkbox"] {
text-decoration: none; text-decoration: none;
} }
.lme-geometry-toolbar a.lme-fiducial-download {
font-size: 12px !important;
letter-spacing: 0;
}
.lme-geometry-toolbar a.is-disabled {
color: #8b949e;
background: #eef1f3;
opacity: 0.72;
}
.leaflet-help { .leaflet-help {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -1247,6 +1258,9 @@ input[type="checkbox"] {
.gps-overlay.is-collapsed { .gps-overlay.is-collapsed {
grid-template-columns: auto; grid-template-columns: auto;
right: auto;
width: 48px;
max-width: 48px;
padding: 5px; padding: 5px;
} }
@@ -1255,6 +1269,28 @@ input[type="checkbox"] {
display: none; display: none;
} }
.fiducial-list {
display: grid;
gap: 8px;
}
.fiducial-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #d9e0e3;
}
.fiducial-item strong {
overflow-wrap: anywhere;
}
.fiducial-item .secondary-command {
margin: 0;
}
.gps-overlay.search-open { .gps-overlay.search-open {
top: 140px; top: 140px;
} }
@@ -1318,6 +1354,21 @@ input[type="checkbox"] {
height: 38px; height: 38px;
} }
.cadastral-sheet-label {
width: auto !important;
height: auto !important;
border: 1px solid #9a5b00;
border-radius: 4px;
padding: 3px 6px;
background: rgba(255, 251, 235, 0.94);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28);
color: #713f12;
font-size: 11px;
font-weight: 800;
line-height: 1.2;
white-space: nowrap;
}
.lme-drawing-scope-actions a[aria-disabled="true"] { .lme-drawing-scope-actions a[aria-disabled="true"] {
opacity: 0.45; opacity: 0.45;
pointer-events: none; pointer-events: none;
@@ -1623,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;
} }
@@ -1866,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;
@@ -1891,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;
@@ -1921,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>
+726 -41
View File
File diff suppressed because it is too large. Load diff
+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