aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend
diff options
context:
space:
mode:
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
-rw-r--r--src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs1
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs14
-rw-r--r--src/Costasdev.Busurbano.Backend/Program.cs1
-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
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/Nominatim/NominatimModels.cs75
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs40
-rw-r--r--src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs6
10 files changed, 225 insertions, 133 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
index 3204e33..9e4d12f 100644
--- a/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
+++ b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
@@ -10,6 +10,7 @@ public class AppConfiguration
[Obsolete]
public required string OtpPlannerBaseUrl { get; set; } = "https://planificador-rutas.vigo.org/otp/routers/default";
public required string OpenTripPlannerBaseUrl { get; set; }
+ public string NominatimBaseUrl { get; set; } = "https://nominatim.openstreetmap.org";
// Default Routing Parameters
public double WalkSpeed { get; set; } = 1.4;
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs
index 7d47383..a7faf44 100644
--- a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs
+++ b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs
@@ -15,18 +15,21 @@ public partial class RoutePlannerController : ControllerBase
{
private readonly ILogger<RoutePlannerController> _logger;
private readonly OtpService _otpService;
+ private readonly IGeocodingService _geocodingService;
private readonly AppConfiguration _config;
private readonly HttpClient _httpClient;
public RoutePlannerController(
ILogger<RoutePlannerController> logger,
OtpService otpService,
+ IGeocodingService geocodingService,
IOptions<AppConfiguration> config,
HttpClient httpClient
)
{
_logger = logger;
_otpService = otpService;
+ _geocodingService = geocodingService;
_config = config.Value;
_httpClient = httpClient;
}
@@ -39,14 +42,14 @@ public partial class RoutePlannerController : ControllerBase
return BadRequest("Query cannot be empty");
}
- var results = await _otpService.GetAutocompleteAsync(query);
+ var results = await _geocodingService.GetAutocompleteAsync(query);
return Ok(results);
}
[HttpGet("reverse")]
public async Task<ActionResult<PlannerSearchResult>> Reverse([FromQuery] double lat, [FromQuery] double lon)
{
- var result = await _otpService.GetReverseGeocodeAsync(lat, lon);
+ var result = await _geocodingService.GetReverseGeocodeAsync(lat, lon);
if (result == null)
{
return NotFound();
@@ -60,13 +63,13 @@ public partial class RoutePlannerController : ControllerBase
[FromQuery] double fromLon,
[FromQuery] double toLat,
[FromQuery] double toLon,
- [FromQuery] DateTimeOffset time,
+ [FromQuery] DateTimeOffset? time,
[FromQuery] bool arriveBy = false)
{
try
{
var requestContent = PlanConnectionContent.Query(
- new PlanConnectionContent.Args(fromLat, fromLon, toLat, toLon, time, arriveBy)
+ new PlanConnectionContent.Args(fromLat, fromLon, toLat, toLon, time ?? DateTimeOffset.Now, arriveBy)
);
var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
@@ -78,7 +81,7 @@ public partial class RoutePlannerController : ControllerBase
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<PlanConnectionResponse>>();
- if (responseBody is not { IsSuccess: true } || responseBody.Data?.PlanConnection.Edges.Length == 0)
+ if (responseBody is not { IsSuccess: true })
{
LogErrorFetchingRoutes(response.StatusCode, await response.Content.ReadAsStringAsync());
return StatusCode(500, "An error occurred while planning the route.");
@@ -96,5 +99,4 @@ public partial class RoutePlannerController : ControllerBase
[LoggerMessage(LogLevel.Error, "Error fetching route planning, received {statusCode} {responseBody}")]
partial void LogErrorFetchingRoutes(HttpStatusCode? statusCode, string responseBody);
-
}
diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs
index 94ebe62..97e7354 100644
--- a/src/Costasdev.Busurbano.Backend/Program.cs
+++ b/src/Costasdev.Busurbano.Backend/Program.cs
@@ -36,6 +36,7 @@ builder.Services.AddScoped<IArrivalsProcessor, ShapeProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, FeedConfigProcessor>();
builder.Services.AddScoped<ArrivalsPipeline>();
+builder.Services.AddHttpClient<IGeocodingService, NominatimGeocodingService>();
builder.Services.AddHttpClient<OtpService>();
builder.Services.AddScoped<VitrasaTransitProvider>();
builder.Services.AddScoped<RenfeTransitProvider>();
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
};
}
diff --git a/src/Costasdev.Busurbano.Backend/Types/Nominatim/NominatimModels.cs b/src/Costasdev.Busurbano.Backend/Types/Nominatim/NominatimModels.cs
new file mode 100644
index 0000000..a73cdd2
--- /dev/null
+++ b/src/Costasdev.Busurbano.Backend/Types/Nominatim/NominatimModels.cs
@@ -0,0 +1,75 @@
+using System.Text.Json.Serialization;
+
+namespace Costasdev.Busurbano.Backend.Types.Nominatim;
+
+public class NominatimSearchResult
+{
+ [JsonPropertyName("place_id")]
+ public long PlaceId { get; set; }
+
+ [JsonPropertyName("licence")]
+ public string? Licence { get; set; }
+
+ [JsonPropertyName("osm_type")]
+ public string? OsmType { get; set; }
+
+ [JsonPropertyName("osm_id")]
+ public long OsmId { get; set; }
+
+ [JsonPropertyName("lat")]
+ public string? Lat { get; set; }
+
+ [JsonPropertyName("lon")]
+ public string? Lon { get; set; }
+
+ [JsonPropertyName("display_name")]
+ public string? DisplayName { get; set; }
+
+ [JsonPropertyName("address")]
+ public NominatimAddress? Address { get; set; }
+
+ [JsonPropertyName("extratags")]
+ public Dictionary<string, string>? ExtraTags { get; set; }
+
+ [JsonPropertyName("category")]
+ public string? Category { get; set; }
+
+ [JsonPropertyName("type")]
+ public string? Type { get; set; }
+
+ [JsonPropertyName("importance")]
+ public double Importance { get; set; }
+}
+
+public class NominatimAddress
+{
+ [JsonPropertyName("house_number")]
+ public string? HouseNumber { get; set; }
+
+ [JsonPropertyName("road")]
+ public string? Road { get; set; }
+
+ [JsonPropertyName("suburb")]
+ public string? Suburb { get; set; }
+
+ [JsonPropertyName("city")]
+ public string? City { get; set; }
+
+ [JsonPropertyName("municipality")]
+ public string? Municipality { get; set; }
+
+ [JsonPropertyName("county")]
+ public string? County { get; set; }
+
+ [JsonPropertyName("state")]
+ public string? State { get; set; }
+
+ [JsonPropertyName("postcode")]
+ public string? Postcode { get; set; }
+
+ [JsonPropertyName("country")]
+ public string? Country { get; set; }
+
+ [JsonPropertyName("country_code")]
+ public string? CountryCode { get; set; }
+}
diff --git a/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs b/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs
index b67663d..2c076a2 100644
--- a/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs
+++ b/src/Costasdev.Busurbano.Backend/Types/Otp/OtpModels.cs
@@ -163,43 +163,3 @@ public class OtpWalkStep
[JsonPropertyName("lon")]
public double Lon { get; set; }
}
-
-// Geocoding Models (Pelias-like)
-public class OtpGeocodeResponse
-{
- [JsonPropertyName("features")]
- public List<OtpGeocodeFeature> Features { get; set; } = new();
-}
-
-public class OtpGeocodeFeature
-{
- [JsonPropertyName("geometry")]
- public OtpGeocodeGeometry? Geometry { get; set; }
-
- [JsonPropertyName("properties")]
- public OtpGeocodeProperties? Properties { get; set; }
-}
-
-public class OtpGeocodeGeometry
-{
- [JsonPropertyName("coordinates")]
- public List<double> Coordinates { get; set; } = new(); // [lon, lat]
-}
-
-public class OtpGeocodeProperties
-{
- [JsonPropertyName("name")]
- public string? Name { get; set; }
-
- [JsonPropertyName("postalcode")]
- public string? PostalCode { get; set; }
-
- [JsonPropertyName("localadmin")]
- public string? LocalAdmin { get; set; }
-
- [JsonPropertyName("region")]
- public string? Region { get; set; }
-
- [JsonPropertyName("layer")]
- public string? Layer { get; set; }
-}
diff --git a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs
index f88942f..a0cf754 100644
--- a/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs
+++ b/src/Costasdev.Busurbano.Backend/Types/Planner/PlannerResponse.cs
@@ -16,8 +16,10 @@ public class Itinerary
public double TransitTimeSeconds { get; set; }
public double WaitingTimeSeconds { get; set; }
public List<Leg> Legs { get; set; } = [];
- public decimal? CashFareEuro { get; set; }
- public decimal? CardFareEuro { get; set; }
+ public decimal? CashFare { get; set; }
+ public bool? CashFareIsTotal { get; set; }
+ public decimal? CardFare { get; set; }
+ public bool? CardFareIsTotal { get; set; }
}
public class Leg