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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user