using System.Globalization; using System.Net; using System.Net.Http.Headers; using System.Xml.Linq; namespace LMEMappeCatastaliApp.Services; public sealed class OfficialCadastreService { private const string WfsEndpoint = "https://wfs.cartografia.agenziaentrate.gov.it/inspire/wfs/owfs01.php"; private readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) }; public OfficialCadastreService() { _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LMEMappeCatastali/1.0 (Android)"); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/gml+xml")); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml", 0.9)); } public async Task GetParcelsNearAsync( double latitude, double longitude, double radiusMeters, CancellationToken cancellationToken = default) { var firstRadius = Math.Clamp(radiusMeters, 12, 30); var xml = await GetFeaturesNearAsync( latitude, longitude, firstRadius, "CP:CadastralParcel", 100, cancellationToken); if (firstRadius >= 30 || ContainsFeatures(xml)) { return xml; } return await GetFeaturesNearAsync( latitude, longitude, 30, "CP:CadastralParcel", 100, cancellationToken); } public async Task GetSheetsNearAsync( double latitude, double longitude, double radiusMeters = 10, CancellationToken cancellationToken = default) => await GetFeaturesNearAsync( latitude, longitude, radiusMeters, "CP:CadastralZoning", 20, cancellationToken); private async Task GetFeaturesNearAsync( double latitude, double longitude, double radiusMeters, string typeName, int count, CancellationToken cancellationToken) { var meters = Math.Clamp(radiusMeters, 2, 30); var deltaLatitude = meters / 111_320d; var longitudeScale = Math.Cos(latitude * Math.PI / 180d); var deltaLongitude = meters / (111_320d * Math.Max(0.1, longitudeScale)); var bbox = string.Join(",", [ Format(latitude - deltaLatitude), Format(longitude - deltaLongitude), Format(latitude + deltaLatitude), Format(longitude + deltaLongitude) ]); var query = new Dictionary { ["language"] = "ita", ["SERVICE"] = "WFS", ["VERSION"] = "2.0.0", ["TYPENAMES"] = typeName, ["SRSNAME"] = "urn:ogc:def:crs:EPSG::6706", ["BBOX"] = bbox, ["REQUEST"] = "GetFeature", ["COUNT"] = count.ToString(CultureInfo.InvariantCulture) }; var url = WfsEndpoint + "?" + string.Join("&", query.Select(item => $"{Uri.EscapeDataString(item.Key)}={Uri.EscapeDataString(item.Value)}")); for (var attempt = 0; attempt < 3; attempt++) { try { using var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = HttpVersion.Version11, VersionPolicy = HttpVersionPolicy.RequestVersionOrLower }; request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; using var response = await _httpClient.SendAsync( request, HttpCompletionOption.ResponseContentRead, cancellationToken); if (response.IsSuccessStatusCode) { var xml = await response.Content.ReadAsStringAsync(cancellationToken); ThrowIfExceptionReport(xml); return xml; } if (attempt < 2 && IsTransient(response.StatusCode)) { await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken); continue; } throw new HttpRequestException($"Il servizio WFS AdE ha risposto {((int)response.StatusCode)}."); } catch (HttpRequestException) when (attempt < 2) { await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken); } catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested && attempt < 2) { await Task.Delay(TimeSpan.FromMilliseconds(500 * (attempt + 1)), cancellationToken); } } throw new HttpRequestException("Il servizio WFS AdE non ha risposto dopo tre tentativi."); } private static bool IsTransient(HttpStatusCode statusCode) => statusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests || (int)statusCode >= 500; private static bool ContainsFeatures(string xml) { try { var document = XDocument.Parse(xml); return document.Descendants().Any(element => element.Name.LocalName is "member" or "featureMember"); } catch (System.Xml.XmlException) { return false; } } private static string Format(double value) => value.ToString("R", CultureInfo.InvariantCulture); private static void ThrowIfExceptionReport(string xml) { try { var document = XDocument.Parse(xml); var exceptionText = document.Descendants() .FirstOrDefault(element => element.Name.LocalName == "ExceptionText") ?.Value .Trim(); if (!string.IsNullOrWhiteSpace(exceptionText)) { throw new InvalidOperationException($"Servizio WFS AdE: {exceptionText}"); } } catch (System.Xml.XmlException error) { throw new InvalidDataException("Il servizio WFS AdE ha restituito una risposta non valida.", error); } } }