diff options
Diffstat (limited to 'src/Costasdev.Busurbano.Backend/Services')
4 files changed, 136 insertions, 85 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Services/FareService.cs b/src/Costasdev.Busurbano.Backend/Services/FareService.cs index d0423e6..c08d1d5 100644 --- a/src/Costasdev.Busurbano.Backend/Services/FareService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/FareService.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Options; namespace Costasdev.Busurbano.Backend.Services; -public record FareResult(decimal CashFareEuro, decimal CardFareEuro); +public record FareResult(decimal CashFareEuro, bool CashFareIsTotal, decimal CardFareEuro, bool CardFareIsTotal); public class FareService { @@ -41,18 +41,23 @@ public class FareService if (!transitLegs.Any()) { - return new FareResult(0, 0); + return new FareResult(0, true, 0, true); } + var cashResult = CalculateCashTotal(transitLegs); + var cardResult = CalculateCardTotal(transitLegs); + return new FareResult( - CalculateCashTotal(transitLegs), - CalculateCardTotal(transitLegs) + cashResult.Item1, cashResult.Item2, + cardResult.Item1, cardResult.Item2 ); } - private decimal CalculateCashTotal(IEnumerable<Leg> legs) + private (decimal, bool) CalculateCashTotal(IEnumerable<Leg> legs) { decimal total = 0L; + bool allLegsProcessed = true; + foreach (var leg in legs) { switch (leg.FeedId) @@ -80,21 +85,25 @@ public class FareService total += _xuntaFareProvider.GetPrice(leg.From!.ZoneId!, leg.To!.ZoneId!)!.PriceCash; break; + default: + allLegsProcessed = false; + _logger.LogWarning("Unknown FeedId: {FeedId}", leg.FeedId); + break; } } - return total; + return (total, allLegsProcessed); } - private decimal CalculateCardTotal(IEnumerable<Leg> legs) + private (decimal, bool) CalculateCardTotal(IEnumerable<Leg> legs) { List<TicketPurchased> wallet = []; decimal totalCost = 0; + bool allLegsProcessed = true; + foreach (var leg in legs) { - _logger.LogDebug("Processing leg {leg}", leg); - int maxMinutes; int maxUsages; string? metroArea = null; @@ -138,6 +147,7 @@ public class FareService break; default: _logger.LogWarning("Unknown FeedId: {FeedId}", leg.FeedId); + allLegsProcessed = false; continue; } @@ -193,17 +203,17 @@ public class FareService } } - return totalCost; + return (totalCost, allLegsProcessed); } } public class TicketPurchased { - public string FeedId { get; set; } + public required string FeedId { get; set; } public DateTime PurchasedAt { get; set; } public string? MetroArea { get; set; } - public string StartZone { get; set; } + public required string StartZone { get; set; } public int UsedTimes = 1; public decimal TotalPaid { get; set; } diff --git a/src/Costasdev.Busurbano.Backend/Services/IGeocodingService.cs b/src/Costasdev.Busurbano.Backend/Services/IGeocodingService.cs new file mode 100644 index 0000000..3ac29d6 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Services/IGeocodingService.cs @@ -0,0 +1,9 @@ +using Costasdev.Busurbano.Backend.Types.Planner; + +namespace Costasdev.Busurbano.Backend.Services; + +public interface IGeocodingService +{ + Task<List<PlannerSearchResult>> GetAutocompleteAsync(string query); + Task<PlannerSearchResult?> GetReverseGeocodeAsync(double lat, double lon); +} diff --git a/src/Costasdev.Busurbano.Backend/Services/NominatimGeocodingService.cs b/src/Costasdev.Busurbano.Backend/Services/NominatimGeocodingService.cs new file mode 100644 index 0000000..01e57f1 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Services/NominatimGeocodingService.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using Costasdev.Busurbano.Backend.Configuration; +using Costasdev.Busurbano.Backend.Types.Nominatim; +using Costasdev.Busurbano.Backend.Types.Planner; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Costasdev.Busurbano.Backend.Services; + +public class NominatimGeocodingService : IGeocodingService +{ + private readonly HttpClient _httpClient; + private readonly IMemoryCache _cache; + private readonly ILogger<NominatimGeocodingService> _logger; + private readonly AppConfiguration _config; + + private const string GaliciaBounds = "-9.3,43.8,-6.7,41.7"; + + public NominatimGeocodingService(HttpClient httpClient, IMemoryCache cache, ILogger<NominatimGeocodingService> logger, IOptions<AppConfiguration> config) + { + _httpClient = httpClient; + _cache = cache; + _logger = logger; + _config = config.Value; + + // Nominatim requires a User-Agent + if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent")) + { + _httpClient.DefaultRequestHeaders.Add("User-Agent", "Busurbano/1.0 (https://github.com/arielcostas/Busurbano)"); + } + } + + public async Task<List<PlannerSearchResult>> GetAutocompleteAsync(string query) + { + if (string.IsNullOrWhiteSpace(query)) return new List<PlannerSearchResult>(); + + var cacheKey = $"nominatim_autocomplete_{query.ToLowerInvariant()}"; + if (_cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedResults) && cachedResults != null) + { + return cachedResults; + } + + try + { + var url = $"{_config.NominatimBaseUrl}/search?q={Uri.EscapeDataString(query)}&format=jsonv2&viewbox={GaliciaBounds}&bounded=1&countrycodes=es&addressdetails=1"; + var response = await _httpClient.GetFromJsonAsync<List<NominatimSearchResult>>(url); + + var results = response?.Select(MapToPlannerSearchResult).ToList() ?? new List<PlannerSearchResult>(); + + _cache.Set(cacheKey, results, TimeSpan.FromMinutes(30)); + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Nominatim autocomplete results from {Url}", _config.NominatimBaseUrl); + return new List<PlannerSearchResult>(); + } + } + + public async Task<PlannerSearchResult?> GetReverseGeocodeAsync(double lat, double lon) + { + var cacheKey = $"nominatim_reverse_{lat:F5}_{lon:F5}"; + if (_cache.TryGetValue(cacheKey, out PlannerSearchResult? cachedResult) && cachedResult != null) + { + return cachedResult; + } + + try + { + var url = $"{_config.NominatimBaseUrl}/reverse?lat={lat.ToString(CultureInfo.InvariantCulture)}&lon={lon.ToString(CultureInfo.InvariantCulture)}&format=jsonv2&addressdetails=1"; + var response = await _httpClient.GetFromJsonAsync<NominatimSearchResult>(url); + + if (response == null) return null; + + var result = MapToPlannerSearchResult(response); + + _cache.Set(cacheKey, result, TimeSpan.FromMinutes(60)); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Nominatim reverse geocode results from {Url}", _config.NominatimBaseUrl); + return null; + } + } + + private PlannerSearchResult MapToPlannerSearchResult(NominatimSearchResult result) + { + var name = result.Address?.Road ?? result.DisplayName?.Split(',').FirstOrDefault(); + var label = result.DisplayName; + + return new PlannerSearchResult + { + Name = name, + Label = label, + Lat = double.TryParse(result.Lat, CultureInfo.InvariantCulture, out var lat) ? lat : 0, + Lon = double.TryParse(result.Lon, CultureInfo.InvariantCulture, out var lon) ? lon : 0, + Layer = result.Type + }; + } +} diff --git a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs index b7e2d3f..704139d 100644 --- a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs @@ -30,77 +30,6 @@ public class OtpService _feedService = feedService; } - public async Task<List<PlannerSearchResult>> GetAutocompleteAsync(string query) - { - if (string.IsNullOrWhiteSpace(query)) return new List<PlannerSearchResult>(); - - var cacheKey = $"otp_autocomplete_{query.ToLowerInvariant()}"; - if (_cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedResults) && cachedResults != null) - { - return cachedResults; - } - - try - { - // https://planificador-rutas-api.vigo.org/v1/autocomplete?text=XXXX&layers=venue,street,address&lang=es - var url = $"{_config.OtpGeocodingBaseUrl}/autocomplete?text={Uri.EscapeDataString(query)}&layers=venue,address&lang=es"; - var response = await _httpClient.GetFromJsonAsync<OtpGeocodeResponse>(url); - - var results = response?.Features.Select(f => new PlannerSearchResult - { - Name = f.Properties?.Name, - Label = $"{f.Properties?.PostalCode} {f.Properties?.LocalAdmin}, {f.Properties?.Region}", - Layer = f.Properties?.Layer, - Lat = f.Geometry?.Coordinates.Count > 1 ? f.Geometry.Coordinates[1] : 0, - Lon = f.Geometry?.Coordinates.Count > 0 ? f.Geometry.Coordinates[0] : 0 - }).ToList() ?? new List<PlannerSearchResult>(); - - _cache.Set(cacheKey, results, TimeSpan.FromMinutes(30)); // Cache for 30 mins - return results; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching autocomplete results"); - return new List<PlannerSearchResult>(); - } - } - - public async Task<PlannerSearchResult?> GetReverseGeocodeAsync(double lat, double lon) - { - var cacheKey = $"otp_reverse_{lat:F5}_{lon:F5}"; - if (_cache.TryGetValue(cacheKey, out PlannerSearchResult? cachedResult) && cachedResult != null) - { - return cachedResult; - } - - try - { - // https://planificador-rutas-api.vigo.org/v1/reverse?point.lat=LAT&point.lon=LON&lang=es - var url = $"{_config.OtpGeocodingBaseUrl}/reverse?point.lat={lat.ToString(CultureInfo.InvariantCulture)}&point.lon={lon.ToString(CultureInfo.InvariantCulture)}&lang=es"; - var response = await _httpClient.GetFromJsonAsync<OtpGeocodeResponse>(url); - - var feature = response?.Features.FirstOrDefault(); - if (feature == null) return null; - - var result = new PlannerSearchResult - { - Name = feature.Properties?.Name, - Label = $"{feature.Properties?.PostalCode} {feature.Properties?.LocalAdmin}, {feature.Properties?.Region}", - Layer = feature.Properties?.Layer, - Lat = feature.Geometry?.Coordinates.Count > 1 ? feature.Geometry.Coordinates[1] : 0, - Lon = feature.Geometry?.Coordinates.Count > 0 ? feature.Geometry.Coordinates[0] : 0 - }; - - _cache.Set(cacheKey, result, TimeSpan.FromMinutes(60)); // Cache for 1 hour - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching reverse geocode results"); - return null; - } - } - private Leg MapLeg(OtpLeg otpLeg) { return new Leg @@ -240,8 +169,10 @@ public class OtpService TransitTimeSeconds = node.DurationSeconds - node.WalkSeconds - node.WaitingSeconds, WaitingTimeSeconds = node.WaitingSeconds, Legs = legs, - CashFareEuro = fares.CashFareEuro, - CardFareEuro = fares.CardFareEuro + CashFare = fares.CashFareEuro, + CashFareIsTotal = fares.CashFareIsTotal, + CardFare = fares.CardFareEuro, + CardFareIsTotal = fares.CardFareIsTotal }; } |
