using System.Net.Http.Json; using System.Text.Json; namespace TrajetKmTracker.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); } }