using Microsoft.Extensions.Logging;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.Api.Services;
///
/// Implémentation de .
///
/// 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_
///
public class DistanceCalculatorService : IDistanceCalculator
{
/// Distance minimale stockable (km).
private const decimal DistanceMinKm = 0.01m;
/// Distance maximale stockable (km).
private const decimal DistanceMaxKm = 99_999.99m;
private readonly IBANClient _banClient;
private readonly IORSClient _orsClient;
private readonly ILogger _logger;
public DistanceCalculatorService(
IBANClient banClient,
IORSClient orsClient,
ILogger logger)
{
_banClient = banClient;
_orsClient = orsClient;
_logger = logger;
}
///
public async Task 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);
}
///
/// Géocode une adresse via BAN et retourne les coordonnées du meilleur (premier) résultat.
/// Retourne (null, null) si aucune adresse n'est trouvée ou si BAN est indisponible,
/// sans propager d'exception (échec BAN non bloquant).
///
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);
}
}
/// Vrai si latitude ET longitude sont toutes deux renseignées.
private static bool EstCoordonneeConnue(double? latitude, double? longitude)
=> latitude.HasValue && longitude.HasValue;
/// Borne une distance dans l'intervalle stockable [0,01 ; 99 999,99] km.
private static decimal BornerDistance(decimal distanceKm)
{
if (distanceKm < DistanceMinKm)
{
return DistanceMinKm;
}
return distanceKm > DistanceMaxKm ? DistanceMaxKm : distanceKm;
}
///
/// 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).
///
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);
}
}