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=[\\\"'](?[^\\\"']*)[\\\"'][^>]*>(?