diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index 41155b3..d883467 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -3,6 +3,7 @@ @inject PreciseLocationService LocationService @inject CadastralCatalogService CatalogService @inject OfficialCadastreService OfficialCadastreService +@inject FiducialPointService FiducialPointService @inject LocationSearchService LocationSearchService @inject NetworkLocationService NetworkLocationService @inject AzureMapsService AzureMapsService @@ -46,6 +47,7 @@ + @@ -201,6 +203,84 @@ } + else if (ActiveTab == "fiducials") + { +
+ @if (!CanUse(AppFeatures.FiducialPoints)) + { +
+ Funzione LME Pro +

La ricerca dei Punti Fiduciali e delle monografie ufficiali è disponibile con abbonamento Pro o chiave manuale valida.

+ +
+ } + else + { + + + + + + + + + @if (!string.IsNullOrWhiteSpace(FiducialMessage)) + { +
@FiducialMessage
+ } + + @if (FiducialPoints.Count > 0) + { +
+

Punti trovati (@FiducialPoints.Count)

+
+ @foreach (var point in FiducialPoints) + { +
+ @point.Identifier + +
+ } +
+
+ } + +
+ Fonte ufficiale Agenzia delle Entrate +

L'elenco e i PDF arrivano dal servizio pubblico delle monografie. Il layer sulla mappa sarà abilitato soltanto quando sono disponibili coordinate geografiche trasformate e verificabili.

+
+ } +
+ } else if (ActiveTab == "data") {
@@ -745,6 +825,11 @@ private string MonthlyPrice { get; set; } = string.Empty; private string YearlyPrice { get; set; } = string.Empty; private bool BillingBusy { get; set; } + private bool FiducialBusy { get; set; } + private string SelectedFiducialProvinceCode { get; set; } = string.Empty; + private string SelectedFiducialMunicipalityCode { get; set; } = string.Empty; + private string SelectedFiducialSheet { get; set; } = string.Empty; + private string FiducialMessage { get; set; } = string.Empty; private string Status { get; set; } = "online"; private string SelectedBaseLayer { get; set; } = "satellite"; private string GoogleMapsApiKey { get; set; } = string.Empty; @@ -821,6 +906,10 @@ private IReadOnlyList Municipalities { get; set; } = []; private IReadOnlyList Sheets { get; set; } = []; private IReadOnlyList Parcels { get; set; } = []; + private IReadOnlyList FiducialProvinces { get; set; } = []; + private IReadOnlyList FiducialMunicipalities { get; set; } = []; + private IReadOnlyList FiducialSheets { get; set; } = []; + private IReadOnlyList FiducialPoints { get; set; } = []; private IReadOnlyList Projects { get; set; } = []; private IReadOnlyList ProjectVersions { get; set; } = []; private MapProjectSummary? SelectedProject => Projects.FirstOrDefault(item => item.Id == SelectedProjectId); @@ -834,6 +923,7 @@ private string PanelTitle => ActiveTab switch { "cadastre" => "Ricerca catastale", + "fiducials" => "Punti fiduciali", "data" => "Dati e disegni", "projects" => "Archivio progetti", "save" => "Salvataggio ed esportazione", @@ -845,6 +935,7 @@ private string PanelEyebrow => ActiveTab switch { "cadastre" => "Regione, Comune, Foglio, Particella", + "fiducials" => "Monografie ufficiali AdE", "data" => "File GIS, misure e POI", "projects" => "Salva, apri e visualizza i tuoi lavori", "save" => "Esporta file GIS", @@ -1056,6 +1147,10 @@ { await RefreshProjectsAsync(); } + if (tab == "fiducials" && CanUse(AppFeatures.FiducialPoints) && FiducialProvinces.Count == 0) + { + await LoadFiducialProvincesAsync(); + } await Task.Delay(30); await JS.InvokeVoidAsync("lmeMap.resize"); if (LayerPanelOpen) @@ -1135,6 +1230,132 @@ : await CatalogService.GetMunicipalitiesAsync(SelectedRegionFile); } + private async Task LoadFiducialProvincesAsync() + { + FiducialBusy = true; + FiducialMessage = "Carico l'elenco delle province..."; + try + { + FiducialProvinces = await FiducialPointService.GetProvincesAsync(); + FiducialMessage = string.Empty; + } + catch (Exception error) + { + FiducialMessage = $"Servizio monografie non disponibile: {error.GetBaseException().Message}"; + } + finally + { + FiducialBusy = false; + } + } + + private async Task OnFiducialProvinceChanged(ChangeEventArgs args) + { + SelectedFiducialProvinceCode = args.Value?.ToString() ?? string.Empty; + SelectedFiducialMunicipalityCode = string.Empty; + SelectedFiducialSheet = string.Empty; + FiducialMunicipalities = []; + FiducialSheets = []; + FiducialPoints = []; + if (string.IsNullOrWhiteSpace(SelectedFiducialProvinceCode)) + { + FiducialMessage = string.Empty; + return; + } + + FiducialBusy = true; + FiducialMessage = "Carico i Comuni catastali..."; + try + { + FiducialMunicipalities = await FiducialPointService.GetMunicipalitiesAsync(SelectedFiducialProvinceCode); + FiducialMessage = string.Empty; + } + catch (Exception error) + { + FiducialMessage = $"Comuni non disponibili: {error.GetBaseException().Message}"; + } + finally + { + FiducialBusy = false; + } + } + + private async Task OnFiducialMunicipalityChanged(ChangeEventArgs args) + { + SelectedFiducialMunicipalityCode = args.Value?.ToString() ?? string.Empty; + SelectedFiducialSheet = string.Empty; + FiducialSheets = []; + FiducialPoints = []; + if (string.IsNullOrWhiteSpace(SelectedFiducialMunicipalityCode)) + { + FiducialMessage = string.Empty; + return; + } + + FiducialBusy = true; + FiducialMessage = "Carico i fogli disponibili..."; + try + { + FiducialSheets = await FiducialPointService.GetSheetsAsync( + SelectedFiducialProvinceCode, + SelectedFiducialMunicipalityCode); + FiducialMessage = string.Empty; + } + catch (Exception error) + { + FiducialMessage = $"Fogli non disponibili: {error.GetBaseException().Message}"; + } + finally + { + FiducialBusy = false; + } + } + + private async Task SearchFiducials() + { + var province = FiducialProvinces.FirstOrDefault(item => item.Code == SelectedFiducialProvinceCode); + var municipality = FiducialMunicipalities.FirstOrDefault(item => item.Code == SelectedFiducialMunicipalityCode); + if (province is null || municipality is null || string.IsNullOrWhiteSpace(SelectedFiducialSheet)) + { + return; + } + + FiducialBusy = true; + FiducialPoints = []; + FiducialMessage = "Interrogo il servizio ufficiale delle monografie..."; + try + { + FiducialPoints = await FiducialPointService.GetFiducialsAsync( + province.Name, + municipality.Name, + municipality.Code, + SelectedFiducialSheet); + FiducialMessage = FiducialPoints.Count == 0 + ? "Nessun Punto Fiduciale pubblicato per il foglio selezionato." + : $"Trovati {FiducialPoints.Count} Punti Fiduciali."; + } + catch (Exception error) + { + FiducialMessage = $"Ricerca PF non riuscita: {error.GetBaseException().Message}"; + } + finally + { + FiducialBusy = false; + } + } + + private async Task DownloadFiducialMonograph(FiducialPointEntry point) + { + try + { + await Launcher.Default.OpenAsync(new Uri(point.MonographUrl)); + } + catch (Exception error) + { + FiducialMessage = $"Impossibile aprire la monografia: {error.GetBaseException().Message}"; + } + } + private async Task OnMunicipalityChanged(ChangeEventArgs args) { SelectedMunicipalityCode = args.Value?.ToString() ?? string.Empty; diff --git a/LMEMappeCatastaliApp.csproj b/LMEMappeCatastaliApp.csproj index dc9af14..77c4a49 100644 --- a/LMEMappeCatastaliApp.csproj +++ b/LMEMappeCatastaliApp.csproj @@ -28,8 +28,8 @@ it.liviomerola.lmemappecatastali.pro - 1.0.11 - 111 + 1.0.12 + 112 $(ApplicationDisplayVersion) diff --git a/MauiProgram.cs b/MauiProgram.cs index 2653f34..dea0b43 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -19,6 +19,7 @@ public static class MauiProgram builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Services/EntitlementService.cs b/Services/EntitlementService.cs index f924291..d98c38b 100644 --- a/Services/EntitlementService.cs +++ b/Services/EntitlementService.cs @@ -21,7 +21,8 @@ public sealed class EntitlementService( AppFeatures.CloudSync, AppFeatures.OfflineMaps, AppFeatures.AdvancedGeometry, - AppFeatures.GpsTrail + AppFeatures.GpsTrail, + AppFeatures.FiducialPoints }; public bool IsGoogleProActive { get; private set; } @@ -185,4 +186,5 @@ public static class AppFeatures public const string OfflineMaps = "offline-maps"; public const string AdvancedGeometry = "advanced-geometry"; public const string GpsTrail = "gps-trail"; + public const string FiducialPoints = "fiducial-points"; } diff --git a/Services/FiducialPointService.cs b/Services/FiducialPointService.cs new file mode 100644 index 0000000..5abb72d --- /dev/null +++ b/Services/FiducialPointService.cs @@ -0,0 +1,148 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; + +namespace LMEMappeCatastaliApp.Services; + +public sealed partial class FiducialPointService +{ + private const string ServiceBaseUrl = "https://www1.agenziaentrate.gov.it/servizi/Monografie/"; + private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) }; + + public FiducialPointService() + { + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)"); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); + } + + public async Task> GetProvincesAsync( + CancellationToken cancellationToken = default) + { + var html = await GetHtmlAsync("ricerca.php", cancellationToken); + return ParseOptions(html, "provincia"); + } + + public async Task> 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> 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 { ["co"] = municipality }, cancellationToken); + return ParseOptions(html, "foglio"); + } + + public async Task> 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 { ["foglio"] = normalizedSheet }, + cancellationToken); + + return DownloadLinkRegex().Matches(html) + .Select(match => + { + var relativeUrl = WebUtility.HtmlDecode(match.Groups["url"].Value); + var fileName = WebUtility.HtmlDecode(match.Groups["name"].Value); + return new FiducialPointEntry( + FormatIdentifier(fileName), + new Uri(new Uri(ServiceBaseUrl), relativeUrl).AbsoluteUri); + }) + .DistinctBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase) + .OrderBy(item => item.Identifier, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + private async Task 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 PostFormAsync( + string relativeUrl, + IReadOnlyDictionary 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 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("]*(?:id|name)=[\\\"'](?[^\\\"']+)[\\\"'][^>]*>(?[\\s\\S]*?)", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 2000)] + private static partial Regex SelectRegex(); + + [GeneratedRegex("]*value=[\\\"'](?[^\\\"']*)[\\\"'][^>]*>(?