Skip to content

Java-parity for arrivals/departures: deviation + distance + tripStatus#1000

Open
Ahmedhossamdev wants to merge 17 commits into
mainfrom
fix/arrival-departure-java-parity
Open

Java-parity for arrivals/departures: deviation + distance + tripStatus#1000
Ahmedhossamdev wants to merge 17 commits into
mainfrom
fix/arrival-departure-java-parity

Conversation

@Ahmedhossamdev

@Ahmedhossamdev Ahmedhossamdev commented Jun 2, 2026

Copy link
Copy Markdown
Member

Java parity for arrival / departure trip status

Summary

This PR makes Maglev's arrival/departure responses follow Java OneBusAway's
block-location algorithm for:

  • distanceFromStop
  • numberOfStopsAway
  • tripStatus.scheduleDeviation
  • tripStatus.scheduledDistanceAlongTrip
  • scheduled-only tripStatus position/orientation fields

The previous implementation mixed GPS projection, per-trip GTFS-RT delay,
and publisher-specific current_stop_sequence semantics. That produced
zeros for scheduled-only rows, large loop-route distance errors, unit
mismatches when shape_dist_traveled was not metres, and schedule-deviation
differences on multi-trip blocks.

The new path mirrors Java's model: compute a block-wide schedule deviation,
interpolate the scheduled block at currentTime - scheduleDeviation, then
derive stop metrics from block distance and block stop sequence.

When GTFS-RT is unavailable for an arrival, Maglev now still computes the bus's
scheduled position by interpolating the static GTFS block at the request time.
That gives scheduled-only rows meaningful distanceFromStop,
numberOfStopsAway, and tripStatus fields instead of leaving them at zero.

Scheduled interpolation math

With GTFS-RT, the scheduled snapshot is evaluated at:

effectiveTime = currentTime - scheduleDeviation

Without GTFS-RT, effectiveTime is just the request time.

The snapshot builder finds the two scheduled block stops around that time:

from = previous scheduled block stop
to   = next scheduled block stop

Then it computes how far the effective time is through that scheduled segment:

ratio =
  (effectiveSeconds - from.effectiveStopSeconds)
  / (to.effectiveStopSeconds - from.effectiveStopSeconds)

Finally, it linearly interpolates the block distance:

snapshot.distanceAlongBlock =
  from.distanceAlongBlock
  + ratio * (to.distanceAlongBlock - from.distanceAlongBlock)

If the request time is halfway between two scheduled stops, the snapshot
position is halfway along the scheduled block distance between those stops.
Before the first stop it clamps to the first stop distance; after the last stop
it clamps to the last stop distance.

The arrival metrics are then:

distanceFromStop =
  targetStop.distanceAlongBlock - snapshot.distanceAlongBlock

Positive means the target stop is still ahead; negative means the scheduled
position has already passed it.

numberOfStopsAway =
  targetStop.blockSequence - nextStop.blockSequence

Positive means the target stop is that many stops ahead; zero means it is the
next/current stop; negative means it is behind.

Worked example

Assume the scheduled block has these stops:

Stop Time Distance along block Block sequence
A 12:00:00 1,000 m 10
B 12:10:00 2,000 m 11
C 12:20:00 3,500 m 12

If the request time is 12:05:00 and there is no GTFS-RT, then:

effectiveTime = 12:05:00
from = A
to   = B

ratio =
  (12:05 - 12:00)
  / (12:10 - 12:00)
  = 5 minutes / 10 minutes
  = 0.5

snapshot.distanceAlongBlock =
  1,000 + 0.5 * (2,000 - 1,000)
  = 1,500 m

For target stop B:

distanceFromStop =
  2,000 - 1,500
  = 500 m

numberOfStopsAway =
  11 - 11
  = 0

The bus's scheduled position is 500 m before B, and B is the next scheduled
stop.

For target stop C:

distanceFromStop =
  3,500 - 1,500
  = 2,000 m

numberOfStopsAway =
  12 - 11
  = 1

C is 2,000 m ahead of the scheduled position and one stop after the next stop.

If GTFS-RT says the bus is 2 minutes late at 12:05:00, then:

effectiveTime = 12:05:00 - 2 minutes = 12:03:00
ratio = 3 minutes / 10 minutes = 0.3

snapshot.distanceAlongBlock =
  1,000 + 0.3 * (2,000 - 1,000)
  = 1,300 m

The same formulas still apply; the only difference is that the scheduled
snapshot is evaluated 2 minutes earlier because the bus is late.

