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,193 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implémentation de <see cref="IDistanceCalculator"/>.
|
||||
///
|
||||
/// Résout d'abord les coordonnées manquantes via BAN (priorité BAN sur ORS,
|
||||
/// Property 16), puis calcule la distance routière via ORS. En cas de timeout ou
|
||||
/// d'échec ORS, applique le fallback orthodromique (Haversine) sans erreur. Si les
|
||||
/// coordonnées ne peuvent pas être résolues (BAN sans résultat / indisponible), retourne
|
||||
/// une distance nulle assortie d'un avertissement, sans lever d'exception bloquante.
|
||||
///
|
||||
/// _Requirements: 2.1, 2.2, 2.3, 2.4, 2.6_
|
||||
/// </summary>
|
||||
public class DistanceCalculatorService : IDistanceCalculator
|
||||
{
|
||||
/// <summary>Distance minimale stockable (km).</summary>
|
||||
private const decimal DistanceMinKm = 0.01m;
|
||||
|
||||
/// <summary>Distance maximale stockable (km).</summary>
|
||||
private const decimal DistanceMaxKm = 99_999.99m;
|
||||
|
||||
private readonly IBANClient _banClient;
|
||||
private readonly IORSClient _orsClient;
|
||||
private readonly ILogger<DistanceCalculatorService> _logger;
|
||||
|
||||
public DistanceCalculatorService(
|
||||
IBANClient banClient,
|
||||
IORSClient orsClient,
|
||||
ILogger<DistanceCalculatorService> logger)
|
||||
{
|
||||
_banClient = banClient;
|
||||
_orsClient = orsClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DistanceCalculationResult> CalculerAsync(
|
||||
string adresseDepart,
|
||||
string adresseArrivee,
|
||||
double? departLatitude,
|
||||
double? departLongitude,
|
||||
double? arriveeLatitude,
|
||||
double? arriveeLongitude,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Coordonnées de départ connues seulement si latitude ET longitude sont présentes.
|
||||
double? depLat = departLatitude;
|
||||
double? depLon = departLongitude;
|
||||
double? arrLat = arriveeLatitude;
|
||||
double? arrLon = arriveeLongitude;
|
||||
|
||||
// Résolution BAN AVANT tout appel ORS pour les coordonnées manquantes (Property 16).
|
||||
if (!EstCoordonneeConnue(depLat, depLon))
|
||||
{
|
||||
var (lat, lon) = await GeocoderAsync(adresseDepart, cancellationToken);
|
||||
depLat = lat;
|
||||
depLon = lon;
|
||||
}
|
||||
|
||||
if (!EstCoordonneeConnue(arrLat, arrLon))
|
||||
{
|
||||
var (lat, lon) = await GeocoderAsync(adresseArrivee, cancellationToken);
|
||||
arrLat = lat;
|
||||
arrLon = lon;
|
||||
}
|
||||
|
||||
// Si une coordonnée reste absente après géocodage BAN → distance nulle + avertissement.
|
||||
if (!EstCoordonneeConnue(depLat, depLon) || !EstCoordonneeConnue(arrLat, arrLon))
|
||||
{
|
||||
return new DistanceCalculationResult(
|
||||
DepartLatitude: depLat,
|
||||
DepartLongitude: depLon,
|
||||
ArriveeLatitude: arrLat,
|
||||
ArriveeLongitude: arrLon,
|
||||
DistanceKm: null,
|
||||
GeometrieJson: null,
|
||||
Avertissement: true,
|
||||
Message: "Le trajet a été enregistré mais la distance n'a pas pu être "
|
||||
+ "calculée faute de coordonnées géographiques.");
|
||||
}
|
||||
|
||||
// À ce stade, les quatre coordonnées sont non-null : jamais d'appel ORS avec des coordonnées nulles.
|
||||
var lat1 = depLat!.Value;
|
||||
var lon1 = depLon!.Value;
|
||||
var lat2 = arrLat!.Value;
|
||||
var lon2 = arrLon!.Value;
|
||||
|
||||
decimal distanceKm;
|
||||
string? geometrieJson = null;
|
||||
var avertissement = false;
|
||||
string? message = null;
|
||||
|
||||
try
|
||||
{
|
||||
var route = await _orsClient.CalculateRouteAsync(lat1, lon1, lat2, lon2, cancellationToken);
|
||||
distanceKm = route.DistanceKm;
|
||||
geometrieJson = route.GeometrieJson;
|
||||
}
|
||||
catch (ORSTimeoutException ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Timeout ORS : application du fallback Haversine pour le calcul de distance.");
|
||||
distanceKm = HaversineKm(lat1, lon1, lat2, lon2);
|
||||
}
|
||||
catch (ORSException ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Échec ORS : application du fallback Haversine pour le calcul de distance.");
|
||||
distanceKm = HaversineKm(lat1, lon1, lat2, lon2);
|
||||
}
|
||||
|
||||
// Arrondi 2 décimales + bornage [0,01 ; 99 999,99] km.
|
||||
distanceKm = BornerDistance(Math.Round(distanceKm, 2, MidpointRounding.AwayFromZero));
|
||||
|
||||
return new DistanceCalculationResult(
|
||||
DepartLatitude: lat1,
|
||||
DepartLongitude: lon1,
|
||||
ArriveeLatitude: lat2,
|
||||
ArriveeLongitude: lon2,
|
||||
DistanceKm: distanceKm,
|
||||
GeometrieJson: geometrieJson,
|
||||
Avertissement: avertissement,
|
||||
Message: message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Géocode une adresse via BAN et retourne les coordonnées du meilleur (premier) résultat.
|
||||
/// Retourne <c>(null, null)</c> si aucune adresse n'est trouvée ou si BAN est indisponible,
|
||||
/// sans propager d'exception (échec BAN non bloquant).
|
||||
/// </summary>
|
||||
private async Task<(double? Latitude, double? Longitude)> GeocoderAsync(
|
||||
string adresse,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(adresse))
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var resultats = await _banClient.RechercherAsync(adresse, cancellationToken);
|
||||
if (resultats.Count == 0)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
// Meilleur / premier résultat (la BAN retourne les résultats par pertinence décroissante).
|
||||
var meilleur = resultats[0];
|
||||
return (meilleur.Latitude, meilleur.Longitude);
|
||||
}
|
||||
catch (BANIndisponibleException ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"BAN indisponible lors du géocodage de l'adresse manquante ; distance non calculable.");
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Vrai si latitude ET longitude sont toutes deux renseignées.</summary>
|
||||
private static bool EstCoordonneeConnue(double? latitude, double? longitude)
|
||||
=> latitude.HasValue && longitude.HasValue;
|
||||
|
||||
/// <summary>Borne une distance dans l'intervalle stockable [0,01 ; 99 999,99] km.</summary>
|
||||
private static decimal BornerDistance(decimal distanceKm)
|
||||
{
|
||||
if (distanceKm < DistanceMinKm)
|
||||
{
|
||||
return DistanceMinKm;
|
||||
}
|
||||
|
||||
return distanceKm > DistanceMaxKm ? DistanceMaxKm : distanceKm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calcule la distance orthodromique (formule de Haversine) entre deux points, en
|
||||
/// kilomètres, arrondie à 2 décimales. R = 6371 km (rayon moyen de la Terre).
|
||||
/// </summary>
|
||||
private static decimal HaversineKm(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
const double R = 6371.0;
|
||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
var dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2)
|
||||
+ Math.Cos(lat1 * Math.PI / 180) * Math.Cos(lat2 * Math.PI / 180)
|
||||
* Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||
return Math.Round((decimal)(R * c), 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user