- Backend : projets, solution, namespaces et références TrajetKmTracker.Api/.Tests -> SchotzIadKmTracker.Api/.Tests (dossiers, .csproj, .sln, .http, namespaces C#). GUID UserSecretsId préservé. - Specs : dossier .kiro/specs/trajet-km-tracker -> schotziad-km-tracker et mise à jour des références internes (chemins, tags de propriétés). - Frontend : projet Angular trajet-km-tracker -> schotziad-km-tracker (package.json, angular.json, chemin dist, Dockerfile), titre applicatif 'SchotzIAD KM Tracker'. - Docker et scripts (backend/Dockerfile, frontend/Dockerfile, run-dev.sh) mis à jour. - Suppression du fichier résiduel package.json.bak. Backend (solution) et frontend (build production) compilent sans erreur.
134 lines
4.7 KiB
C#
134 lines
4.7 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace SchotzIadKmTracker.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Client typé (HttpClient) de l'API OpenRouteService (ORS) pour le calcul
|
|
/// d'itinéraire routier. La <c>BaseAddress</c>, le <c>Timeout</c> et l'en-tête
|
|
/// de clé d'API sont configurés lors de l'enregistrement du client typé dans
|
|
/// <c>Program.cs</c> à partir de <c>ORSOptions</c>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<RouteResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <c>properties.summary.distance</c> (mètres) et <c>geometry</c> (GeoJSON).
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|