Files

183 lines
6.2 KiB
C#

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<string> 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<string> GetSheetsNearAsync(
double latitude,
double longitude,
double radiusMeters = 10,
CancellationToken cancellationToken = default)
=> await GetFeaturesNearAsync(
latitude,
longitude,
radiusMeters,
"CP:CadastralZoning",
20,
cancellationToken);
private async Task<string> 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<string, string>
{
["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);
}
}
}