aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs5
-rw-r--r--src/Costasdev.Busurbano.Backend/Services/OtpService.cs47
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json8
-rw-r--r--src/frontend/app/routes/planner.tsx21
4 files changed, 53 insertions, 28 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
index 3224515..db0b573 100644
--- a/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
+++ b/src/Costasdev.Busurbano.Backend/Configuration/AppConfiguration.cs
@@ -14,6 +14,11 @@ public class AppConfiguration
public int MaxWalkTime { get; set; } = 20;
public int NumItineraries { get; set; } = 4;
+ // Comfort/Slack Parameters
+ public int TransferSlackSeconds { get; set; } = 120; // Extra buffer for transfers
+ public int MinTransferTimeSeconds { get; set; } = 120; // Minimum transfer time
+ public double WalkReluctance { get; set; } = 2.0; // Slightly penalize walking to add slack
+
// Fare Configuration
public double FareCashPerBus { get; set; } = 1.63;
public double FareCardPerBus { get; set; } = 0.67;
diff --git a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs
index 87895d3..6b01c5c 100644
--- a/src/Costasdev.Busurbano.Backend/Services/OtpService.cs
+++ b/src/Costasdev.Busurbano.Backend/Services/OtpService.cs
@@ -98,9 +98,26 @@ public class OtpService
{
try
{
- var date = time ?? DateTime.Now;
- var dateStr = date.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture);
- var timeStr = date.ToString("h:mm tt", CultureInfo.InvariantCulture);
+ // Convert the provided time to Europe/Madrid local time and pass explicit offset to OTP
+ var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid");
+ DateTime utcReference;
+ if (time.HasValue)
+ {
+ var t = time.Value;
+ if (t.Kind == DateTimeKind.Unspecified)
+ t = DateTime.SpecifyKind(t, DateTimeKind.Utc);
+ utcReference = t.Kind == DateTimeKind.Utc ? t : t.ToUniversalTime();
+ }
+ else
+ {
+ utcReference = DateTime.UtcNow;
+ }
+
+ var localMadrid = TimeZoneInfo.ConvertTimeFromUtc(utcReference, tz);
+ var offsetSeconds = (int)tz.GetUtcOffset(localMadrid).TotalSeconds;
+
+ var dateStr = localMadrid.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture);
+ var timeStr = localMadrid.ToString("HH:mm", CultureInfo.InvariantCulture);
var queryParams = new Dictionary<string, string>
{
@@ -116,9 +133,15 @@ public class OtpService
{ "walkSpeed", _config.WalkSpeed.ToString(CultureInfo.InvariantCulture) },
{ "maxWalkDistance", _config.MaxWalkDistance.ToString() }, // Note: OTP might ignore this if it's too small
{ "optimize", "QUICK" },
- { "wheelchair", "false" }
+ { "wheelchair", "false" },
+ { "timeZoneOffset", offsetSeconds.ToString(CultureInfo.InvariantCulture) }
};
+ // Add slack/comfort parameters
+ queryParams["transferSlack"] = _config.TransferSlackSeconds.ToString();
+ queryParams["minTransferTime"] = _config.MinTransferTimeSeconds.ToString();
+ queryParams["walkReluctance"] = _config.WalkReluctance.ToString(CultureInfo.InvariantCulture);
+
var queryString = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
var url = $"{_config.OtpPlannerBaseUrl}/plan?{queryString}";
@@ -140,14 +163,12 @@ public class OtpService
private RoutePlan MapToRoutePlan(OtpPlan otpPlan)
{
- // Compute time offset: OTP's "date" field is the server time in millis
- var otpServerTime = DateTimeOffset.FromUnixTimeMilliseconds(otpPlan.Date);
- var now = DateTimeOffset.Now;
- var timeOffsetSeconds = (long)(otpServerTime - now).TotalSeconds;
+ // OTP times are already correct when requested with explicit offset
+ var timeOffsetSeconds = 0L;
return new RoutePlan
{
- Itineraries = otpPlan.Itineraries.Select(MapItinerary).ToList(),
+ Itineraries = otpPlan.Itineraries.Select(MapItinerary).OrderBy(i => i.DurationSeconds).ToList(),
TimeOffsetSeconds = timeOffsetSeconds
};
}
@@ -185,8 +206,8 @@ public class OtpService
return new Itinerary
{
DurationSeconds = otpItinerary.Duration,
- StartTime = DateTimeOffset.FromUnixTimeMilliseconds(otpItinerary.StartTime).LocalDateTime, // Assuming local time or handling timezone
- EndTime = DateTimeOffset.FromUnixTimeMilliseconds(otpItinerary.EndTime).LocalDateTime,
+ StartTime = DateTimeOffset.FromUnixTimeMilliseconds(otpItinerary.StartTime).UtcDateTime,
+ EndTime = DateTimeOffset.FromUnixTimeMilliseconds(otpItinerary.EndTime).UtcDateTime,
WalkDistanceMeters = otpItinerary.WalkDistance,
WalkTimeSeconds = otpItinerary.WalkTime,
TransitTimeSeconds = otpItinerary.TransitTime,
@@ -211,8 +232,8 @@ public class OtpService
RouteTextColor = otpLeg.RouteTextColor,
From = MapPlace(otpLeg.From),
To = MapPlace(otpLeg.To),
- StartTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.StartTime).LocalDateTime,
- EndTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.EndTime).LocalDateTime,
+ StartTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.StartTime).UtcDateTime,
+ EndTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.EndTime).UtcDateTime,
DistanceMeters = otpLeg.Distance,
Geometry = DecodePolyline(otpLeg.LegGeometry?.Points),
Steps = otpLeg.Steps.Select(MapStep).ToList(),
diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json
index d7c94ea..ab18c7b 100644
--- a/src/frontend/app/i18n/locales/es-ES.json
+++ b/src/frontend/app/i18n/locales/es-ES.json
@@ -120,8 +120,8 @@
"searching_ellipsis": "Buscando…",
"results": "Resultados",
"close": "Cerrar",
- "results_title": "Results",
- "clear": "Clear",
+ "results_title": "Resultados",
+ "clear": "Borrar",
"no_routes_found": "No se encontraron rutas",
"no_routes_message": "No pudimos encontrar una ruta para tu viaje. Intenta cambiar la hora o las ubicaciones.",
"walk": "Caminar",
@@ -129,8 +129,8 @@
"from_to": "De {{from}} a {{to}}",
"itinerary_details": "Detalles del itinerario",
"back": "← Atrás",
- "cash_fare": "{{amount}} €",
- "card_fare": "{{amount}} €"
+ "fare": "{{amount}} €",
+ "free": "Gratuito"
},
"common": {
"loading": "Cargando...",
diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx
index 0f52fef..5121dce 100644
--- a/src/frontend/app/routes/planner.tsx
+++ b/src/frontend/app/routes/planner.tsx
@@ -104,10 +104,12 @@ const ItinerarySummary = ({
const startTime = new Date(itinerary.startTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
+ timeZone: "Europe/Madrid",
});
const endTime = new Date(itinerary.endTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
+ timeZone: "Europe/Madrid",
});
const walkTotals = sumWalkMetrics(itinerary.legs);
@@ -121,15 +123,6 @@ const ItinerarySummary = ({
itinerary.cardFareEuro ?? busLegsCount * FARE_CARD_PER_BUS
).toFixed(2);
- // Format currency based on locale (ES/GL: "1,50 €", EN: "€1.50")
- const formatCurrency = (amount: string) => {
- const isSpanishOrGalician =
- i18n.language.startsWith("es") || i18n.language.startsWith("gl");
- return isSpanishOrGalician
- ? t("planner.cash_fare", { amount })
- : t("planner.cash_fare", { amount });
- };
-
return (
<div
className="bg-white dark:bg-slate-800 p-4 rounded-lg shadow mb-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-slate-700 border border-gray-200 dark:border-slate-700"
@@ -193,11 +186,15 @@ const ItinerarySummary = ({
<span className="flex items-center gap-3">
<span className="flex items-center gap-1 font-semibold text-slate-700 dark:text-slate-300">
<Coins className="w-4 h-4" />
- {formatCurrency(cashFare)}
+ {cashFare === "0.00"
+ ? t("planner.free")
+ : t("planner.fare", { amount: cashFare })}
</span>
<span className="flex items-center gap-1 text-slate-600 dark:text-slate-400">
<CreditCard className="w-4 h-4" />
- {t("planner.card_fare", { amount: cardFare })}
+ {cardFare === "0.00"
+ ? t("planner.free")
+ : t("planner.fare", { amount: cardFare })}
</span>
</span>
</div>
@@ -628,6 +625,7 @@ const ItineraryDetail = ({
{new Date(leg.startTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
+ timeZone: "Europe/Madrid",
})}{" "}
-{" "}
{(
@@ -812,6 +810,7 @@ export default function PlannerPage() {
? new Date(searchTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
+ timeZone: "Europe/Madrid",
})
: null;