using System.Net.Http.Json;
using System.Text.Json;
namespace SchotzIadKmTracker.Api.Services;
///
/// Client typé (HttpClient) de l'API OpenRouteService (ORS) pour le calcul
/// d'itinéraire routier. La BaseAddress, le Timeout et l'en-tête
/// de clé d'API sont configurés lors de l'enregistrement du client typé dans
/// Program.cs à partir de ORSOptions.
///
public class ORSClient : IORSClient
{
// Point d'accès GeoJSON : retourne une FeatureCollection avec la géométrie
// de l'itinéraire au format GeoJSON et le résumé (distance en mètres).
private const string DirectionsGeoJsonPath = "v2/directions/driving-car/geojson";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly HttpClient _httpClient;
public ORSClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
///
public async Task CalculateRouteAsync(
double lat1,
double lon1,
double lat2,
double lon2,
CancellationToken cancellationToken = default)
{
// ORS attend les coordonnées au format [longitude, latitude].
var body = new
{
coordinates = new[]
{
new[] { lon1, lat1 },
new[] { lon2, lat2 }
}
};
HttpResponseMessage response;
try
{
response = await _httpClient.PostAsJsonAsync(DirectionsGeoJsonPath, body, cancellationToken);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
// Le HttpClient annule via son Timeout : OperationCanceledException est levée
// sans que le jeton de l'appelant soit annulé -> il s'agit d'un dépassement de délai.
throw new ORSTimeoutException(
"L'appel à OpenRouteService a dépassé le délai imparti.", ex);
}
catch (TaskCanceledException ex)
{
throw new ORSTimeoutException(
"L'appel à OpenRouteService a dépassé le délai imparti.", ex);
}
catch (HttpRequestException ex)
{
throw new ORSException(
"Échec de l'appel réseau à OpenRouteService.", ex);
}
if (!response.IsSuccessStatusCode)
{
throw new ORSException(
$"OpenRouteService a retourné un statut non conforme : {(int)response.StatusCode}.");
}
try
{
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
return ParseRouteResult(document.RootElement);
}
catch (JsonException ex)
{
throw new ORSException(
"La réponse d'OpenRouteService n'a pas pu être analysée.", ex);
}
}
///
/// Extrait la distance (mètres -> km) et la géométrie GeoJSON de la réponse ORS.
/// La réponse GeoJSON est une FeatureCollection dont la première feature contient
/// properties.summary.distance (mètres) et geometry (GeoJSON).
///
private static RouteResult ParseRouteResult(JsonElement root)
{
if (!root.TryGetProperty("features", out var features)
|| features.ValueKind != JsonValueKind.Array
|| features.GetArrayLength() == 0)
{
throw new ORSException(
"La réponse d'OpenRouteService ne contient aucun itinéraire.");
}
var feature = features[0];
double distanceMeters;
if (feature.TryGetProperty("properties", out var properties)
&& properties.TryGetProperty("summary", out var summary)
&& summary.TryGetProperty("distance", out var distanceElement)
&& distanceElement.ValueKind == JsonValueKind.Number)
{
distanceMeters = distanceElement.GetDouble();
}
else
{
throw new ORSException(
"La distance est absente de la réponse d'OpenRouteService.");
}
// Conversion mètres -> km, arrondi à 2 décimales.
var distanceKm = Math.Round((decimal)(distanceMeters / 1000.0), 2, MidpointRounding.AwayFromZero);
string? geometrieJson = null;
if (feature.TryGetProperty("geometry", out var geometry)
&& geometry.ValueKind == JsonValueKind.Object)
{
geometrieJson = geometry.GetRawText();
}
return new RouteResult(distanceKm, geometrieJson);
}
}