Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 20 additions & 29 deletions internal/restapi/arrival_and_departure_for_stop_handler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package restapi

import (
"context"
"database/sql"
"errors"
"net/http"
Expand Down Expand Up @@ -37,7 +36,7 @@ func parseArrivalAndDepartureParams(r *http.Request, loc ...*time.Location) (Arr
fieldErrors := make(map[string][]string)

const maxMinutesAfter = 240
const maxMinutesBefore = 60
const maxMinutesBefore = 240

// Validate minutesAfter
if minutesAfterStr := r.URL.Query().Get("minutesAfter"); minutesAfterStr != "" {
Expand Down Expand Up @@ -185,7 +184,11 @@ func (api *RestAPI) arrivalAndDepartureForStopHandler(w http.ResponseWriter, r *

stopAgency, err := api.GtfsManager.GtfsDB.Queries.GetAgency(ctx, stopAgencyID)
if err != nil {
api.serverErrorResponse(w, r, err)
if errors.Is(err, sql.ErrNoRows) {
api.sendNotFound(w, r)
} else {
api.serverErrorResponse(w, r, err)
}
return
}

Expand Down Expand Up @@ -351,14 +354,20 @@ func (api *RestAPI) arrivalAndDepartureForStopHandler(w http.ResponseWriter, r *
predicted = false
}

if vehicle != nil && vehicle.Position != nil {
distanceFromStop = api.getBlockDistanceToStop(ctx, tripID, stopCode, vehicle, serviceDate)

numberOfStopsAwayPtr := api.getNumberOfStopsAway(ctx, tripID, int(targetStopTime.StopSequence), vehicle, serviceDate)
if numberOfStopsAwayPtr != nil {
numberOfStopsAway = *numberOfStopsAwayPtr
} else {
numberOfStopsAway = -1
// Interpolate the block schedule at currentTime−scheduleDeviation.
// GPS lat/lon doesn't enter the formula. No RT vehicle → no shift.
effectiveTime := currentTime
if vehicle != nil && vehicle.Trip != nil && vehicle.Trip.ID.ID != "" {
blockTripIDs := api.blockTripIDsSortedByStartTime(ctx,
api.blockTripIDsForServiceDate(ctx, tripID, serviceDate))
if dev, hasRT := api.GetScheduleDeviationForBlock(ctx, blockTripIDs, serviceDate, currentTime); hasRT {
effectiveTime = currentTime.Add(-time.Duration(dev) * time.Second)
}
Comment thread
Ahmedhossamdev marked this conversation as resolved.
}
if snapshot := api.computeScheduledBlockSnapshot(ctx, tripID, effectiveTime, serviceDate); snapshot != nil {
if d, n, ok := snapshot.metricsForStop(tripID, int(targetStopTime.StopSequence)); ok {
distanceFromStop = d
numberOfStopsAway = n
}
}
}
Expand Down Expand Up @@ -710,21 +719,3 @@ func (api *RestAPI) getPredictedTimes(

return predictedArrival, predictedDeparture, true
}

func (api *RestAPI) getNumberOfStopsAway(ctx context.Context, targetTripID string, targetStopSequence int, vehicle *gtfs.Vehicle, serviceDate time.Time) *int {
currentVehicleStopSequence := getCurrentVehicleStopSequence(vehicle)
if currentVehicleStopSequence == nil {
return nil
}

activeTripID := GetVehicleActiveTripID(vehicle)
if activeTripID == "" {
activeTripID = targetTripID
}

targetGlobalSeq := api.getBlockSequenceForStopSequence(ctx, targetTripID, targetStopSequence, serviceDate)
vehicleGlobalSeq := api.getBlockSequenceForStopSequence(ctx, activeTripID, *currentVehicleStopSequence, serviceDate)

numberOfStopsAway := targetGlobalSeq - vehicleGlobalSeq - 1
return &numberOfStopsAway
}
34 changes: 1 addition & 33 deletions internal/restapi/arrival_and_departure_for_stop_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,38 +345,6 @@ func TestGetPredictedTimes_EqualArrivalDeparture(t *testing.T) {
assert.False(t, predicted)
}

func TestGetBlockDistanceToStop_NilVehicle(t *testing.T) {
api := createTestApi(t)
defer api.Shutdown()

result := api.getBlockDistanceToStop(t.Context(), "test_trip", "test_stop", nil, time.Now())

assert.Equal(t, 0.0, result)
}

func TestGetBlockDistanceToStop_NoPosition(t *testing.T) {
api := createTestApi(t)
defer api.Shutdown()
ctx := context.Background()

vehicle := &gtfs.Vehicle{
Position: nil,
}

result := api.getBlockDistanceToStop(ctx, "test_trip", "test_stop", vehicle, time.Now())

assert.Equal(t, 0.0, result)
}

func TestGetNumberOfStopsAway_NilCurrentSequence(t *testing.T) {
api := createTestApi(t)
vehicle := &gtfs.Vehicle{}

result := api.getNumberOfStopsAway(context.Background(), "test_trip", 5, vehicle, time.Now())

assert.Nil(t, result)
}

func TestParseArrivalAndDepartureParams_AllParameters(t *testing.T) {
api := createTestApi(t)
defer api.Shutdown()
Expand Down Expand Up @@ -449,7 +417,7 @@ func TestParseArrivalAndDepartureParams_LargeValues(t *testing.T) {

assert.Empty(t, errs)
assert.Equal(t, 240, params.MinutesAfter)
assert.Equal(t, 60, params.MinutesBefore)
assert.Equal(t, 240, params.MinutesBefore)
}

func TestArrivalAndDepartureForStopHandlerWithMalformedID(t *testing.T) {
Expand Down
114 changes: 64 additions & 50 deletions internal/restapi/arrivals_and_departures_for_stop_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package restapi

import (
"context"
"database/sql"
"errors"
"log/slog"
"net/http"
"strconv"
Expand All @@ -24,7 +26,8 @@ type ArrivalsStopParams struct {

// parseArrivalsAndDeparturesParams parses and validates parameters.
func (api *RestAPI) parseArrivalsAndDeparturesParams(r *http.Request) (ArrivalsStopParams, map[string][]string) {
const maxBefore = 60 * time.Minute
// Both windows cap at 240 min, matching Java and the absence of any spec maximum.
const maxBefore = 240 * time.Minute
const maxAfter = 240 * time.Minute

params := ArrivalsStopParams{
Expand Down Expand Up @@ -105,7 +108,13 @@ func (api *RestAPI) arrivalsAndDeparturesForStopHandler(w http.ResponseWriter, r

agency, err := api.GtfsManager.GtfsDB.Queries.GetAgency(ctx, stopAgencyID)
if err != nil {
api.serverErrorResponse(w, r, err)
// Unknown agency (e.g. wrong case in the URL like "hsr_2543" when the
// agency_id is "HSR") is a client error — surface 404 instead of 500.
if errors.Is(err, sql.ErrNoRows) {
api.sendNotFound(w, r)
} else {
api.serverErrorResponse(w, r, err)
}
return
}

Expand Down Expand Up @@ -338,61 +347,66 @@ func (api *RestAPI) arrivalsAndDeparturesForStopHandler(w http.ResponseWriter, r
predictedDepartureTime = predDep
}

if vehicle != nil {
// Use route.AgencyID instead of stopAgencyID for BuildTripStatus
status, statusErr := api.BuildTripStatus(ctx, route.AgencyID, st.TripID, nil, serviceMidnight, params.Time)
if statusErr != nil {
api.Logger.Warn("BuildTripStatus failed for arrival",
"tripID", st.TripID, "error", statusErr)
}
if status != nil {
tripStatus = status
// Always built — Java attaches a BlockLocation (real-time or scheduled) to
// every arrival, so tripStatus is always non-null.
status, statusErr := api.BuildTripStatus(ctx, route.AgencyID, st.TripID, nil, serviceMidnight, params.Time)
if statusErr != nil {
api.Logger.Warn("BuildTripStatus failed for arrival",
"tripID", st.TripID, "error", statusErr)
}
if status != nil {
tripStatus = status

if status.NextStop != "" {
_, nextStopID, err := utils.ExtractAgencyIDAndCodeID(status.NextStop)
if err == nil {
stopIDSet[nextStopID] = true
}
if status.NextStop != "" {
_, nextStopID, err := utils.ExtractAgencyIDAndCodeID(status.NextStop)
if err == nil {
stopIDSet[nextStopID] = true
}
if status.ClosestStop != "" {
_, closestStopID, err := utils.ExtractAgencyIDAndCodeID(status.ClosestStop)
if err == nil {
stopIDSet[closestStopID] = true
}
}
if status.ClosestStop != "" {
_, closestStopID, err := utils.ExtractAgencyIDAndCodeID(status.ClosestStop)
if err == nil {
stopIDSet[closestStopID] = true
}
}

if vehicle.Position != nil {
distanceFromStop = api.getBlockDistanceToStop(ctx, st.TripID, stopCode, vehicle, params.Time)

numberOfStopsAwayPtr := api.getNumberOfStopsAway(ctx, st.TripID, int(st.StopSequence), vehicle, params.Time)
if numberOfStopsAwayPtr != nil {
numberOfStopsAway = *numberOfStopsAwayPtr
} else {
numberOfStopsAway = -1
}
// Interpolate the block schedule at params.Time−scheduleDeviation.
// See GetScheduleDeviationForBlock for the deviation rules.
effectiveTime := params.Time
if vehicle != nil && vehicle.Trip != nil && vehicle.Trip.ID.ID != "" {
blockTripIDs := api.blockTripIDsSortedByStartTime(ctx,
api.blockTripIDsForServiceDate(ctx, st.TripID, serviceMidnight))
if dev, hasRT := api.GetScheduleDeviationForBlock(ctx, blockTripIDs, serviceMidnight, params.Time); hasRT {
effectiveTime = params.Time.Add(-time.Duration(dev) * time.Second)
Comment thread
Ahmedhossamdev marked this conversation as resolved.
}
}
if snapshot := api.computeScheduledBlockSnapshot(ctx, st.TripID, effectiveTime, serviceMidnight); snapshot != nil {
if d, n, ok := snapshot.metricsForStop(st.TripID, int(st.StopSequence)); ok {
distanceFromStop = d
numberOfStopsAway = n
}
}

// If there's an active trip that's different from the current trip, add it to references
if status.ActiveTripID != "" {
_, activeTripID, err := utils.ExtractAgencyIDAndCodeID(status.ActiveTripID)
if err == nil && activeTripID != st.TripID {
// Check cache for active trip
if _, exists := tripIDSet[activeTripID]; !exists {
activeTrip, err := api.GtfsManager.GtfsDB.Queries.GetTrip(ctx, activeTripID)
if err != nil {
api.Logger.Debug("skipping active trip reference: trip not found",
slog.String("activeTripID", activeTripID),
slog.String("scheduledTripID", st.TripID),
slog.Any("error", err))
// If there's an active trip that's different from the current trip, add it to references
if status.ActiveTripID != "" {
_, activeTripID, err := utils.ExtractAgencyIDAndCodeID(status.ActiveTripID)
if err == nil && activeTripID != st.TripID {
// Check cache for active trip
if _, exists := tripIDSet[activeTripID]; !exists {
activeTrip, err := api.GtfsManager.GtfsDB.Queries.GetTrip(ctx, activeTripID)
if err != nil {
api.Logger.Debug("skipping active trip reference: trip not found",
slog.String("activeTripID", activeTripID),
slog.String("scheduledTripID", st.TripID),
slog.Any("error", err))
} else {
tripIDSet[activeTrip.ID] = &activeTrip
activeRoute, err := api.GtfsManager.GtfsDB.Queries.GetRoute(ctx, activeTrip.RouteID)
if err == nil {
routeIDSet[activeRoute.ID] = &activeRoute
} else {
tripIDSet[activeTrip.ID] = &activeTrip
activeRoute, err := api.GtfsManager.GtfsDB.Queries.GetRoute(ctx, activeTrip.RouteID)
if err == nil {
routeIDSet[activeRoute.ID] = &activeRoute
} else {
api.Logger.Warn("failed to fetch route for active trip reference",
"tripID", activeTripID, "routeID", activeTrip.RouteID, "error", err)
}
api.Logger.Warn("failed to fetch route for active trip reference",
"tripID", activeTripID, "routeID", activeTrip.RouteID, "error", err)
}
}
}
Expand Down
99 changes: 0 additions & 99 deletions internal/restapi/block_distance_helper.go

This file was deleted.

Loading
Loading