Skip to content

fix: Valhalla detour baseline + preserve stations on click#17

Merged
GeiserX merged 7 commits intomainfrom
fix/valhalla-baseline-detour
Apr 6, 2026
Merged

fix: Valhalla detour baseline + preserve stations on click#17
GeiserX merged 7 commits intomainfrom
fix/valhalla-baseline-detour

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented Apr 6, 2026

Summary

  • Detour baseline: Replaced linear-interpolation baseline (routeDuration * fraction) with a parallel Valhalla exit → rejoin route call. The old approach assumed uniform speed along the entire route, producing wildly inaccurate detour estimates on mixed highway/town routes (e.g. +39min displayed instead of the real +12min for Erla on Sangüesa→Murcia).
  • Station-leg click: Clicking a station now recalculates the route through it for display only — corridor stations are NOT re-fetched. A new stationLegRoutes state in home-client stores the display-only route, while the base routeState still controls corridor fetching. MapView receives a new displayRoutes prop used exclusively for route-layer rendering.

Test plan

  • Calculate Sangüesa → Murcia route, verify Erla detour shows ~12min (not 39)
  • Click a station in the list — route line updates to go through it, station list stays unchanged
  • Click another station — route updates again, station list still unchanged
  • Remove station-leg waypoint — route returns to original
  • Change origin/destination — full recalculation (route + stations) still works
  • Verify detour values are reasonable across different route lengths

Summary by CodeRabbit

  • New Features

    • Map and search support independent station-leg previews: select/clear stations to show or abort preview routes without replacing the primary route; preview lifecycle is isolated from main route requests.
  • Bug Fixes

    • Detour calculations now compute and validate a baseline route via a separate routing call, run checks concurrently, and return a safe fallback for invalid or failed results.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces baseline-from-request with a second Valhalla call run in parallel for baseline duration; removes routeDuration from the detour API. Adds station-leg preview state and wiring, and a displayRoutes prop to render preview routes independently from primary routes.

Changes

