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;
|
||||
using System.Text.Json.Serialization;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client typé de l'API BAN. Appelle <c>GET /search/?q={query}&limit=5</c>,
|
||||
/// parse la réponse GeoJSON (FeatureCollection) et mappe les features en
|
||||
/// <see cref="BANAdresseDto"/>. Lève <see cref="BANIndisponibleException"/>
|
||||
/// en cas d'échec ou de timeout.
|
||||
/// </summary>
|
||||
public class BANClient : IBANClient
|
||||
{
|
||||
private const int MaxResultats = 5;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BANClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BANAdresseDto>> RechercherAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return Array.Empty<BANAdresseDto>();
|
||||
}
|
||||
|
||||
var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}";
|
||||
|
||||
BANFeatureCollection? collection;
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
collection = await response.Content.ReadFromJsonAsync<BANFeatureCollection>(
|
||||
JsonOptions, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Le timeout configuré sur le HttpClient déclenche une annulation
|
||||
// non demandée par l'appelant.
|
||||
throw new BANIndisponibleException(
|
||||
"L'appel à l'API BAN a dépassé le délai imparti (timeout).");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new BANIndisponibleException(
|
||||
"L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new BANIndisponibleException(
|
||||
"La réponse de l'API BAN n'a pas pu être interprétée.", ex);
|
||||
}
|
||||
|
||||
if (collection?.Features is null || collection.Features.Count == 0)
|
||||
{
|
||||
return Array.Empty<BANAdresseDto>();
|
||||
}
|
||||
|
||||
var resultats = new List<BANAdresseDto>(collection.Features.Count);
|
||||
foreach (var feature in collection.Features)
|
||||
{
|
||||
var coordinates = feature.Geometry?.Coordinates;
|
||||
if (coordinates is null || coordinates.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// GeoJSON : coordinates = [longitude, latitude].
|
||||
var longitude = coordinates[0];
|
||||
var latitude = coordinates[1];
|
||||
|
||||
resultats.Add(new BANAdresseDto(
|
||||
Label: feature.Properties?.Label ?? string.Empty,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
Postcode: feature.Properties?.Postcode ?? string.Empty,
|
||||
City: feature.Properties?.City ?? string.Empty));
|
||||
}
|
||||
|
||||
return resultats;
|
||||
}
|
||||
|
||||
// --- Modèles internes de désérialisation de la réponse GeoJSON BAN ---
|
||||
|
||||
private sealed class BANFeatureCollection
|
||||
{
|
||||
[JsonPropertyName("features")]
|
||||
public List<BANFeature>? Features { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANFeature
|
||||
{
|
||||
[JsonPropertyName("properties")]
|
||||
public BANProperties? Properties { get; set; }
|
||||
|
||||
[JsonPropertyName("geometry")]
|
||||
public BANGeometry? Geometry { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANProperties
|
||||
{
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; set; }
|
||||
|
||||
[JsonPropertyName("postcode")]
|
||||
public string? Postcode { get; set; }
|
||||
|
||||
[JsonPropertyName("city")]
|
||||
public string? City { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANGeometry
|
||||
{
|
||||
// [longitude, latitude]
|
||||
[JsonPropertyName("coordinates")]
|
||||
public List<double>? Coordinates { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user