What Changed

  • Added internal/restapi/scheduled_block_helper.go.

    • Resolves the active block/shift for a trip.
    • Projects stops monotonically through route shapes, including loop routes.
    • Builds a scheduled block snapshot with DistanceAlongBlock,
      NextStopIndex, active trip, and active-trip scheduled distance.
    • Computes Java-equivalent distanceFromStop and numberOfStopsAway.
  • Reworked GetScheduleDeviationForBlock.

    • Iterates all TripUpdates for trips in the block, in block start order.
    • Preserves Java's "trip-level delay wins" behavior.
    • Chooses the closest StopTimeUpdate only when there is no trip-level delay.
    • Applies Java's blockNotActive guard by discarding deviations over 1 hour.
  • Updated both arrivals handlers.

    • Scheduled-only rows now get a tripStatus.
    • Stop distance/count metrics now come from the scheduled block snapshot.
    • minutesBefore now caps at 240 minutes, matching minutesAfter.
    • Unknown/wrong-case agencies return 404 instead of 500.
  • Updated BuildTripStatus.

    • Uses block-wide deviation.
    • Fills scheduledDistanceAlongTrip from the active block trip.
    • Emits raw GTFS-RT vehicleId instead of double-prefixing it.
    • Uses the shared scheduled-position fallback when no RT vehicle is present.
  • Removed superseded helpers.

    • block_distance_helper.go
    • block_sequence_helper.go
    • dead vehicle current-stop/active-trip helpers
  • Refreshed testdata/openapi.yml from upstream.

Java References

The implementation was checked against these Java paths:

  • GtfsRealtimeTripLibrary.applyTripUpdatesToRecord
    • block trip iteration
    • trip-level delay short-circuit
    • per-stop closest-in-time deviation selection
  • GtfsRealtimeSource.blockNotActive
    • ignores records whose schedule deviation is over 1 hour
  • ArrivalsAndDeparturesBeanServiceImpl.applyBlockLocationToBean
    • distanceFromStop = stopTime.distanceAlongBlock - blockLocation.distanceAlongBlock
    • numberOfStopsAway = stopTime.blockSequence - nextStop.blockSequence
  • TripStatusBeanServiceImpl
    • scheduledDistanceAlongTrip = scheduledDistanceAlongBlock - activeBlockTrip.distanceAlongBlock
  • DistanceAlongShapeLibrary.computeBestAssignment
    • monotonic stop-to-shape assignment for loop routes

Validation

  • Added focused tests for:

    • scheduled block snapshot interpolation
    • stop metrics
    • active-trip scheduled position
    • monotonic loop-route projection
    • shape-distance unit scaling
    • block-wide schedule deviation rules
    • Java's 1-hour stale-deviation guard
  • Live comparison against Java showed exact schedule-deviation parity on the
    sampled Unitrans arrivals, with remaining differences limited to stop-boundary
    and independent-feed-snapshot timing effects.

Follow-Ups

  • Cache scheduled block snapshots per request / RT update to reduce DB query
    amplification.
  • Decide whether Maglev should keep smooth per-request interpolation or match
    Java's stepped GTFS-RT-poll cadence.
  • Make GTFS-RT agency-ids filtering case-insensitive so config casing cannot
    silently drop feeds.
  • Work in TO-DOS

Summary by CodeRabbit

  • Bug Fixes

    • More accurate real-time predictions using adjacent-stop and trip-level delay fallback, with safer active-trip handling and consistent time calculations.
    • Improved stop distance and “stops away” by using scheduled block snapshots instead of vehicle-position estimates.
    • Corrected not-found behavior for missing GTFS entities in arrival/departure responses.
  • Improvements

    • Expanded query time windows to a full 24-hour service day.
    • Enhanced schedule deviation and block-based trip tracking for steadier status details.
  • Documentation

    • Updated API schema to allow null schedule/prediction frequency fields.

Align distanceFromStop, numberOfStopsAway, and tripStatus with Java
OneBusAway behavior.

* Add scheduled_block_helper.go implementing Java BlockLocation logic
* Port block snapshot and traversal algorithms from Java
* Refactor GetScheduleDeviation to use block-wide last-wins iteration
* Add 1-hour blockNotActive discard guard from GtfsRealtimeSource
* Resolve response differences between Go and Java arrivals/departures APIs
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Ahmedhossamdev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 965ca486-b5aa-439b-ab81-2f001c4217f7

📥 Commits

Reviewing files that changed from the base of the PR and between 88202ca and 7b67c32.

📒 Files selected for processing (3)
  • internal/restapi/db_errors.go
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/trip_updates_helper.go
📝 Walkthrough

Walkthrough

Adds scheduled-block snapshot computation, block-scoped schedule deviation lookup, and block-based distance/stop calculations in the REST handlers and trip-status path. Also updates nearby-stop selection, GTFS layover timing, OpenAPI nullability, and related tests.

Changes

Block snapshot, deviation, and handler integration