Cohort / File(s) Summary
Route detour calculation
src/app/api/route-detour/route.ts
Removed routeDuration from request schema and handler; compute baselineDuration via a second Valhalla routing call executed in parallel with the detour call (Promise.all); per-station processing wrapped in try/catch; mark detour invalid if either call returns null; compute detour as max(0, detourDuration - baselineDuration); retain and re-check exit/rejoin collapse handling after widening.
Station-leg route state & handlers
src/components/home-client.tsx
Added stationLegRoutes state and stationLegAbortRef; extended handleRoute(..., options?: { isStationLeg?: boolean }) to separate station-leg vs normal flows; adjust abort/clear/selection handlers to manage station-leg preview independently; stop sending route.duration to /api/route-detour.
Map rendering for alternate routes
src/components/map/map-view.tsx
Added `displayRoutes?: Route[]
Search panel waypoint & API changes
src/components/search/search-panel.tsx
onRoute signature gains optional options ({ isStationLeg?: boolean }); added onClearStationLeg and selectedStationId props; track station-leg waypoint bumps via stationLegBumpRef to trigger station-leg-only recalcs and avoid full wpRouteVersion increments when removing station-leg waypoints; filter out station-leg waypoints on station deselect and invoke onClearStationLeg.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the two main objectives: replacing Valhalla detour baseline calculation and preserving station state on click.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/valhalla-baseline-detour

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/api/route-detour/route.ts (1)

125-138: Race condition: concurrent results.push() from multiple workers.

Multiple async workers call results.push(result) concurrently. While JS is single-threaded, the await yields control, and if workers resume in interleaved order, push operations could theoretically interleave. In practice this works in Node.js, but it's fragile. Consider using the station index or collecting per-worker arrays.

Safer pattern
-    const results: DetourResult[] = [];
-    const queue = [...stations];
+    const results: DetourResult[] = new Array(stations.length);
+    let nextIdx = 0;

     async function processStation(
       s: z.infer<typeof stationSchema>,
+      idx: number,
     ): Promise<DetourResult> {
       // ... existing logic ...
     }

     const workers: Promise<void>[] = [];
     for (let i = 0; i < CONCURRENCY; i++) {
       workers.push(
         (async () => {
-          while (queue.length > 0) {
-            const station = queue.shift()!;
-            const result = await processStation(station);
-            results.push(result);
+          while (nextIdx < stations.length) {
+            const idx = nextIdx++;
+            const station = stations[idx];
+            results[idx] = await processStation(station, idx);
           }
         })(),
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/route-detour/route.ts` around lines 125 - 138, The current loop
in which multiple async workers call results.push(result) (inside the
CONCURRENCY worker tasks that await processStation and shift from queue) can
race; change each worker to accumulate into a local array (e.g., localResults)
instead of mutating the shared results, have each worker return that array (so
workers become Promise<MyType[]> rather than Promise<void>), then await
Promise.all(workers) and flatten/concat the array-of-arrays into the shared
results once all workers complete; reference the existing symbols queue,
processStation, results, workers, and CONCURRENCY to locate and update the code
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/route-detour/route.ts`:
- Around line 97-116: The code calls getRouteDuration in Promise.all to compute
detourDuration and baselineDuration but does not catch exceptions (e.g.,
TimeoutError or network errors), so a single failing call can abort the whole
batch; wrap the Promise.all(...) invocation that sets detourDuration and
baselineDuration in a try-catch around the await, and in the catch return the
fallback object { id: s.id, detourMin: -1 } (same as the null-check path);
ensure you still keep the existing null-check for
detourDuration/baselineDuration but handle thrown errors by catching them and
returning the fallback immediately.

---

Nitpick comments:
In `@src/app/api/route-detour/route.ts`:
- Around line 125-138: The current loop in which multiple async workers call
results.push(result) (inside the CONCURRENCY worker tasks that await
processStation and shift from queue) can race; change each worker to accumulate
into a local array (e.g., localResults) instead of mutating the shared results,
have each worker return that array (so workers become Promise<MyType[]> rather
than Promise<void>), then await Promise.all(workers) and flatten/concat the
array-of-arrays into the shared results once all workers complete; reference the
existing symbols queue, processStation, results, workers, and CONCURRENCY to
locate and update the code accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d92868e8-d2e4-4cbf-ac30-e909d325e8f1

📥 Commits

Reviewing files that changed from the base of the PR and between c1dfc0e and 3bfe149.

📒 Files selected for processing (4)
  • src/app/api/route-detour/route.ts
  • src/components/home-client.tsx
  • src/components/map/map-view.tsx
  • src/components/search/search-panel.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
src/components/search/search-panel.tsx (1)

351-357: ⚠️ Potential issue | 🟡 Minor

Preview legs still count toward MAX_WAYPOINTS.

This branch excludes isStationLeg from manualCount, but addWaypoint() and the add-waypoint render guard still use waypoints.length. With 4 manual stops + 1 selected station, the UI hides the add action one stop early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/search/search-panel.tsx` around lines 351 - 357, The cap check
is inconsistent: this branch computes manualCount by excluding isStationLeg but
addWaypoint and the add-waypoint render guard use waypoints.length, causing the
add action to hide early; change the check to use the same total count as the
rest of the code (e.g., replace manualCount with totalCount = waypoints.length
or create a shared helper like getWaypointCount that returns the full waypoint
count) and update both this check and addWaypoint/add-waypoint render guard to
call that same helper so station-preview legs are consistently counted against
MAX_WAYPOINTS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/search/search-panel.tsx`:
- Around line 97-106: The clear logic in search-panel.tsx only calls
onClearStationLeg(), but onClearStationLeg in home-client.tsx merely nulls
stationLegRoutes while an in-flight handleRoute() request can later overwrite
the preview; fix by making onClearStationLeg cancel/ignore pending station-leg
requests (e.g., add an AbortController or a requestId flag used by handleRoute)
and ensure handleRoute checks the abort/requestId before writing
stationLegRoutes; update the clear-sites (both the useEffect for
selectedStationId and the click-X handler) to call the enhanced
onClearStationLeg so any in-flight station-leg preview is aborted or ignored.
- Around line 141-145: When building the waypoint coordinates passed to onRoute
(the wpCoords used by calculateRoute/route callback), filter out preview-only
station legs by excluding WaypointEntry items with isStationLeg truthy before
mapping to .location.coordinates; i.e., change the wpCoords formation in the
anonymous route callback (and the analogous code around lines 196-198) to
.filter(wp => wp.location != null && !wp.isStationLeg).map(wp =>
wp.location!.coordinates) so onRoute receives only real stops for base-route
recalcs.

---

Outside diff comments:
In `@src/components/search/search-panel.tsx`:
- Around line 351-357: The cap check is inconsistent: this branch computes
manualCount by excluding isStationLeg but addWaypoint and the add-waypoint
render guard use waypoints.length, causing the add action to hide early; change
the check to use the same total count as the rest of the code (e.g., replace
manualCount with totalCount = waypoints.length or create a shared helper like
getWaypointCount that returns the full waypoint count) and update both this
check and addWaypoint/add-waypoint render guard to call that same helper so
station-preview legs are consistently counted against MAX_WAYPOINTS.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b0e81788-78f5-43e2-bf80-8cbd5d63c1b1

📥 Commits

Reviewing files that changed from the base of the PR and between 3bfe149 and f259213.

📒 Files selected for processing (2)
  • src/components/home-client.tsx
  • src/components/search/search-panel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/home-client.tsx

Comment on lines +141 to +145
(o: Location, d: Location, wps: WaypointEntry[], options?: { isStationLeg?: boolean }) => {
const wpCoords = wps
.filter((wp) => wp.location != null)
.map((wp) => wp.location!.coordinates);
onRoute(o.coordinates, d.coordinates, wpCoords.length > 0 ? wpCoords : undefined);
onRoute(o.coordinates, d.coordinates, wpCoords.length > 0 ? wpCoords : undefined, options);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Exclude isStationLeg entries from base-route recalcs.

waypoints now mixes real stops and preview-only station legs, but calculateRoute() always forwards every resolved waypoint. After a station click, any later normal recalculation will send that preview leg through onRoute as part of the base route and re-fetch corridors on the wrong path.

Suggested fix
     (o: Location, d: Location, wps: WaypointEntry[], options?: { isStationLeg?: boolean }) => {
       const wpCoords = wps
-        .filter((wp) => wp.location != null)
+        .filter((wp) => wp.location != null && (options?.isStationLeg || !wp.isStationLeg))
         .map((wp) => wp.location!.coordinates);
       onRoute(o.coordinates, d.coordinates, wpCoords.length > 0 ? wpCoords : undefined, options);
     },

Also applies to: 196-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/search/search-panel.tsx` around lines 141 - 145, When building
the waypoint coordinates passed to onRoute (the wpCoords used by
calculateRoute/route callback), filter out preview-only station legs by
excluding WaypointEntry items with isStationLeg truthy before mapping to
.location.coordinates; i.e., change the wpCoords formation in the anonymous
route callback (and the analogous code around lines 196-198) to .filter(wp =>
wp.location != null && !wp.isStationLeg).map(wp => wp.location!.coordinates) so
onRoute receives only real stops for base-route recalcs.

