mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
fix(maps): stop the day route drawing drives to airports you flew from
A flight leaving overnight contributes only its departure airport on the day it leaves, and the evening hotel bookend was attaching to whatever point sat at the day's edge. So the map drew a straight line from the airport you took off from to the hotel you were checking into. The same blind spot on the run builder produced the long rays: a day's order is a bare wall clock, so an inbound long-haul whose departure clock reads late sorts after the day's local stops, and its far departure airport got appended to that run. OSRM answers such a pair with NoRoute and the fallback straightens the whole chunk, taking the genuine local drive with it. Both now ask whether the pair could be driven at all. A flight, train, ferry, cruise or coach carries you out of the day's geography, so its departure point has no drive back to tonight's hotel and its arrival point had no drive from this morning's. A hire car is the opposite and stays out of that set: you keep driving it, so a multi-day rental's pickup and drop-off points keep their hotel legs. Alongside it, any leg touching a booking endpoint has to be within MAX_DRIVE_KM, which catches the mis-sorted edges the role alone cannot see. The sidebar gets both guards so its hotel connectors stay in step with the drawn route; the mobile map and timeline read the same hook.
This commit is contained in:
@@ -2,8 +2,9 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
||||
import { useTripStore } from '../store/tripStore'
|
||||
import { useSettingsStore } from '../store/settingsStore'
|
||||
import { calculateRouteWithLegs, withHotelBookends, type RouteProfileKey } from '../components/Map/RouteCalculator'
|
||||
import { getTransportRouteEndpoints, getTransportForDay, getMergedItems } from '../utils/dayMerge'
|
||||
import { getDayBookendHotels, shouldDrawMorningLeg, shouldDrawEveningLeg } from '../utils/dayOrder'
|
||||
import { getTransportRouteEndpoints, getTransportForDay, getMergedItems, isCarrierTransport } from '../utils/dayMerge'
|
||||
import { getDayBookendHotels, shouldDrawMorningLeg, shouldDrawEveningLeg, type CarrierEdge } from '../utils/dayOrder'
|
||||
import { withinDriveRange } from '../utils/geo'
|
||||
import { resolveLegMode } from '../components/Planner/legMode'
|
||||
import type { TripStoreState } from '../store/tripStore'
|
||||
import type { RouteSegment, RouteResult, RouteVia, Accommodation } from '../types'
|
||||
@@ -76,7 +77,7 @@ 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; incoming: string | null }
|
||||
| { kind: 'transport'; from: { lat: number; lng: number } | null; to: { lat: number; lng: number } | null; pos: number }
|
||||
| { kind: 'transport'; from: { lat: number; lng: number } | null; to: { lat: number; lng: number } | null; pos: number; carrier: boolean }
|
||||
const entries: Entry[] = merged.flatMap((item): Entry[] => {
|
||||
if (item.type === 'place') {
|
||||
const a = item.data
|
||||
@@ -91,7 +92,7 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
||||
}
|
||||
if (item.type !== 'transport') return []
|
||||
const { from, to } = getTransportRouteEndpoints(item.data, dayId)
|
||||
return [{ kind: 'transport', from, to, pos: item.sortKey }]
|
||||
return [{ kind: 'transport', from, to, pos: item.sortKey, carrier: isCarrierTransport(item.data) }]
|
||||
})
|
||||
|
||||
// Group located places into driving runs.
|
||||
@@ -104,23 +105,40 @@ 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).
|
||||
//
|
||||
// A booking endpoint also has to be REACHABLE from the stop before it. The day's
|
||||
// order is a wall clock with no timezone behind it, so a long-haul arrival whose
|
||||
// departure clock reads late sorts after the day's local stops — and its departure
|
||||
// airport, an ocean away, then gets stapled to the last of them. The router answers
|
||||
// that pair with NoRoute and the whole chunk falls back to a straight line, which is
|
||||
// the long ray the map draws (#2133). Distance is only ever consulted for a leg
|
||||
// touching a transport endpoint: two real places far apart are a drive someone
|
||||
// planned, an airport that far from the stop before it never is.
|
||||
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
|
||||
const closeRun = () => {
|
||||
if (currentRun.length >= 2 && runHasPlace) runs.push(currentRun)
|
||||
currentRun = []
|
||||
runHasPlace = false
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.kind === 'place') {
|
||||
const prev = currentRun[currentRun.length - 1]
|
||||
// The open run may be nothing but a far-away arrival endpoint — break rather
|
||||
// than draw the ocean.
|
||||
if (prev && !prev.isPlace && !withinDriveRange(prev, entry)) closeRun()
|
||||
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, isPlace: false })
|
||||
if (currentRun.length >= 2 && runHasPlace) runs.push(currentRun)
|
||||
currentRun = []
|
||||
runHasPlace = false
|
||||
const prev = currentRun[currentRun.length - 1]
|
||||
if (entry.from && (!prev || withinDriveRange(prev, entry.from))) currentRun.push({ ...entry.from, isPlace: false })
|
||||
closeRun()
|
||||
if (entry.to) currentRun.push({ ...entry.to, isPlace: false })
|
||||
}
|
||||
}
|
||||
if (currentRun.length >= 2 && runHasPlace) runs.push(currentRun)
|
||||
closeRun()
|
||||
|
||||
// Bookend the route with the day's accommodation: a hotel → first-stop run and
|
||||
// a last-stop → hotel run, so the drawn line matches the sidebar's hotel legs.
|
||||
@@ -146,19 +164,38 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
||||
// (you dropped bags first, or swung back before checking out). A place before check-in (an
|
||||
// airport you reach first, #1465), a later "home" stop on the checkout day (#1465), or a
|
||||
// transport endpoint on an arrival/departure day (#1321, S7) all draw no bookend.
|
||||
// A carrier endpoint at an edge additionally kills its own leg outright, however
|
||||
// near the hotel it sits: you flew out of that airport, so nothing drove back from
|
||||
// it tonight, and you flew into that one, so nothing drove to it this morning
|
||||
// (#2133). Which of a transport's two points sits at the edge depends on its span —
|
||||
// an overnight flight contributes only its departure on the day it leaves.
|
||||
const contributes = (e: Entry) => e.kind === 'place' || !!e.from || !!e.to
|
||||
const firstStop = entries.find(contributes)
|
||||
const lastStop = [...entries].reverse().find(contributes)
|
||||
const edgeInfo = (e?: Entry) =>
|
||||
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 edgeInfo = (e: Entry | undefined, side: 'first' | 'last') => {
|
||||
if (!e) return undefined
|
||||
if (e.kind === 'place') return { isPlace: true, time: e.time }
|
||||
const role: CarrierEdge = side === 'first'
|
||||
? (e.from ? 'departure' : 'arrival')
|
||||
: (e.to ? 'arrival' : 'departure')
|
||||
return { isPlace: false, time: null, carrierEdge: e.carrier ? role : null }
|
||||
}
|
||||
const firstWay = flatPts[0]
|
||||
const lastWay = flatPts[flatPts.length - 1]
|
||||
const morningHotel = hotelPt(bookends?.morning)
|
||||
const eveningHotel = hotelPt(bookends?.evening)
|
||||
// Same reachability test as the run builder: a hotel is not joined to a point no
|
||||
// one could have driven between.
|
||||
const drawMorning = !!bookends && !!day && shouldDrawMorningLeg(bookends, day, edgeInfo(firstStop, 'first'))
|
||||
&& (!morningHotel || !firstWay || firstWay.isPlace || withinDriveRange(morningHotel, firstWay))
|
||||
const drawEvening = !!bookends && !!day && shouldDrawEveningLeg(bookends, day, edgeInfo(lastStop, 'last'))
|
||||
&& (!eveningHotel || !lastWay || lastWay.isPlace || withinDriveRange(eveningHotel, lastWay))
|
||||
const runsWithHotel: RunPoint[][] = withHotelBookends(
|
||||
runs,
|
||||
flatPts[0],
|
||||
flatPts[flatPts.length - 1],
|
||||
drawMorning ? hotelPt(bookends?.morning) : null,
|
||||
drawEvening ? hotelPt(bookends?.evening) : null,
|
||||
firstWay,
|
||||
lastWay,
|
||||
drawMorning ? morningHotel : null,
|
||||
drawEvening ? eveningHotel : null,
|
||||
)
|
||||
|
||||
// Transfer day with no activities: you check out of one accommodation and into
|
||||
|
||||
Reference in New Issue
Block a user