Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
38a7b54
feat(maglev): implement arrivals-and-departures-for-location endpoint
ARCoder181105 Mar 28, 2026
7c79faf
fix double prefix in situation id
ARCoder181105 Mar 28, 2026
f7a3490
fixes
ARCoder181105 Mar 28, 2026
2ee7f01
feat: achieve full parity for arrivals-and-departures-for-location en…
ARCoder181105 Mar 29, 2026
4c74707
fix: correct lastUpdateTime serialization, nearbyStopIds radius, and …
ARCoder181105 Apr 26, 2026
3c21702
style: replace map[string]interface{} with map[string]any in response…
ARCoder181105 Apr 26, 2026
3b9bffc
fix: block-based prediction propagation and blockTripSequence parity
ARCoder181105 Apr 26, 2026
5b09806
Merge branch 'OneBusAway:main' into feature/arrivals-departures-for-l…
ARCoder181105 May 14, 2026
2ae4e5a
added ASC to query.sql
ARCoder181105 May 14, 2026
074a94e
internal/restapi/arrivals_and_departures_for_location.go
ARCoder181105 May 14, 2026
b34b613
ran fmt
ARCoder181105 May 14, 2026
faeb9e1
rafactor arrivalsAndDeparturesForLocationHandler as per sonar cloud
ARCoder181105 May 14, 2026
0204e8b
Refactor arrivalsAndDeparturesForLocationHandler to reduce cognitive …
ARCoder181105 May 14, 2026
96eab9c
Refactor buildArrivalsFromLocationStopTimes to reduce cognitive compl…
ARCoder181105 May 14, 2026
1cd4a6c
Refactor location handlers to reduce Cognitive Complexity below 15
ARCoder181105 May 14, 2026
e069e08
Remove unused params variable from fetchStopTimesForDayOffset
ARCoder181105 May 14, 2026
9d3989b
Remove unused queryTime parameter from getLocationNearbyStops
ARCoder181105 May 14, 2026
16533b7
fix: achieve API parity for location arrivals (filters, layovers, fre…
ARCoder181105 May 17, 2026
17b5af7
fix: remove non-standard scheduled field and add position-based numbe…
ARCoder181105 May 17, 2026
22f6d5f
Merge branch 'main' into feature/arrivals-departures-for-location
ARCoder181105 May 17, 2026
7e11bb5
layover fix
ARCoder181105 May 18, 2026
a84f621
fix(api): add missing agency prefix to routeId and stopId in situatio…
ARCoder181105 May 20, 2026
94d3ede
updated openaAPI
ARCoder181105 May 20, 2026
de22fdf
some parity minor fixes
ARCoder181105 May 20, 2026
001e99e
Merge branch 'OneBusAway:main' into feature/arrivals-departures-for-l…
ARCoder181105 May 20, 2026
838338c
fixes the test failure dure to time
ARCoder181105 May 20, 2026
5367457
some minor revert and fixes
ARCoder181105 May 22, 2026
ccf440f
file name change added handler at the end
ARCoder181105 May 22, 2026
2223c20
Merge branch 'main' into feature/arrivals-departures-for-location
ARCoder181105 May 23, 2026
6ccc6c4
refactor(restapi): reduce cognitive complexity and parameter count in…
ARCoder181105 May 24, 2026
5da7ad8
fix: address PR feedback for location handler and tests
ARCoder181105 May 24, 2026
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
21 changes: 19 additions & 2 deletions gtfsdb/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1415,13 +1415,30 @@ func (c *Client) buildBlockLayoverIndex(ctx context.Context, staticData *gtfs.St
continue
}

layoverStart := int64(lastStopCurrent.ArrivalTime)
layoverEnd := int64(firstStopNext.DepartureTime)

// ArrivalTime/DepartureTime are nanoseconds since service-day midnight
// (they come from the go-gtfs library as time.Duration values).
// If the layover appears negative, check if it's a valid midnight wraparound
// (e.g. within a reasonable layover threshold, like 4 hours).
const dayNs = int64(24 * time.Hour) // 86_400_000_000_000 ns
const maxLayoverNs = int64(4 * time.Hour) // 14_400_000_000_000 ns
if layoverStart > layoverEnd {
if (layoverEnd+dayNs)-layoverStart < maxLayoverNs {
layoverEnd += dayNs // It crosses midnight, shift it by 24h
} else {
continue // It's invalid data
}
}

err := qtx.CreateBlockLayover(ctx, CreateBlockLayoverParams{
BlockID: key.blockID,
ServiceID: key.serviceID,
RouteID: nextTrip.Route.Id,
LayoverStopID: lastStopCurrent.Stop.Id,
LayoverStart: int64(lastStopCurrent.DepartureTime),
LayoverEnd: int64(firstStopNext.ArrivalTime),
LayoverStart: layoverStart,
LayoverEnd: layoverEnd,
NextTripID: nextTrip.ID,
})
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion gtfsdb/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,8 @@ FROM
JOIN routes ON trips.route_id = routes.id
JOIN agencies a ON routes.agency_id = a.id
WHERE
stop_times.stop_id IN (sqlc.slice('stop_ids'));
stop_times.stop_id IN (sqlc.slice('stop_ids'))
ORDER BY a.id ASC, stop_times.stop_id ASC;

-- name: GetStopTimesForTrip :many
SELECT
Expand Down
1 change: 1 addition & 0 deletions gtfsdb/query.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/models/arrival_and_departure.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type ArrivalAndDeparture struct {
DistanceFromStop float64 `json:"distanceFromStop"`
Frequency *Frequency `json:"frequency"`
HistoricalOccupancy string `json:"historicalOccupancy"`
LastUpdateTime ModelTime `json:"lastUpdateTime,omitzero"`
LastUpdateTime ModelTime `json:"lastUpdateTime"`
NumberOfStopsAway int `json:"numberOfStopsAway"`
OccupancyStatus string `json:"occupancyStatus"`
Predicted bool `json:"predicted"`
Expand Down
32 changes: 32 additions & 0 deletions internal/models/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,38 @@ func NewArrivalsAndDepartureResponse(arrivalsAndDepartures any, references Refer
return NewOKResponse(data, c)
}

func NewArrivalsAndDeparturesForLocationResponse(
arrivalsAndDepartures []ArrivalAndDeparture,
references ReferencesModel,
nearbyStopIds []StopWithDistance,
situationIds []string,
stopIds []string,
limitExceeded bool,
c clock.Clock,
) ResponseModel {
if nearbyStopIds == nil {
nearbyStopIds = []StopWithDistance{}
}
if situationIds == nil {
situationIds = []string{}
}
if stopIds == nil {
stopIds = []string{}
}
entryData := map[string]any{
"arrivalsAndDepartures": arrivalsAndDepartures,
"limitExceeded": limitExceeded,
"nearbyStopIds": nearbyStopIds,
"situationIds": situationIds,
"stopIds": stopIds,
}
data := map[string]any{
"entry": entryData,
"references": references,
}
return NewOKResponse(data, c)
}
Comment thread
ARCoder181105 marked this conversation as resolved.

// NewResponse creates a standard response using the provided clock.
func NewResponse(code int, data any, text string, c clock.Clock) ResponseModel {
return ResponseModel{
Expand Down
18 changes: 17 additions & 1 deletion internal/models/situation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Situation struct {
ActiveWindows []ActiveWindow `json:"activeWindows"`
AllAffects []AffectedEntity `json:"allAffects"`
ConsequenceMessage string `json:"consequenceMessage"`
Consequences []any `json:"consequences"`
Consequences []Consequence `json:"consequences"`
PublicationWindows []any `json:"publicationWindows"`
Reason string `json:"reason"`
Severity string `json:"severity"`
Expand All @@ -15,6 +15,22 @@ type Situation struct {
URL *TranslatedString `json:"url,omitempty"`
}

type Consequence struct {
Condition string `json:"condition"`
ConditionDetails ConditionDetails `json:"conditionDetails"`
}

type ConditionDetails struct {
DiversionPath DiversionPath `json:"diversionPath"`
DiversionStopIDs []string `json:"diversionStopIds"`
}

type DiversionPath struct {
Length int `json:"length"`
Levels string `json:"levels"`
Points string `json:"points"`
}

type ActiveWindow struct {
From int64 `json:"from"`
To int64 `json:"to"`
Expand Down
8 changes: 8 additions & 0 deletions internal/models/stops.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ type StopsResponse struct {
List []Stop `json:"list"`
OutOfRange bool `json:"outOfRange"`
}

// StopWithDistance represents a nearby stop together with its distance from the
// centre of the query bounds. It matches the Java StopWithDistanceV2Bean and is
// used by the arrivals-and-departures-for-location endpoint.
type StopWithDistance struct {
StopID string `json:"stopId"`
DistanceFromQuery float64 `json:"distanceFromQuery"`
}
70 changes: 69 additions & 1 deletion internal/restapi/arrival_and_departure_for_stop_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -713,8 +713,24 @@ func (api *RestAPI) getPredictedTimes(

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
// Fallback: infer the vehicle's current stop from its lat/lon position.
// This handles agencies (e.g. Sound Transit Link light rail) that don't
// publish current_stop_sequence in GTFS-RT vehicle positions.
if vehicle == nil || vehicle.Position == nil ||
vehicle.Position.Latitude == nil || vehicle.Position.Longitude == nil {
return nil
}
inferred := api.inferStopSequenceFromPosition(
ctx, targetTripID,
float64(*vehicle.Position.Latitude),
float64(*vehicle.Position.Longitude),
)
if inferred == nil {
return nil
}
currentVehicleStopSequence = inferred
}

activeTripID := GetVehicleActiveTripID(vehicle)
Expand All @@ -728,3 +744,55 @@ func (api *RestAPI) getNumberOfStopsAway(ctx context.Context, targetTripID strin
numberOfStopsAway := targetGlobalSeq - vehicleGlobalSeq - 1
return &numberOfStopsAway
}

// inferStopSequenceFromPosition returns the stop_sequence of the stop the vehicle
// is currently at or has most recently passed, determined by projecting the vehicle's
// lat/lon onto the ordered list of stop positions for the trip.
//
// It fetches stop times (ordered by sequence) and stop coordinates in a single batch,
// then finds the last stop that is "behind" the vehicle along the route direction.
// Returns nil when no stop times exist or coordinates cannot be resolved.
func (api *RestAPI) inferStopSequenceFromPosition(ctx context.Context, tripID string, vehLat, vehLon float64) *int {
stopTimes, err := api.GtfsManager.GtfsDB.Queries.GetStopTimesForTrip(ctx, tripID)
if err != nil || len(stopTimes) == 0 {
return nil
}

stopIDs := make([]string, len(stopTimes))
for i, st := range stopTimes {
stopIDs[i] = st.StopID
}

stops, err := api.GtfsManager.GtfsDB.Queries.GetStopsByIDs(ctx, stopIDs)
if err != nil {
return nil
}

coordMap := make(map[string][2]float64, len(stops))
for _, s := range stops {
coordMap[s.ID] = [2]float64{s.Lat, s.Lon}
}

// Find the stop that is geometrically closest to the vehicle's current position.
// OBA Java uses a similar nearest-stop heuristic when stop-sequence is absent.
bestIdx := -1
bestDist := -1.0
for i, st := range stopTimes {
coords, ok := coordMap[st.StopID]
if !ok {
continue
}
d := utils.Distance(vehLat, vehLon, coords[0], coords[1])
if bestDist < 0 || d < bestDist {
bestDist = d
bestIdx = i
}
}

if bestIdx < 0 {
return nil
}

seq := int(stopTimes[bestIdx].StopSequence)
return &seq
}
Loading
Loading