Layer / File(s) Summary
Scheduled block snapshot computation
internal/restapi/scheduled_block_helper.go
Defines block snapshot data structures and implements block trip resolution, trip loading, contiguous shift trimming, ordered stop emission, block-distance interpolation, stop metrics extraction, and scheduled position/orientation interpolation.
Block-level schedule deviation API
internal/restapi/trip_updates_helper.go
Adds block-scoped schedule deviation lookup, trip-update flattening, trip-level delay precedence, schedule matching, active-trip gating, closest-STU selection, reverse-walk fallback, and block trip ordering for service dates. Updates stop-delay extraction to accept current time and skip inactive trips.
Trip status and single-stop prediction path
internal/restapi/trips_helper.go, internal/restapi/arrival_and_departure_for_stop_handler.go, internal/restapi/arrival_and_departure_for_stop_handler_test.go
Updates trip status building to use raw vehicle IDs, block-based schedule deviation, and scheduled-block distance/orientation calculations; updates the single-stop handler to pass current time into prediction lookup and derive stop metrics from the block snapshot; refreshes matching tests for the new prediction inputs and outputs.
Plural arrivals handler and tests
internal/restapi/arrivals_and_departures_for_stop_handler.go, internal/restapi/arrivals_and_departures_for_stop_handler_test.go
Updates plural arrivals request bounds, agency lookup handling, service-ID lookup behavior, nearby-stop radius, and block-snapshot-based stop metrics; rewrites handler tests around absolute stop times, trip-ID matching, and nearby-stop fixture selection.
Scheduled block snapshot and helper tests
internal/restapi/scheduled_block_helper_test.go, internal/restapi/trip_updates_helper_test.go
Adds unit and fixture-backed coverage for block snapshot interpolation, position/orientation mapping, stop metrics, block trip resolution, stop-coordinate loading, snapshot construction, stop projection, active-trip fallback, shift containment, and block-level schedule deviation behavior.
Supporting helper, schema, and cleanup changes
gtfsdb/helpers.go, internal/restapi/vehicles_helper.go, testdata/openapi.yml, internal/restapi/trips_helper_test.go
Adjusts block layover timing bounds, removes vehicle helper functions, refreshes OpenAPI nullability and timestamp, and updates trip-helper tests and benchmarks.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • OneBusAway/maglev#883: Both changes modify groupTripsByDirection behavior in internal/restapi/trips_helper.go.
  • OneBusAway/maglev#982: Both changes update testdata/openapi.yml for arrival/departure schema nullability.

Suggested reviewers

  • fletcherw
  • aaronbrethorst
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change set: Java-parity updates for arrival/departure deviation, distance metrics, and trip status behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/arrival-departure-java-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 336
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 334
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/restapi/trips_helper.go (1)

127-167: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't switch ActiveTripID after deriving stop fields from another trip.

In the no-vehicle flow, Lines 153-166 compute ClosestStop / NextStop from dbTripID, then Lines 223-239 can rewrite status.ActiveTripID, distance, position, and orientation to snapshot.ActiveTripID. For blocks where a different trip is currently active, the response mixes stop IDs from one trip with geometry from another. Resolve the snapshot trip first, or recompute the stop fields after the active trip is known.

Also applies to: 223-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trips_helper.go` around lines 127 - 167, The code currently
computes ClosestStop/NextStop from dbTripID (via
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays and
GetStopDelaysFromTripUpdates) before resolving snapshot.ActiveTripID and later
overwrites status.ActiveTripID (status.ActiveTripID/snapshot.ActiveTripID),
which can mix stop IDs from one trip with geometry from another; fix by
resolving which trip is active first (determine snapshot.ActiveTripID) and then
compute ClosestStop/NextStop for that active trip (or, if you must keep the
existing dbTripID computation, recompute stop fields after you set
status.ActiveTripID), updating calls to
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays or the other
stop-finding helpers accordingly so stop offsets and IDs always come from the
final ActiveTripID used in the status.
🧹 Nitpick comments (3)
internal/restapi/scheduled_block_helper_test.go (1)

436-473: ⚡ Quick win

Add coverage for mixed authoritative/geometric stop projection.

The suite covers all-geometric and unusable-shape paths, but it still misses the important case where one stop uses shape_dist_traveled and the next one falls back to geometry. That mixed path is exactly where the monotonic cursor can regress on loop routes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/scheduled_block_helper_test.go` around lines 436 - 473, Add
a new unit test exercising the mixed authoritative/geometric projection case so
projectStopsInSequence is exercised when one StopTime has ShapeDistTraveled and
the next falls back to geometry; create stopTimes where one element sets
ShapeDistTraveled (e.g., 10.0) and the next has no ShapeDistTraveled but has
resolvable stop coordinates, supply a simple shapePoints/cumDistances and a
matching stopByID map (or use api.fetchStopCoordsForStopTimes pattern used
elsewhere), call projectStopsInSequence and assert the returned distances
length, non-negative values, and monotonic non-decreasing order (distances[i] >=
distances[i-1]) to ensure the cursor does not regress on loop routes.
internal/restapi/trip_updates_helper_test.go (1)

