aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend/Services
diff options
context:
space:
mode:
Diffstat (limited to 'src/Costasdev.Busurbano.Backend/Services')
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/FareService.cs34
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/IGeocodingService.cs9
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/NominatimGeocodingService.cs101
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/OtpService.cs77
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
};
}