GeiserX added 7 commits April 6, 2026 13:42
…eg click

Two fixes:

1. Detour baseline: replace linear-interpolation baseline (routeDuration *
   fraction) with a parallel Valhalla exit→rejoin route. The old approach
   assumed uniform speed along the entire route, which was wildly wrong on
   mixed highway/town routes (showed +39min instead of +12min for Erla).

2. Station-leg click: clicking a station now recalculates the route through
   it (display only) without re-fetching corridor stations. Base routes in
   routeState still control corridor fetching. MapView gets a new
   displayRoutes prop used only for route-layer rendering.
…oint removal

When selectedStationId becomes null (popup close, click elsewhere),
an effect in search-panel removes the station-leg waypoint and calls
onClearStationLeg, which clears stationLegRoutes in home-client.
The display falls back to base routes without re-fetching corridors.

Removing a station-leg waypoint via the X button now skips the
wpRouteVersion bump, so it only clears the preview route instead
of triggering a full route+corridor recalculation.
Station-leg and normal route requests now use separate abort refs
(stationLegAbortRef vs routeAbortRef). Clearing the station-leg
preview aborts the in-flight request so a late response can't
resurrect stationLegRoutes after the user already deselected.
Normal route requests also abort any pending station-leg preview.
Wrap processStation in try/catch so a single Valhalla timeout returns
detourMin: -1 instead of failing the whole batch with HTTP 500.

Add setIsRouteLoading(false) to handleSelectStation(null) and
handleClearStationLeg so aborting a station-leg preview doesn't leave
the loading spinner stuck.
Guard setIsRouteLoading(false) in handleSelectStation(null) and
handleClearStationLeg behind !routeAbortRef.current so that closing
a station popup during a swap/recalc doesn't hide the spinner.
routeAbortRef.current was never cleared after a normal route finished,
staying truthy forever. The station-leg clear handlers checked it to
decide whether to reset the spinner, but the stale ref made them skip
the reset. Now the finally block nulls the ref when its controller is
still current, giving clear handlers an accurate in-flight signal.
handleSelectRoute cleared stationLegRoutes but did not abort
stationLegAbortRef, so a pending station-leg request could still
resolve and overwrite the newly selected base route.
@GeiserX GeiserX force-pushed the fix/valhalla-baseline-detour branch from 9fae53e to 8bd6d5e Compare April 6, 2026 11:43
@GeiserX GeiserX merged commit eebd801 into main Apr 6, 2026
8 checks passed
@GeiserX GeiserX deleted the fix/valhalla-baseline-detour branch April 6, 2026 11:50
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