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:
2026-09-14 07:50:26 +02:00
parent e3f946f7d4
commit 0e6a97ca61
133 changed files with 462 additions and 503 deletions
@@ -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);
}
}