12-17: ⚡ Quick win

The new tests still bypass the primary static-schedule path.

Because these cases use synthetic trip IDs with no stop times, GetScheduleDeviationForBlock only exercises the trip-level-delay branch or the reverse-walk fallback. The changed “closest-in-time against scheduled arrivals/departures” logic never runs here, so the main block-level selection path is still unpinned. Please add at least one case backed by real stop times that goes through the scheduled/STU comparison branch.

Also applies to: 23-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper_test.go` around lines 12 - 17, Tests in
trip_updates_helper_test.go still use synthetic trip IDs (devDate/devNow) with
no stop times so GetScheduleDeviationForBlock only exercises trip-level-delay or
reverse-walk fallback and never hits the "closest-in-time against scheduled
arrivals/departures" block-level selection path; add at least one test case that
uses a real trip/block with actual stop_times (a known static DB trip) and
assert behavior that exercises the scheduled vs STU comparison branch inside
GetScheduleDeviationForBlock by creating a mock update for that real trip ID and
timestamps that force the code to evaluate scheduled arrivals/departures rather
than falling back to reverse-walk. Ensure the new test references the real trip
ID you added and uses devDate/devNow or equivalent to place the update in time
so the scheduled/STU comparison logic is executed.
internal/restapi/trip_updates_helper.go (1)

33-179: 🏗️ Heavy lift

Split the deviation-selection branches into helpers.

This helper now mixes trip-update collection, trip-level overwrite semantics, static-schedule candidate ranking, and reverse-walk fallback in one path. That is already showing up as 73 cognitive complexity, and it will make future Java-parity fixes harder to reason about and test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper.go` around lines 33 - 179,
GetScheduleDeviationForBlock is doing four distinct responsibilities (collecting
tripUpdates, computing trip-level overwrite, ranking scheduled-stop-time
candidates, and reverse-walk fallback), making it too complex; refactor by
extracting helpers: 1) collectTripUpdates(ctx, tripIDs) to build
[]tripUpdateForTrip (used instead of the inline loops), 2)
selectTripLevelDeviation(tripUpdates) to implement the "LAST trip-level delay
wins" logic and threshold check, 3) selectBestSTUDeviation(tripUpdates,
serviceDate, currentTime, loadScheduledForTrip) that contains the
scheduled-stop-time candidate ranking and the per-trip scheduled cache (move
loadScheduled into loadScheduledForTrip helper), and 4)
selectFallbackDelay(tripUpdates) to do the reverse-walk per-stop delay fallback;
then make GetScheduleDeviationForBlock simply call these helpers in order and
return early when a helper finds a value. Use existing symbols tripUpdates,
tripUpdateForTrip, GetScheduleDeviationForBlock, and the Java threshold constant
so callers/logic remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/restapi/arrival_and_departure_for_stop_handler.go`:
- Around line 357-365: The code currently applies the schedule shift only when a
vehicle is present (guarded by "vehicle != nil && vehicle.Trip != nil &&
vehicle.Trip.ID.ID != \"\""), which causes predicted times (Delay) to be applied
but distanceFromStop/numberOfStopsAway to be computed from an unshifted
schedule; remove that vehicle-presence guard so the deviation is applied
whenever api.GetScheduleDeviationForBlock returns hasRT. Specifically, keep the
blockTripIDs retrieval (api.blockTripIDsForServiceDate and
api.blockTripIDsSortedByStartTime) and call
api.GetScheduleDeviationForBlock(ctx, blockTripIDs, serviceDate, currentTime)
unconditionally; if hasRT is true set effectiveTime =
currentTime.Add(-time.Duration(dev) * time.Second).

In `@internal/restapi/arrivals_and_departures_for_stop_handler.go`:
- Around line 373-380: The effectiveTime is only adjusted when a vehicle is
present, so trips with delay-only TripUpdates still use schedule-based
distanceFromStop/numberOfStopsAway; call api.GetScheduleDeviationForBlock and
apply the same shift to effectiveTime even when vehicle == nil but a trip-level
delay is present (i.e., treat delay-only TripUpdates like RT vehicles).
Specifically, after computing blockTripIDs via api.blockTripIDsForServiceDate
and api.blockTripIDsSortedByStartTime, check for a trip-level delay (the
TripUpdate delay for st.TripID) and if present use dev from
api.GetScheduleDeviationForBlock to set effectiveTime =
params.Time.Add(-time.Duration(dev) * time.Second) just as in the
vehicle-present branch so arrivals, distanceFromStop and numberOfStopsAway use
the same shifted time.

In `@internal/restapi/scheduled_block_helper_test.go`:
- Around line 418-433: The test must not hard-code noon; instead derive
currentTime from the loaded snapshot data: call computeScheduledBlockSnapshot to
obtain the block/trip stop times, pick a deterministic stop timestamp from that
snapshot (e.g. use the last stop's ScheduledDeparture or a stop time guaranteed
after the trip start via snap.Stops[len(snap.Stops)-1].ScheduledDeparture), then
recreate the snapshot using that derived currentTime before calling
metricsForStop on firstStop (referencing computeScheduledBlockSnapshot,
snap.Stops, firstStop, and metricsForStop) so the sign assertions are
deterministic.

In `@internal/restapi/scheduled_block_helper.go`:
- Around line 69-71: computeScheduledBlockSnapshot is causing O(rows ×
blockSize) DB round-trips because it queries block/service-date once then calls
GetStopTimesForTrip, GetShapePointsByTripID, and GetStopsByIDs per trip and is
invoked per arrival row; batch these lookups before the handlers by collecting
all needed (blockID, serviceDate) pairs (and all tripIDs/stopIDs) and performing
a single batched query for stop times, shape points, and stops (or add a
memoized cache keyed by blockID+serviceDate) and then have
computeScheduledBlockSnapshot accept or read from that batch/cache so handlers
call it with pre-fetched results, reducing DB calls to O(1) per unique block
rather than per row.
- Around line 333-343: The code currently swallows errors from
GetShapePointsByTripID and appends trips with totalDist==0 which causes
downstream distance corruption; change the logic in the block using
GetShapePointsByTripID/shapeRowsToPoints/preCalculateCumulativeDistances so that
if GetShapePointsByTripID returns an error or shapePoints has fewer than 2
points you do NOT append the trip as a zero-length trip — either skip this trip
entirely or implement a dedicated stop-only fallback (call path:
projectStopsInSequence) before appending; specifically check and handle the
error from api.GtfsManager.GtfsDB.Queries.GetShapePointsByTripID, only compute
cumDistances/totalDist when len(shapePoints) >= 2 and otherwise branch to skip
or invoke the stop-only projection, and ensure any appending code uses the
validated totalDist/cumulative data.
- Around line 427-433: In the stopTimes loop inside the helper, when the branch
that uses st.ShapeDistTraveled.Valid writes distances[i] and continues, advance
the monotonic scan cursor by updating lastMatchedIndex (e.g., set
lastMatchedIndex = i) so subsequent geometry-based scans start from the
most-recent matched stop; this preserves the monotonicity guarantee for the
function that fills the distances slice.

In `@internal/restapi/trip_updates_helper.go`:
- Around line 78-93: The scheduledByTrip cache and loadScheduled function
currently collapse stop times by StopID, losing duplicate visits; update
loadScheduled (and any lookup logic that later matches STUs) to store multiple
scheduled entries per trip per stop by keying on stop_sequence when available
and otherwise appending occurrences (e.g., map[string][]{arr,dep,stop_sequence}
or similar) instead of a single struct; then change the matching logic that uses
scheduledByTrip to first attempt a match by stop_sequence (if STU has a
stop_sequence) and fall back to checking all stored occurrences for that StopID
to find the correct arrival/departure for loop/lasso trips.
- Around line 135-148: The code computes predicted using Time.Unix() -
serviceDate.Unix(), which uses epoch seconds and breaks across DST; instead
compute predicted as the service-day (wall-clock) seconds offset consistent with
schedArr/schedDep. Replace the two branches that set predicted from
stu.Arrival.Time and stu.Departure.Time so they convert the Time value into
seconds since the service-day start (the same basis used to compute
schedArr/schedDep) before subtracting the service-day start, and then pass that
value to considerCandidate; update the Arrival case (stu.Arrival.Time) and
Departure case (stu.Departure.Time) accordingly (keep other branches that use
Delay unchanged).

---

Outside diff comments:
In `@internal/restapi/trips_helper.go`:
- Around line 127-167: The code currently computes ClosestStop/NextStop from
dbTripID (via findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays and
GetStopDelaysFromTripUpdates) before resolving snapshot.ActiveTripID and later
overwrites status.ActiveTripID (status.ActiveTripID/snapshot.ActiveTripID),
which can mix stop IDs from one trip with geometry from another; fix by
resolving which trip is active first (determine snapshot.ActiveTripID) and then
compute ClosestStop/NextStop for that active trip (or, if you must keep the
existing dbTripID computation, recompute stop fields after you set
status.ActiveTripID), updating calls to
findClosestStopByTimeWithDelays/findNextStopByTimeWithDelays or the other
stop-finding helpers accordingly so stop offsets and IDs always come from the
final ActiveTripID used in the status.

---

Nitpick comments:
In `@internal/restapi/scheduled_block_helper_test.go`:
- Around line 436-473: Add a new unit test exercising the mixed
authoritative/geometric projection case so projectStopsInSequence is exercised
when one StopTime has ShapeDistTraveled and the next falls back to geometry;
create stopTimes where one element sets ShapeDistTraveled (e.g., 10.0) and the
next has no ShapeDistTraveled but has resolvable stop coordinates, supply a
simple shapePoints/cumDistances and a matching stopByID map (or use
api.fetchStopCoordsForStopTimes pattern used elsewhere), call
projectStopsInSequence and assert the returned distances length, non-negative
values, and monotonic non-decreasing order (distances[i] >= distances[i-1]) to
ensure the cursor does not regress on loop routes.

In `@internal/restapi/trip_updates_helper_test.go`:
- Around line 12-17: Tests in trip_updates_helper_test.go still use synthetic
trip IDs (devDate/devNow) with no stop times so GetScheduleDeviationForBlock
only exercises trip-level-delay or reverse-walk fallback and never hits the
"closest-in-time against scheduled arrivals/departures" block-level selection
path; add at least one test case that uses a real trip/block with actual
stop_times (a known static DB trip) and assert behavior that exercises the
scheduled vs STU comparison branch inside GetScheduleDeviationForBlock by
creating a mock update for that real trip ID and timestamps that force the code
to evaluate scheduled arrivals/departures rather than falling back to
reverse-walk. Ensure the new test references the real trip ID you added and uses
devDate/devNow or equivalent to place the update in time so the scheduled/STU
comparison logic is executed.

In `@internal/restapi/trip_updates_helper.go`:
- Around line 33-179: GetScheduleDeviationForBlock is doing four distinct
responsibilities (collecting tripUpdates, computing trip-level overwrite,
ranking scheduled-stop-time candidates, and reverse-walk fallback), making it
too complex; refactor by extracting helpers: 1) collectTripUpdates(ctx, tripIDs)
to build []tripUpdateForTrip (used instead of the inline loops), 2)
selectTripLevelDeviation(tripUpdates) to implement the "LAST trip-level delay
wins" logic and threshold check, 3) selectBestSTUDeviation(tripUpdates,
serviceDate, currentTime, loadScheduledForTrip) that contains the
scheduled-stop-time candidate ranking and the per-trip scheduled cache (move
loadScheduled into loadScheduledForTrip helper), and 4)
selectFallbackDelay(tripUpdates) to do the reverse-walk per-stop delay fallback;
then make GetScheduleDeviationForBlock simply call these helpers in order and
return early when a helper finds a value. Use existing symbols tripUpdates,
tripUpdateForTrip, GetScheduleDeviationForBlock, and the Java threshold constant
so callers/logic remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65ddc6c0-59fd-4329-9988-a1d7a41b0f35

📥 Commits

Reviewing files that changed from the base of the PR and between f5bf005 and 3b189f5.

📒 Files selected for processing (12)
  • internal/restapi/arrival_and_departure_for_stop_handler.go
  • internal/restapi/arrival_and_departure_for_stop_handler_test.go
  • internal/restapi/arrivals_and_departures_for_stop_handler.go
  • internal/restapi/block_distance_helper.go
  • internal/restapi/block_sequence_helper.go
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/scheduled_block_helper_test.go
  • internal/restapi/trip_updates_helper.go
  • internal/restapi/trip_updates_helper_test.go
  • internal/restapi/trips_helper.go
  • internal/restapi/trips_helper_test.go
  • internal/restapi/vehicles_helper.go
💤 Files with no reviewable changes (3)
  • internal/restapi/block_sequence_helper.go
  • internal/restapi/block_distance_helper.go
  • internal/restapi/vehicles_helper.go

Comment thread internal/restapi/arrival_and_departure_for_stop_handler.go
Comment thread internal/restapi/arrivals_and_departures_for_stop_handler.go
Comment thread internal/restapi/scheduled_block_helper_test.go
Comment thread internal/restapi/scheduled_block_helper.go Outdated
Comment thread internal/restapi/scheduled_block_helper.go Outdated
Comment thread internal/restapi/scheduled_block_helper.go
Comment thread internal/restapi/trip_updates_helper.go Outdated
Comment thread internal/restapi/trip_updates_helper.go Outdated
…ling

- Update `projectStopsInSequence` to advance `lastMatchedIndex` after an authoritative `shape_dist_traveled` match, preventing subsequent geometric matches from regressing before the previous confirmed stop.
- Fix `GetScheduleDeviationForBlock` for loop trips by preserving all `stop_time` occurrences for a given `stop_id` instead of collapsing them, and matching STUs using `stop_sequence` to select the correct scheduled arrival.
- Add `TestProjectStopsInSequence_MixedAuthoritativeAndGeometric` to verify correct ordering when authoritative and geometric stop matching are combined.
- Add `TestGetScheduleDeviationForBlock_ClosestInTimeAgainstRealSchedule` to verify schedule deviation calculations against real loop-trip schedules.
- Add a TODO note documenting a potential DST edge case in time-based prediction calculations where UTC epoch timestamps are compared with service-day-relative seconds.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.0 ms
Error rate 0.00%
Total requests 330
Req/sec 10.9

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/restapi/trip_updates_helper.go (1)

119-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict the future-over-past preference to equal-distance candidates.

At Line 119, (!isInPast && bestIsInPast) runs even when the future candidate is farther from currentTime than the current best past candidate. That can change scheduleDeviation away from the true closest event, and the handlers then shift block interpolation time by the wrong amount.

Suggested fix
-		if delta < bestDelta || (!isInPast && bestIsInPast) {
+		if delta < bestDelta || (delta == bestDelta && !isInPast && bestIsInPast) {
 			bestDelta = delta
 			bestDeviation = int(deviation)
 			bestIsInPast = isInPast
 			found = true
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/trip_updates_helper.go` around lines 119 - 123, The current
selection compares future vs past with "(!isInPast && bestIsInPast)"
unconditionally, allowing a farther future candidate to override a closer past
candidate; change the selection logic in the loop that updates
bestDelta/bestDeviation/bestIsInPast/found so the future-over-past tie-breaker
only applies when distances are equal (i.e., only prefer a future candidate if
delta == bestDelta and !isInPast && bestIsInPast); keep the primary comparison
delta < bestDelta unchanged and update the condition accordingly where
bestDelta, delta, isInPast, and bestIsInPast are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/restapi/scheduled_block_helper.go`:
- Around line 436-444: The loop that advances lastMatchedIndex stops too early
when distances[i] exactly equals a cumulative breakpoint: change the check so
equality advances to the segment on the right. In the loop that iterates over
cumulativeDistances (using variables cumulativeDistances, distances and
lastMatchedIndex), replace the current if cumulativeDistances[j+1] >=
distances[i] logic with logic that treats equality as belonging to the
right-hand segment (e.g., use if cumulativeDistances[j+1] > distances[i] {
lastMatchedIndex = j; break } else if cumulativeDistances[j+1] == distances[i] {
lastMatchedIndex = j+1; break }) so the monotonic cursor moves to the correct
segment.

In `@internal/restapi/trip_updates_helper_test.go`:
- Around line 250-258: The mocked StopTimeUpdates in MockAddTripUpdate only set
StopID which breaks tests when mustGetTrip returns a trip with repeated stop
IDs; update the test to set unique StopSequence values on the
gtfs.StopTimeUpdate entries (e.g., set StopSequence for nearStop and farStop) so
the code that resolves updates by stop sequence (GetScheduleDeviationForBlock /
mustGetTrip) can disambiguate duplicate StopIDs, or alternatively detect when
the chosen stop IDs collide and skip the test case; modify the calls to
Api.GtfsManager.MockAddTripUpdate in this fixture to include StopSequence on the
gtfs.StopTimeUpdate structs (and mirror the same fix at the other block around
lines referenced).

---

Outside diff comments:
In `@internal/restapi/trip_updates_helper.go`:
- Around line 119-123: The current selection compares future vs past with
"(!isInPast && bestIsInPast)" unconditionally, allowing a farther future
candidate to override a closer past candidate; change the selection logic in the
loop that updates bestDelta/bestDeviation/bestIsInPast/found so the
future-over-past tie-breaker only applies when distances are equal (i.e., only
prefer a future candidate if delta == bestDelta and !isInPast && bestIsInPast);
keep the primary comparison delta < bestDelta unchanged and update the condition
accordingly where bestDelta, delta, isInPast, and bestIsInPast are used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 761b6b85-5a0b-42ff-9399-d3503871787c

📥 Commits

Reviewing files that changed from the base of the PR and between 3b189f5 and b0096bb.

📒 Files selected for processing (5)
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/scheduled_block_helper_test.go
  • internal/restapi/trip_updates_helper.go
  • internal/restapi/trip_updates_helper_test.go
  • testdata/openapi.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/restapi/scheduled_block_helper_test.go

Comment thread internal/restapi/scheduled_block_helper.go Outdated
Comment thread internal/restapi/trip_updates_helper_test.go
…lexity threshold without changing behavior.

- Refactor `GetScheduleDeviationForBlock` into smaller helper functions for trip update collection, schedule matching, and deviation calculation.
- Refactor `projectStopsInSequence` by extracting shape-distance scaling, cursor advancement, and geometric stop projection helpers.
- Refactor `computeScheduledBlockSnapshot` by moving stop emission logic into a dedicated helper.

All tests continue to pass and functionality remains unchanged.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.7 ms
Error rate 0.00%
Total requests 324
Req/sec 10.7

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.9 ms
Error rate 0.00%
Total requests 346
Req/sec 11.3

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

…nd enhance test coverage

- Updated error handling in arrivalsAndDeparturesForStopHandler to surface 404 for unknown agencies.
- Adjusted nearby stop fetching logic to align with Java implementation, setting radius to 100m.
- Enhanced test cases for arrivals and departures, ensuring trip IDs are correctly matched and delays are accurately reflected.
- Introduced isTripActive function to determine if trip updates are still relevant based on current time.
- Modified GetStopDelaysFromTripUpdates to include current time for better accuracy in delay calculations.
- Improved performance comments and TODOs in scheduled block helper and trip updates helper.
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 335
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 337
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 1.9 ms
Error rate 0.00%
Total requests 334
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 335
Req/sec 11.0

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 332
Req/sec 10.8

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@Ahmedhossamdev Ahmedhossamdev marked this pull request as ready for review June 19, 2026 18:39
@Ahmedhossamdev

Copy link
Copy Markdown
Member Author

Hey @aaronbrethorst,
If you get some time today, could you take a look at this PR?
Thanks!

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.1 ms
Error rate 0.00%
Total requests 332
Req/sec 10.9

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/restapi/scheduled_block_helper_test.go (1)

530-536: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Top comment mismatches actual fixture.

The header comment says "stop_B at sequence 4 is at the same (0, 0) coords" and projects "near point[3]", but the actual fixture (lines 555-571) puts the duplicate coords on stop_C (sequence 3), and the assertion at line 586 correctly compares distances[2] (stop_C) vs distances[0], not distances[3]. The later comment block (lines 556-559, 584-588) is accurate; only this top summary is stale/inconsistent and could mislead anyone debugging this complex loop-route projection logic.

📝 Suggested comment fix
-// We build a figure-eight shape that revisits (0, 0) at the midpoint:
-// stop_A at sequence 1 is at the start (shape segment 0); stop_B at
-// sequence 4 is at the same (0, 0) coords but should project to the
-// LATER segment near point[3]. With the monotonic cursor advancing,
-// distances[3] must be strictly greater than distances[0] — both
-// physical coordinates are identical, only the cursor tells them apart.
+// We build a figure-eight shape that revisits (0, 0) at the midpoint:
+// stop_A at sequence 1 is at the start (shape segment 0); stop_C at
+// sequence 3 is at the same (0, 0) coords but should project to the
+// LATER segment near point[2]. With the monotonic cursor advancing,
+// distances[2] must be strictly greater than distances[0] — both
+// physical coordinates are identical, only the cursor tells them apart.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/restapi/scheduled_block_helper_test.go` around lines 530 - 536, The
top summary comment in the scheduled block helper test is stale and no longer
matches the fixture or assertion. Update the narrative near the figure-eight
setup to refer to the actual duplicated point holder and the correct
sequence/index mapping used by the projection logic in this test, so it aligns
with the `stop_C` fixture and the `distances[2]` vs `distances[0]` assertion.
Keep the later detailed comment block unchanged if it is already accurate; only
revise the mismatched summary text to reflect the current test data and
cursor-based projection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/restapi/scheduled_block_helper_test.go`:
- Around line 530-536: The top summary comment in the scheduled block helper
test is stale and no longer matches the fixture or assertion. Update the
narrative near the figure-eight setup to refer to the actual duplicated point
holder and the correct sequence/index mapping used by the projection logic in
this test, so it aligns with the `stop_C` fixture and the `distances[2]` vs
`distances[0]` assertion. Keep the later detailed comment block unchanged if it
is already accurate; only revise the mismatched summary text to reflect the
current test data and cursor-based projection behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5a806cf-2402-40eb-94dc-1fc9f6bb81be

📥 Commits

Reviewing files that changed from the base of the PR and between d9cf061 and 88202ca.

📒 Files selected for processing (6)
  • internal/restapi/arrival_and_departure_for_stop_handler.go
  • internal/restapi/arrivals_and_departures_for_stop_handler.go
  • internal/restapi/arrivals_and_departures_for_stop_handler_test.go
  • internal/restapi/scheduled_block_helper.go
  • internal/restapi/scheduled_block_helper_test.go
  • internal/restapi/trip_updates_helper.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/restapi/arrivals_and_departures_for_stop_handler.go
  • internal/restapi/trip_updates_helper.go
  • internal/restapi/arrival_and_departure_for_stop_handler.go
  • internal/restapi/scheduled_block_helper.go

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Performance Smoke Test Results

Status: PASSED

Metric Value
p(95) latency 2.2 ms
Error rate 0.00%
Total requests 332
Req/sec 10.9

Smoke test config: 5 VUs x 30s. Thresholds: p(95) < 300ms, error rate < 1%.

Full results uploaded as workflow artifact: k6-smoke-summary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant