mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +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:
@@ -54,6 +54,27 @@ export function hidesOnMiddleDay(
|
||||
return !!r.type && MIDDLE_DAY_HIDDEN_TYPES.has(r.type) && getSpanPhase(r, dayId) === 'middle'
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookings you ride and then leave behind: the vehicle carries you out of the day's
|
||||
* geography and keeps going without you. Their endpoints are therefore not the ends of
|
||||
* a drive — you did not drive to tonight's hotel from the airport you took off from, and
|
||||
* you did not drive to the airport you landed at from this morning's hotel (#2133).
|
||||
*
|
||||
* A hire car, a parked car or a hired bike is the exact opposite and must stay out of
|
||||
* this set: you collect the vehicle and keep driving it, so a multi-day rental's pickup
|
||||
* point IS where a drive to the hotel starts, and its drop-off point IS where a drive
|
||||
* from the hotel ends (both pinned by the car-rental span rules above).
|
||||
*
|
||||
* `taxi`, `transit` and `transport_other` are left out on purpose: they are same-day
|
||||
* hops, so both their endpoints land on the day anyway and the distinction never fires.
|
||||
* Keeping them out means this set cannot change any behaviour they have today.
|
||||
*/
|
||||
const CARRIER_TRANSPORT_TYPES = new Set(['flight', 'train', 'ferry', 'cruise', 'bus'])
|
||||
|
||||
export function isCarrierTransport(r: { type?: string | null }): boolean {
|
||||
return !!r.type && CARRIER_TRANSPORT_TYPES.has(r.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* The route waypoints a transport contributes on a given day, respecting multi-day spans.
|
||||
* A car rental (or any reservation whose span covers several days) is only routed to on its
|
||||
|
||||
@@ -166,6 +166,25 @@ describe('shouldDrawMorningLeg', () => {
|
||||
expect(shouldDrawMorningLeg(bookends, checkInDay, { isPlace: true, time: '06:00' })).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT draw when the day opens on an airport you landed at (#2133)', () => {
|
||||
// You flew in. Even on a night you provably slept in this hotel, nobody drove out
|
||||
// of it to the airport that set you down.
|
||||
const bookends = { morning: into(), morningIsSleptHere: true }
|
||||
expect(shouldDrawMorningLeg(bookends, checkInDay, { isPlace: false, carrierEdge: 'arrival' })).toBe(false)
|
||||
})
|
||||
|
||||
it('still draws when the day opens on an airport you departed from (#2133)', () => {
|
||||
// The legitimate mirror: you woke in the hotel and drove to your outbound airport.
|
||||
const bookends = { morning: into(), morningIsSleptHere: true }
|
||||
expect(shouldDrawMorningLeg(bookends, checkInDay, { isPlace: false, carrierEdge: 'departure' })).toBe(true)
|
||||
})
|
||||
|
||||
it('still draws for a hire-car drop-off point, which carries no carrierEdge (#2133)', () => {
|
||||
// A car you keep driving is not a carrier: the hotel → drop-off depot drive is real.
|
||||
const bookends = { morning: into(), morningIsSleptHere: true }
|
||||
expect(shouldDrawMorningLeg(bookends, checkInDay, { isPlace: false, carrierEdge: null })).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT draw on a check-in day when the first place is before check-in (#1465)', () => {
|
||||
const bookends = { morning: into({ check_in: '15:00' }), morningIsSleptHere: false }
|
||||
// Airport place at 10:00, check-in 15:00 — you have not reached the hotel yet.
|
||||
@@ -218,6 +237,25 @@ describe('shouldDrawEveningLeg', () => {
|
||||
expect(shouldDrawEveningLeg(bookends, checkOutDay, { isPlace: true, time: '23:00' })).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT draw when the day ends at an airport you took off from (#2133)', () => {
|
||||
// The reported bug: an overnight flight leaves only its departure airport on the
|
||||
// day it leaves, and that airport was being joined to tonight's hotel.
|
||||
const bookends = { evening: out(), eveningIsOvernight: true }
|
||||
expect(shouldDrawEveningLeg(bookends, checkOutDay, { isPlace: false, carrierEdge: 'departure' })).toBe(false)
|
||||
})
|
||||
|
||||
it('still draws when the day ends at an airport you landed at (#2133)', () => {
|
||||
// The legitimate case the fix must not eat: you land, then drive to the hotel.
|
||||
const bookends = { evening: out(), eveningIsOvernight: true }
|
||||
expect(shouldDrawEveningLeg(bookends, checkOutDay, { isPlace: false, carrierEdge: 'arrival' })).toBe(true)
|
||||
})
|
||||
|
||||
it('still draws for a hire-car pickup point, which carries no carrierEdge (#2133)', () => {
|
||||
// You collect the car at 18:00 and drive it to tonight's hotel — a real leg.
|
||||
const bookends = { evening: out(), eveningIsOvernight: true }
|
||||
expect(shouldDrawEveningLeg(bookends, checkOutDay, { isPlace: false, carrierEdge: null })).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT draw on a check-out day when the last place is after check-out (#1465)', () => {
|
||||
const bookends = { evening: out({ check_out: '11:00' }), eveningIsOvernight: false }
|
||||
// "home" place at 18:00, check-out 11:00 — you have already left the hotel.
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { Day, Accommodation, RouteAnchors } from '../types'
|
||||
import { parseTimeToMinutes } from './dayMerge'
|
||||
|
||||
/**
|
||||
* Set on an edge waypoint that is the endpoint of a booking which CARRIES you out of
|
||||
* the day's geography (see `isCarrierTransport`): 'departure' = you left from this
|
||||
* point, 'arrival' = you were set down at it. A hire car — a vehicle you keep driving —
|
||||
* leaves this unset, so its pickup/drop-off points keep the hotel legs they have always
|
||||
* drawn. Undefined always means "no opinion", which is what keeps every existing caller
|
||||
* and the activity-free transfer day behaving exactly as before.
|
||||
*/
|
||||
export type CarrierEdge = 'departure' | 'arrival' | null
|
||||
|
||||
export const getDayOrder = (day: Day, days: Day[]): number =>
|
||||
day.day_number ?? days.indexOf(day)
|
||||
|
||||
@@ -67,8 +77,12 @@ export const getAccommodationAnchors = (
|
||||
export const shouldDrawMorningLeg = (
|
||||
bookends: { morning?: Accommodation; morningIsSleptHere?: boolean },
|
||||
day: Day,
|
||||
firstStop?: { isPlace: boolean; time?: string | null },
|
||||
firstStop?: { isPlace: boolean; time?: string | null; carrierEdge?: CarrierEdge },
|
||||
): boolean => {
|
||||
// You landed here. Whatever hotel the day belongs to, nobody drove out of it to the
|
||||
// airport they arrived at — so there is no morning leg, not even on a night you
|
||||
// provably slept in that hotel (#2133).
|
||||
if (firstStop?.carrierEdge === 'arrival') return false
|
||||
if (bookends.morningIsSleptHere) return true
|
||||
const m = bookends.morning
|
||||
if (!m || m.start_day_id !== day.id || !firstStop?.isPlace) return false
|
||||
@@ -91,8 +105,11 @@ export const shouldDrawMorningLeg = (
|
||||
export const shouldDrawEveningLeg = (
|
||||
bookends: { evening?: Accommodation; eveningIsOvernight?: boolean },
|
||||
day: Day,
|
||||
lastStop?: { isPlace: boolean; time?: string | null },
|
||||
lastStop?: { isPlace: boolean; time?: string | null; carrierEdge?: CarrierEdge },
|
||||
): boolean => {
|
||||
// Mirror: you took off from here, so no drive leads from it back to tonight's hotel —
|
||||
// the reported "flight starting airport connected to the accommodation" (#2133).
|
||||
if (lastStop?.carrierEdge === 'departure') return false
|
||||
if (bookends.eveningIsOvernight) return true
|
||||
const e = bookends.evening
|
||||
if (!e || e.end_day_id !== day.id || !lastStop?.isPlace) return false
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Straight-line distance between two coordinates, in kilometres.
|
||||
*
|
||||
* Lifted out of useTransportRoutes so the day-route builder can share the one
|
||||
* implementation instead of adding a fourth copy. The map overlays keep their own
|
||||
* `[lat, lng]`-tuple variants — different call shape, and they only sum arc lengths.
|
||||
*/
|
||||
export function haversineKm(a: { lat: number; lng: number }, b: { lat: number; lng: number }): number {
|
||||
const R = 6371
|
||||
const dLat = (b.lat - a.lat) * Math.PI / 180
|
||||
const dLng = (b.lng - a.lng) * Math.PI / 180
|
||||
const la1 = a.lat * Math.PI / 180
|
||||
const la2 = b.lat * Math.PI / 180
|
||||
const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLng / 2) ** 2
|
||||
return 2 * R * Math.asin(Math.sqrt(h))
|
||||
}
|
||||
|
||||
/**
|
||||
* Beyond this straight-line distance a leg of a day's route is not a drive anyone
|
||||
* makes between two stops — it is a booking endpoint that landed next to a local
|
||||
* stop, and the road router answers such a pair with NoRoute (#2133). The same
|
||||
* 2000 km useTransportRoutes has always used for booking geometry.
|
||||
*
|
||||
* Deliberately only ever applied to a leg that touches a TRANSPORT endpoint. Two
|
||||
* real places 2000 km apart are a long drive someone planned; an airport 2000 km
|
||||
* from the stop before it never is.
|
||||
*/
|
||||
export const MAX_DRIVE_KM = 2000
|
||||
|
||||
/** Whether two points are close enough to plausibly be joined by a road leg. */
|
||||
export function withinDriveRange(a: { lat: number; lng: number }, b: { lat: number; lng: number }): boolean {
|
||||
return haversineKm(a, b) <= MAX_DRIVE_KM
|
||||
}
|
||||
Reference in New Issue
Block a user