From 38bdb1d4b77f0d1d5d2d6455ec8fc8b2a1565e87 Mon Sep 17 00:00:00 2001 From: Ida Rogalska Date: Tue, 11 Aug 2026 19:30:44 +0200 Subject: [PATCH] client: resolve boundary-leg modes from either endpoint in the map route calc --- client/src/components/Map/RouteCalculator.ts | 16 +++++----- client/src/hooks/useRouteCalculation.ts | 33 ++++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/client/src/components/Map/RouteCalculator.ts b/client/src/components/Map/RouteCalculator.ts index 98dc5bb4e..94c76391f 100644 --- a/client/src/components/Map/RouteCalculator.ts +++ b/client/src/components/Map/RouteCalculator.ts @@ -92,14 +92,14 @@ export async function calculateRoute( * hotel and the first/last located waypoint exist; passing nulls leaves `runs` * untouched. The shared first/last waypoint is repeated so the polylines join. */ -export function withHotelBookends( - runs: Waypoint[][], - firstWay: Waypoint | undefined, - lastWay: Waypoint | undefined, - startHotel: Waypoint | null, - endHotel: Waypoint | null, -): Waypoint[][] { - const out: Waypoint[][] = [] +export function withHotelBookends( + runs: T[][], + firstWay: T | undefined, + lastWay: T | undefined, + startHotel: T | null, + endHotel: T | null, +): T[][] { + const out: T[][] = [] if (startHotel && firstWay) out.push([startHotel, firstWay]) out.push(...runs) if (endHotel && lastWay) out.push([lastWay, endHotel]) diff --git a/client/src/hooks/useRouteCalculation.ts b/client/src/hooks/useRouteCalculation.ts index 2c74968b8..b3653355f 100644 --- a/client/src/hooks/useRouteCalculation.ts +++ b/client/src/hooks/useRouteCalculation.ts @@ -4,6 +4,7 @@ import { useSettingsStore } from '../store/settingsStore' import { calculateRouteWithLegs, withHotelBookends, type RouteProfileKey } from '../components/Map/RouteCalculator' import { getTransportRouteEndpoints } from '../utils/dayMerge' import { getDayBookendHotels, shouldDrawMorningLeg, shouldDrawEveningLeg } from '../utils/dayOrder' +import { resolveLegMode } from '../components/Planner/legMode' import type { TripStoreState } from '../store/tripStore' import type { RouteSegment, RouteResult, RouteVia, Accommodation } from '../types' @@ -73,13 +74,15 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu // Build a unified list of places + transports sorted by effective position. type Entry = - | { kind: 'place'; lat: number; lng: number; pos: number; time: string | null; mode: string | null } + | { kind: 'place'; lat: number; lng: number; pos: number; time: string | null; mode: string | null; incoming: string | null } | { kind: 'transport'; from: { lat: number; lng: number } | null; to: { lat: number; lng: number } | null; pos: number } const entries: Entry[] = [ ...da.filter(a => a.place?.lat && a.place?.lng).map(a => ({ kind: 'place' as const, lat: a.place.lat!, lng: a.place.lng!, pos: a.order_index, time: a.place?.place_time ?? null, // Per-segment travel mode (#1281): mode of the leg leaving this place. mode: (a as { leg_transport_mode?: string | null }).leg_transport_mode ?? null, + // Boundary-leg mode (#1281 follow-up): mode of the leg arriving at this place. + incoming: (a as { incoming_leg_transport_mode?: string | null }).incoming_leg_transport_mode ?? null, })), ...dayTransports.map(r => { const { from, to } = getTransportRouteEndpoints(r, dayId) @@ -102,19 +105,20 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu // back-to-back transports (e.g. two flights on one day) would otherwise pair the // first's arrival point with the second's departure point into a phantom // [airport → airport] road route — that is the flight itself, not a drive (#1394). - const runs: { lat: number; lng: number; mode?: string | null }[][] = [] - let currentRun: { lat: number; lng: number; mode?: string | null }[] = [] + type RunPoint = { lat: number; lng: number; isPlace: boolean; leg_transport_mode?: string | null; incoming_leg_transport_mode?: string | null } + const runs: RunPoint[][] = [] + let currentRun: RunPoint[] = [] let runHasPlace = false for (const entry of entries) { if (entry.kind === 'place') { - currentRun.push({ lat: entry.lat, lng: entry.lng, mode: entry.mode }) + currentRun.push({ lat: entry.lat, lng: entry.lng, isPlace: true, leg_transport_mode: entry.mode, incoming_leg_transport_mode: entry.incoming }) runHasPlace = true } else if (entry.from || entry.to) { - if (entry.from) currentRun.push(entry.from) + if (entry.from) currentRun.push({ ...entry.from, isPlace: false }) if (currentRun.length >= 2 && runHasPlace) runs.push(currentRun) currentRun = [] runHasPlace = false - if (entry.to) currentRun.push(entry.to) + if (entry.to) currentRun.push({ ...entry.to, isPlace: false }) } } if (currentRun.length >= 2 && runHasPlace) runs.push(currentRun) @@ -127,13 +131,16 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu const bookends = day && optimizeFromAccommodation !== false ? getDayBookendHotels(day, allDays, accommodations) : null - const flatPts: { lat: number; lng: number }[] = [] + const flatPts: RunPoint[] = [] for (const e of entries) { - if (e.kind === 'place') flatPts.push({ lat: e.lat, lng: e.lng }) - else { if (e.from) flatPts.push(e.from); if (e.to) flatPts.push(e.to) } + if (e.kind === 'place') flatPts.push({ lat: e.lat, lng: e.lng, isPlace: true, leg_transport_mode: e.mode, incoming_leg_transport_mode: e.incoming }) + else { if (e.from) flatPts.push({ ...e.from, isPlace: false }); if (e.to) flatPts.push({ ...e.to, isPlace: false }) } } - const hotelPt = (a?: Accommodation) => - a && a.place_lat != null && a.place_lng != null ? { lat: a.place_lat, lng: a.place_lng } : null + // A hotel bookend point is not a place-assignment, so isPlace: false — resolveLegMode + // falls through to the day default for hotel-adjacent legs unless the place endpoint + // carries its own override. + const hotelPt = (a?: Accommodation): RunPoint | null => + a && a.place_lat != null && a.place_lng != null ? { lat: a.place_lat, lng: a.place_lng, isPlace: false } : null // Only draw a hotel bookend when the leg is a real drive. You start/end the day at a hotel // when you slept there / sleep there tonight; on the hotel's own check-in or check-out day // the leg holds only when the edge stop is a PLACE timed after check-in / before check-out @@ -147,7 +154,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu e ? { isPlace: e.kind === 'place', time: e.kind === 'place' ? e.time : null } : undefined const drawMorning = !!bookends && !!day && shouldDrawMorningLeg(bookends, day, edgeInfo(firstStop)) const drawEvening = !!bookends && !!day && shouldDrawEveningLeg(bookends, day, edgeInfo(lastStop)) - const runsWithHotel: { lat: number; lng: number; mode?: string | null }[][] = withHotelBookends( + const runsWithHotel: RunPoint[][] = withHotelBookends( runs, flatPts[0], flatPts[flatPts.length - 1], @@ -199,7 +206,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu } } for (let i = 0; i < run.length - 1; i++) { - const mode = run[i].mode || dayDefaultMode + const mode = resolveLegMode(run[i], run[i + 1], dayDefaultMode) const straight = (): [number, number][] => [[run[i].lat, run[i].lng], [run[i + 1].lat, run[i + 1].lng]] try { const r = await calculateRouteWithLegs([{ lat: run[i].lat, lng: run[i].lng }, { lat: run[i + 1].lat, lng: run[i + 1].lng }], { signal: controller.signal, profile: mode, tripId, dayId })