- 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.
134 lines
4.2 KiB
C#
134 lines
4.2 KiB
C#
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; }
|
|
}
|
|
}
|