Files
MadJax 0e6a97ca61 Renommage : TrajetKmTracker -> SchotzIadKmTracker (backend, specs, frontend)
- 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.
2026-09-14 07:50:26 +02:00

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);
}
}