Skip to content

Show every bay's departures at transit-centre zoom (#2107) - #2198

Merged
bmander merged 2 commits into
mainfrom
nearby-arrivals-drawer-2107
Aug 9, 2026
Merged

Show every bay's departures at transit-centre zoom (#2107)#2198
bmander merged 2 commits into
mainfrom
nearby-arrivals-drawer-2107

Conversation

@bmander

@bmander bmander commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes the second half of #2107. Item (1) (#2136) put route names on each stop
marker from zoom 17.5. What a label can't say is when. At that same zoom, with
nothing focused, the arrivals drawer now engages at its peek listing every route
leaving every bay in view — so a rider standing in a transit centre reads the
whole place at once instead of opening bay after bay.

Route-first, not stop-grouped: one row per (route, direction, bay), each naming
where to stand. Grouping by stop would only reproduce the bay-by-bay scan the
feature exists to remove. A route+direction boarding from two bays is two rows —
merging them would either name both (the same scan) or pick one, which is a claim
that can be false on the ground.

One request per viewport, not one per stop

arrivals-and-departures-for-location takes the viewport parameters the map
already computes for stops-for-location and answers with every arrival at every
stop in the box, each carrying its own stopId.

It is missing from developer.onebusaway.orgdocs PR onebusaway-docs#166
is open to add it — but it has been in onebusaway-application-modules since 2022
as ArrivalsAndDeparturesForLocationAction,
with docs in that repo's own src/site/markdown/. Worth stating plainly so a
future reader doesn't conclude it was invented.

Measured at 3rd Ave & Pine, Seattle, in a zoom-17.5-sized box: 8 bays, 40 rows,
~29 KB gzipped
. One single-stop request at the same window is 6.8 KB — so this
is about half the bytes of an eight-way fan-out and an eighth of the requests,
i.e. the same cost as the one focused stop the drawer already polls at 60s.

Two behaviours found by probing the live servers that the code has to know:

  • The empty box answers a different shape. With stops it is
    {entry, references}; with none it is the raw bean with no entry at all
    (emptyResponse() skips factory.getResponse). Identical on all four regions
    that serve it. entry is nullable for exactly this reason — otherwise panning
    onto water throws instead of showing an empty list. Both shapes are pinned by
    NearbyArrivalsDecodeTest.
  • maxCount truncates arrivals, not stops. At maxCount=10 the response
    covered one bay, not ten: the nearest bay's departures eat the budget and the
    rest vanish. Using it as a cost lever would silently drop bays the rider is
    looking straight at. It is left at the server default; minutesAfter is the
    size lever, and limitExceeded is surfaced in the drawer.

Support is discovered, not configured

Puget Sound, Tampa Bay, WMATA, Davis (Unitrans) 200
MTA New York (own fork), San Diego (old build), Adelaide Metro 404

That is every region in the live directory, probed with the app's own key. No
directory field records this, so the first query is the probe and an explicit
HTTP 404 is believed — read off the status line, since San Diego replies with
a raw Tomcat HTML page no JSON decode could classify. Nothing else counts: a
timeout, a 5xx, or a non-OK OBA envelope code stays transient, because those
happen to regions that do serve it.

The verdict is in-memory per region id, deliberately not persisted: a deployment
that upgrades is then picked up on the next launch rather than pinned off until a
reinstall, at a cost of one wasted request per launch per unsupported region.

