263 lines
11 KiB
C#
263 lines
11 KiB
C#
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 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(new string(sheet.Where(char.IsDigit).ToArray()), 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 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 = "");
|