The rest

  • HomeSheetLogic grows a three-way HomeSheetContent in place of
    shouldShowSheet. No new CurrentFocus variant — that type is the map's
    subject, persisted and undoable, and this list is precisely what shows when
    there isn't one. It gates on having rows, so the drawer never opens empty,
    never opens mid-load, and never opens at all where the server can't answer.
  • Back collapses an expanded list to peek; at peek it passes to the system,
    since the list is ambient with nothing behind it to return to.
  • A pan updates the list in place. The previous response is held while the new
    viewport loads — re-emitting Loading would empty the rows, and since the
    sheet gates on rows that would retract and re-reveal the drawer once per pan.
  • settledCamera() is extracted from the byte-identical debounce +
    cameraInteracting gate that StopsMapController and BikeLayerController
    each carried, so all three loaders share one definition of "settled".
  • ETAs are measured against the response's own currentTime, minted to
    ServerTime at the wire boundary (Use the server clock (currentTime) as the ETA baseline everywhere; audit the app for time-domain mixing #1612 / Typed time: make clock-domain/unit mixing impossible by construction #1620).

Row tap shows that route on the map scoped to the row's bay, in one focus
push so one Back returns straight to the list.

Needs a human decision before merge

  1. minutesAfter = 35 is a new product threshold (the starred-stops list's
    value, against the focused-stop default of 65) — ~29 KB vs ~40 KB gzipped, for
    departures further out than anyone standing at the stop is waiting for.
    CLAUDE.md wants thresholds called out for sign-off; this is that.
  2. The long-press menu is partially wired. Star, reminder, tracking and
    report-a-problem each need the stop-scoped ArrivalsViewModel that this
    many-bay list deliberately does not create. Rather than no-op them they focus
    the row's bay, landing the rider on the panel where they are wired. Coherent,
    but not what a rider expects from a menu item — a follow-up issue is the
    honest fix, and service alerts on rows are deferred with it.
  3. 40 rows at 3rd & Pine is a long list and the peek cap
    (PEEK_HEIGHT_FRACTION = 0.30f) is untuned for it — a device-tuning job, the
    way Name the routes on a stop marker at transit-centre zoom (#2107) #2136 tuned the zoom threshold itself.

Also worth noting: three of seven regions, including MTA New York, get nothing.
That was a deliberate call over carrying a second fan-out data path.

Testing

  • 1,895 unit tests, 0 failures, both flavors (obaGoogle + obaMaplibre)
  • compileObaGoogleDebugKotlin + compileObaMaplibreDebugKotlin with
    -PwarningsAsErrors=true — clean
  • spotlessCheck — clean
  • 24 new tests: both wire shapes; grouping/ordering including the two-bay case
    and that row order never depends on ETA; the three-way sheet mode and back
    behaviour; and the query's band gating, requery-on-pan, poll cadence,
    hold-rows-through-a-pan, and the 404-vs-transient split.

Not yet exercised on device.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added nearby arrivals for transit-centre views, including real-time route and bay information.
    • Nearby arrivals appear on the map and in an expandable sheet when zoomed appropriately.
    • Added actions for viewing routes, stops, schedules, trips, favorites, reminders, and service details.
    • Added automatic refresh and graceful handling for unsupported or unavailable nearby-arrivals data.
  • Improvements
    • Improved map loading after camera movement settles.
    • Route rows now remain distinct by stop and display bay or stop labels.
    • Added messaging when nearby results are truncated and users should zoom in.
  • Tests
    • Added coverage for response decoding, grouping, sheet behavior, polling, and error handling.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f6192d0b-610f-432d-8757-fd75c7725ff2

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa972b and a56263f.

📒 Files selected for processing (16)
  • onebusaway-android/src/main/java/org/onebusaway/android/api/data/NearbyArrivalsSupport.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/app/di/RepositoryModule.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/MapDecisions.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/RentalLayerController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/render/StopZoomBand.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/RouteRowGroup.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/MapFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyRowCallbacks.kt
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/test/java/org/onebusaway/android/region/RegionTestFixtures.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModelTest.kt
🚧 Files skipped from review as they are similar to previous changes (10)
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/main/java/org/onebusaway/android/app/di/RepositoryModule.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/MapDecisions.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/RouteRowGroup.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/MapFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyRowCallbacks.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt

📝 Walkthrough

Walkthrough

Adds nearby-arrivals API models, endpoint loading, settled-viewport polling, route grouping, Compose sheet rendering, map actions, and home-screen integration. Tests cover decoding, grouping, sheet behavior, endpoint classification, polling, failures, and region changes.

Changes

Nearby arrivals

Layer / File(s) Summary
API contracts and response loading
onebusaway-android/src/main/java/org/onebusaway/android/api/contract/..., onebusaway-android/src/main/java/org/onebusaway/android/api/data/..., onebusaway-android/src/main/java/org/onebusaway/android/app/di/RepositoryModule.kt, onebusaway-android/src/test/java/org/onebusaway/android/api/...
Adds location-arrivals response models, the Retrofit endpoint, reference resolution, result classification, unsupported-endpoint tracking, Hilt binding, and decoding tests.
Settled viewport polling
onebusaway-android/src/main/java/org/onebusaway/android/map/..., onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/MapFeature.kt, onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt, onebusaway-android/src/test/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModelTest.kt, onebusaway-android/src/test/java/org/onebusaway/android/region/RegionTestFixtures.kt
Centralizes settled-camera updates and connects viewport, zoom-band, region, drawer, polling, retention, and endpoint states.
Nearby route rows and actions
onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/..., onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/..., onebusaway-android/src/main/res/values/strings.xml, onebusaway-android/src/test/java/org/onebusaway/android/ui/arrivals/...
Groups arrivals by stop, route, and direction. It renders bay labels, route rows, actions, truncation notices, and stable row keys.
Home sheet integration
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/..., onebusaway-android/src/test/java/org/onebusaway/android/ui/home/...
Adds nearby content selection, sheet visibility and back handling, route-to-map focus, nearby polling activation, and conditional nearby-sheet rendering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MapFeature
  participant NearbyArrivalsViewModel
  participant NearbyArrivalsDataSource
  participant HomeScreen
  MapFeature->>NearbyArrivalsViewModel: send settled viewport and stop band
  NearbyArrivalsViewModel->>NearbyArrivalsDataSource: request nearby arrivals
  NearbyArrivalsDataSource-->>NearbyArrivalsViewModel: publish loaded or unavailable state
  NearbyArrivalsViewModel-->>HomeScreen: expose nearby UI state
  HomeScreen->>HomeScreen: build and render nearby route rows
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.62% 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 summarizes the main change: showing departures for every bay at transit-centre zoom.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nearby-arrivals-drawer-2107

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.

@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: 4

🧹 Nitpick comments (3)
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt (1)

71-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the transit-centre band predicate with the ViewModel.

NearbyArrivalsViewModel applies the same rule at its gate (band >= StopBand.ROUTES). The query and the sheet decision must agree, or the drawer can gate on a band the query does not serve. Extract one predicate, for example StopBand.showsNearbyArrivals, and call it from both places.

🤖 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
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt`
around lines 71 - 80, Extract the shared transit-centre band check from
homeSheetContent into a StopBand.showsNearbyArrivals predicate, then replace the
direct band >= StopBand.ROUTES checks in both homeSheetContent and
NearbyArrivalsViewModel with that predicate so the sheet and query use identical
gating.
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt (1)

249-253: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The stop-band derivation is duplicated.

MapFeature.kt lines 311-316 build the same flow from mapViewModel.renderState.snapshot and push it into NearbyArrivalsViewModel. This screen builds it again for its own sheet decision. Two derivations of one value can drift if the source field changes.

One option: expose the band from NearbyArrivalsViewModel (it already receives it through onStopBand) and read it here, so MapFeature stays the single producer.

🤖 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
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt`
around lines 249 - 253, Remove the duplicate stop-band flow derived in
HomeScreen around mapViewModel.renderState.snapshot. Expose or reuse the
stop-band state already received by NearbyArrivalsViewModel through onStopBand,
and update the sheet decision to consume that value while keeping MapFeature as
the single producer.
onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt (1)

118-138: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Key the nearby arrivals query on region as well as viewport.

combine(viewport, band, active) does not include regionRepo.region, so switching regions with the same viewport keeps the current state in flatMapLatest. Add regionRepo.region to the combine key; clear lastLoaded and pass the selected region id through poll, using the id already available inside the poll loop.

🤖 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
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt`
around lines 118 - 138, The NearbyArrivalsViewModel state flow currently keys
flatMapLatest only on viewport, band, and active, so region changes with the
same viewport are ignored. Include regionRepo.region in the combine inputs,
clear lastLoaded when the selected region changes or the gate closes, and pass
the selected region ID into poll, reusing the region ID already available within
the poll loop.
🤖 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
`@onebusaway-android/src/main/java/org/onebusaway/android/api/data/NearbyArrivalsSupport.kt`:
- Around line 44-54: Update NearbyArrivalsSupport.isKnownUnsupported and
recordAbsent to key the unsupported cache by the full Region.sidecarTarget
rather than nullable regionId or sidecarId alone. Include both the resolved
endpoint host and region ID in the key, and update callers to pass the
Region-derived sidecarTarget so 404 state remains isolated per deployment.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt`:
- Around line 548-577: Clear the stale pendingFocus latch at the start of
showNearbyRouteOnMap, before pushFocus and the route directive are emitted.
Ensure subsequent onArrivalsLoaded handling cannot consume an unrequested
pending focus and recenter the map.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyRowCallbacks.kt`:
- Around line 49-55: Update the nearby row callback setup to wrap undoViewport
with rememberUpdatedState and have the retained callbacks read that current
state instead of capturing the initial lambda. Add a recomposition test that
replaces undoViewport, then invokes onShowVehiclesOnMap, onEtaClick, and badge
onShowRouteOnMap to verify each uses the updated viewport.

In
`@onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt`:
- Around line 100-105: Update the test `the nearby key is stable while the stop
key is per stop` so the nearby key is captured in separate evaluations and those
two values are compared, rather than comparing the same property access
expression. Preserve the existing stop-key and null assertions.

---

Nitpick comments:
In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt`:
- Around line 249-253: Remove the duplicate stop-band flow derived in HomeScreen
around mapViewModel.renderState.snapshot. Expose or reuse the stop-band state
already received by NearbyArrivalsViewModel through onStopBand, and update the
sheet decision to consume that value while keeping MapFeature as the single
producer.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt`:
- Around line 71-80: Extract the shared transit-centre band check from
homeSheetContent into a StopBand.showsNearbyArrivals predicate, then replace the
direct band >= StopBand.ROUTES checks in both homeSheetContent and
NearbyArrivalsViewModel with that predicate so the sheet and query use identical
gating.

In
`@onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt`:
- Around line 118-138: The NearbyArrivalsViewModel state flow currently keys
flatMapLatest only on viewport, band, and active, so region changes with the
same viewport are ignored. Include regionRepo.region in the combine inputs,
clear lastLoaded when the selected region changes or the gate closes, and pass
the selected region ID into poll, reusing the region ID already available within
the poll loop.
🪄 Autofix

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: bcb91157-bc5f-4f9b-a54b-30075bab0890

📥 Commits

Reviewing files that changed from the base of the PR and between cd38699 and 0aa972b.

📒 Files selected for processing (23)
  • onebusaway-android/src/main/java/org/onebusaway/android/api/contract/ObaApiModels.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/api/contract/ObaWebService.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/api/data/NearbyArrivalsDataSource.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/api/data/NearbyArrivalsSupport.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/app/di/RepositoryModule.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/BikeLayerController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/MapDecisions.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/map/StopsMapController.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/RouteRowGroup.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/arrivals/components/ArrivalRows.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeScreen.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeSheetLogic.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/HomeViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/map/MapFeature.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsSheetHost.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModel.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyRouteRows.kt
  • onebusaway-android/src/main/java/org/onebusaway/android/ui/home/nearby/NearbyRowCallbacks.kt
  • onebusaway-android/src/main/res/values/strings.xml
  • onebusaway-android/src/test/java/org/onebusaway/android/api/NearbyArrivalsDecodeTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/arrivals/NearbyRouteGroupingTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/HomeSheetLogicTest.kt
  • onebusaway-android/src/test/java/org/onebusaway/android/ui/home/nearby/NearbyArrivalsViewModelTest.kt

bmander and others added 2 commits August 8, 2026 23:37
Item (1) of #2107 put route *names* on each stop marker from zoom 17.5. What
a label can't say is *when*. This is item (2): at that same zoom, with nothing
focused, the arrivals drawer engages at its peek listing every route leaving
every bay in view — so a rider standing in a transit centre reads the whole
place at once instead of opening bay after bay.

Route-first, not stop-grouped. One row per (route, direction, bay), each row
naming where to stand. Grouping by stop would only reproduce the bay-by-bay
scan the feature exists to remove. A route+direction boarding from two bays is
two rows: merging them would either name both (the same scan) or pick one,
which is a claim that can be false on the ground.

`arrivals-and-departures-for-location` takes the viewport parameters the map
already computes for `stops-for-location` and answers with every arrival at
every stop in the box, each carrying its own `stopId`. It is missing from
developer.onebusaway.org (docs PR onebusaway-docs#166 is open) but has been in
onebusaway-application-modules since 2022 as
`ArrivalsAndDeparturesForLocationAction`.

Measured at 3rd & Pine, Seattle, in a zoom-17.5-sized box: 8 bays, 40 rows,
~29 KB gzipped. One single-stop request at the same window is 6.8 KB, so this
is about half the bytes of an eight-way fan-out and an eighth of the requests
— the same cost as the one focused stop the drawer already polls.

Two things the endpoint does that the code has to know:

- **The empty box answers a different shape.** With stops it is
  `{entry, references}`; with none it is the raw bean, no `entry` at all
  (`emptyResponse()` skips `factory.getResponse`). Identical on all four
  regions that serve it. `entry` is nullable for exactly this reason —
  otherwise panning onto water throws instead of showing an empty list.
- **`maxCount` truncates arrivals, not stops.** At 10 the response covers one
  bay, not ten; the nearest bay's departures eat the budget and the rest vanish.
  So it is left at the server default and `minutesAfter` is the size lever,
  with `limitExceeded` surfaced in the drawer rather than silently dropping
  bays a rider is looking straight at.

Four of the seven regions in the directory serve it (Puget Sound, Tampa,
WMATA, Davis); MTA New York, San Diego and Adelaide answer 404. No directory
field records this, so the first query is the probe and an explicit HTTP 404 —
read off the status line, since San Diego replies with a Tomcat HTML page — is
believed. Nothing else: a timeout, a 5xx, or a non-OK OBA envelope code stays
transient, because those happen to regions that do serve it. The verdict is
in-memory per region id, so a deployment that upgrades is picked up on the
next launch instead of being pinned off until a reinstall.

- `HomeSheetLogic` grows a three-way `HomeSheetContent` in place of
  `shouldShowSheet`. No new `CurrentFocus` variant: that type is the map's
  *subject*, persisted and undoable, and this list is what shows when there
  isn't one. It gates on having rows, so the drawer never opens empty, never
  opens mid-load, and never opens at all where the server can't answer.
- Back collapses an expanded list to peek, but at peek it passes to the system
  — ambient, with nothing behind it to return to.
- A pan updates the list in place. The previous response is held while the new
  viewport loads; re-emitting Loading would empty the rows, and since the sheet
  gates on rows that would retract and re-reveal the drawer once per pan.
- `settledCamera()` is extracted from the byte-identical debounce +
  `cameraInteracting` gate that `StopsMapController` and `BikeLayerController`
  each carried, so all three loaders share one definition of "settled".
- ETAs are measured against the response's own `currentTime`, minted to
  `ServerTime` at the wire boundary (#1612/#1620).

Row tap shows that route on the map scoped to the row's bay, in one focus push
so one Back returns to the list. The long-press menu's per-stop actions (star,
reminder, tracking, report) need the stop-scoped `ArrivalsViewModel` this
many-bay list doesn't create; rather than no-op them they focus the row's bay,
landing the rider on the panel where they're wired. Worth a follow-up.

Not yet exercised on device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four inline findings and three nitpicks from the PR 2198 review.

**The unsupported verdict is keyed by the OBA base URL.** `NearbyArrivalsSupport`
held it under `Region.id`, our own primary key. The verdict is a statement about
the *server* that answered 404, and the region is only how the app reaches one: a
directory refresh can repoint an existing region's `obaBaseUrl` at a different
deployment under the same id, and a deep-link-added custom region is a host the
directory never named. Either would inherit the other's 404 and lose the drawer
where it in fact works. Keyed on the endpoint, an unknown host is simply
un-probed, so a region switch still needs no reset. The one case the key does not
separate — a user-entered custom API URL applied ahead of the region — is stated
at the site rather than left implicit.

**The query is keyed on the region too.** `combine(viewport, band, active)` left
the standing poll pointed at the old host when the rider switched regions without
moving the camera. The endpoint is now a fourth input, distinct on the endpoint
alone so an unrelated region field can't re-fire it. Rows are held across a *pan*
so the drawer updates in place, but rows from another deployment describe another
city, so a region switch drops them and starts from Loading.

**One band predicate.** `StopBand.showsNearbyArrivals` replaces the `>= ROUTES`
comparison that `HomeSheetLogic` and `NearbyArrivalsViewModel` each carried, so
the sheet cannot decide to show a band the query never asked for. `HomeScreen`
likewise stops deriving the band a second time off the render snapshot and reads
back the one `MapFeature` already pushes into the query — one producer.

**A stale `pendingFocus` latch could recenter an unrequested camera.**
`showNearbyRouteOnMap` now clears it first, the way `clearMapFocus` and
`enterDirections` do: a restore that armed the latch and then had its stop
unfocused before arrivals landed leaves it set with no focus — exactly the state
that puts this list on screen — and the tapped bay's own load would consume it.

**`undoViewport` reads through `rememberUpdatedState`**, like `rows` and
`onShowTrip` beside it. Today's call site passes `{ mapViewModel.viewport }`,
which re-reads on each invocation, so no stale viewport is reachable now; the
guard is what keeps that true of a caller whose lambda closes over a value.

Also fixed a tautological assertion in `HomeSheetLogicTest` that compared
`NearbyRoutes.sheetKey` with itself and so held for any implementation.

Rebased onto main. `BikeLayerController` was deleted by #2168's move to
`RentalLayerController`; the `settledCamera()` extraction applies there instead,
and the two doc comments naming the bike loader now name the rental one.

- 1,998 unit tests, 0 failures, both flavors
- `compileObaGoogleDebugKotlin` + `compileObaMaplibreDebugKotlin` with
  `-PwarningsAsErrors=true`, and `spotlessCheck` — clean
- 3 new tests: the endpoint-scoped verdict, the region-switch requery (and that a
  non-endpoint region field does not requery), and the region-switch row drop

Still not exercised on device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bmander
bmander force-pushed the nearby-arrivals-drawer-2107 branch from 0aa972b to a56263f Compare August 8, 2026 23:49
@bmander

bmander commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (was conflicting) and responded to the review — pushed as a56263f, replies on each thread.

Conflicts. Two, both from #2168's dockless-rentals work landing first:

  • BikeLayerController.kt was deleted out from under this branch and replaced by RentalLayerController.kt. The change here was only swapping its duplicated debounce + cameraInteracting expression for the extracted MapHost.settledCamera(), so that applies to the new file instead. The two doc comments that named "the bike-station loader" as the second sharer now name the rental one.
  • MapFeature.kt was an import-block collision — both sides kept.

Review items. All four actionable, plus the three nitpicks:

404 verdict keyed by region id Now keyed by Region.obaBaseUrl — the server the verdict is about. See the thread: the specific sidecarId collision does not apply to Region.id (custom ids are negative and disjoint), but the endpoint is the right key regardless.
Stale pendingFocus in showNearbyRouteOnMap Fixed, and reachable — unfocusing a restored stop before its arrivals land leaves the latch armed with no focus, which is the state that shows this list.
undoViewport captured in a retained callback rememberUpdatedState, matching rows/onShowTrip beside it. A guard, not a live bug — today's lambda reads through the view model.
Tautological sheetKey assertion Fixed with the literal.
Share the band predicate StopBand.showsNearbyArrivals, read by both the query gate and the sheet decision, with its own test.
Duplicate stop-band derivation HomeScreen reads the band back off NearbyArrivalsViewModel; MapFeature stays the single producer.
Key the query on region The endpoint is a fourth combine input, distinct on the endpoint alone. A region switch also drops the held rows — those describe another city — where a pan still holds them.

Testing: 1,998 unit tests, 0 failures, both flavors; both compile…Kotlin tasks with -PwarningsAsErrors=true and spotlessCheck clean. 3 new tests (endpoint-scoped verdict, region-switch requery + that a non-endpoint region field does not requery, region-switch row drop).

The three items under "Needs a human decision before merge" in the description are unchanged and still open. Still not exercised on device.

@bmander
bmander merged commit cfece96 into main Aug 9, 2026
3 checks passed
@bmander
bmander deleted the nearby-arrivals-drawer-2107 branch August 9, 2026 02:09
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