mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(planner): an untimed hotel bookend only closes a local day-trip loop (#2157)
Without stay times the bookend leg drew unless a carrier or a 2000 km edge stop disproved it, which is exactly the reported car trip: home five hundred kilometres away, routed back to the hotel after check-out. The disproof threshold is a day-trip distance now, and the route optimizer anchors through the same rule on desktop and mobile instead of unconditionally, so drawing and optimizing agree.
This commit is contained in:
@@ -979,7 +979,11 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
||||
// hotel, or — on a transfer day — a run from the hotel you leave to the one you arrive at.
|
||||
const day = days.find(d => d.id === dayId)
|
||||
const anchors = day && useSettingsStore.getState().settings.optimize_from_accommodation !== false
|
||||
? getAccommodationAnchors(day, days, accommodations)
|
||||
? getAccommodationAnchors(
|
||||
day, days, accommodations,
|
||||
unlockedWithCoords.map(a => ({ lat: a.place!.lat!, lng: a.place!.lng! })),
|
||||
(mergedItemsMap[dayId] || []).some(i => i.type === 'transport' && hasCarrierEndpointOnDay(i.data, dayId)),
|
||||
)
|
||||
: {}
|
||||
const optimizedAssignments = unlockedWithCoords.length >= 2
|
||||
? optimizeRoute(unlockedWithCoords.map(a => ({ ...a.place, _assignmentId: a.id })), anchors).map(p => unlockedWithCoords.find(a => a.id === p._assignmentId)).filter(Boolean)
|
||||
|
||||
@@ -31,6 +31,7 @@ export function optimizeDayOrder(
|
||||
dayAssignments: Assignment[],
|
||||
accommodations: Accommodation[],
|
||||
fromAccommodation: boolean,
|
||||
dayHasCarrier?: boolean,
|
||||
): OptimizedDay | null {
|
||||
const locked = new Map<number, Assignment>()
|
||||
const movable: Assignment[] = []
|
||||
@@ -41,7 +42,10 @@ export function optimizeDayOrder(
|
||||
const withCoords = movable.filter(a => a.place?.lat != null && a.place?.lng != null)
|
||||
if (withCoords.length < 2) return null
|
||||
const noCoords = movable.filter(a => a.place?.lat == null || a.place?.lng == null)
|
||||
const anchors = fromAccommodation ? getAccommodationAnchors(day, days, accommodations) : {}
|
||||
const anchors = fromAccommodation
|
||||
? getAccommodationAnchors(day, days, accommodations,
|
||||
withCoords.map(a => ({ lat: a.place!.lat!, lng: a.place!.lng! })), dayHasCarrier)
|
||||
: {}
|
||||
const optimized = optimizeRoute(
|
||||
withCoords.map(a => ({ lat: a.place!.lat!, lng: a.place!.lng!, _assignmentId: a.id })),
|
||||
anchors,
|
||||
|
||||
@@ -302,7 +302,7 @@ export function useMPlanTimeline(planner: TripPlanner) {
|
||||
if (!day) return
|
||||
const prevIds = dayAssignments.map(a => a.id)
|
||||
const result = optimizeDayOrder(
|
||||
day, days, dayAssignments, tripAccommodations, settings.optimize_from_accommodation !== false,
|
||||
day, days, dayAssignments, tripAccommodations, settings.optimize_from_accommodation !== false, dayHasCarrier,
|
||||
)
|
||||
if (!result) { toast.info(t('dayplan.toast.needTwoPlaces')); return }
|
||||
try {
|
||||
@@ -315,7 +315,7 @@ export function useMPlanTimeline(planner: TripPlanner) {
|
||||
} catch (err: unknown) {
|
||||
toast.error(err instanceof Error ? err.message : t('trip.toast.reorderError'))
|
||||
}
|
||||
}, [day, dayAssignments, days, tripAccommodations, settings, tripActions, tripId, pushUndo, updateRouteForDay, toast, t])
|
||||
}, [day, dayAssignments, days, tripAccommodations, settings, dayHasCarrier, tripActions, tripId, pushUndo, updateRouteForDay, toast, t])
|
||||
|
||||
const exportGoogleMaps = useCallback(() => {
|
||||
if (!day) return
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function MDaySheet({ planner, shell }: MTripSheetsProps) {
|
||||
const optimizeDay = () => {
|
||||
if (!day || dayAssignments.length < 3) return
|
||||
const result = optimizeDayOrder(
|
||||
day, planner.days, dayAssignments, planner.tripAccommodations, optimizeFromAccommodation !== false,
|
||||
day, planner.days, dayAssignments, planner.tripAccommodations, optimizeFromAccommodation !== false, dayHasCarrier,
|
||||
)
|
||||
if (!result) return
|
||||
planner.handleReorder(day.id, result.order.map(a => a.id))
|
||||
|
||||
@@ -81,6 +81,39 @@ describe('getAccommodationAnchors', () => {
|
||||
end: { lat: 9, lng: 9 },
|
||||
})
|
||||
})
|
||||
|
||||
// A Reykjavík stay checking in on day 1 and out on day 3, with neither time recorded,
|
||||
// and untimed stops the optimizer is free to reorder.
|
||||
const reykjavik = { lat: 64.14, lng: -21.94 }
|
||||
const stay = () => hotel({ start_day_id: 10, end_day_id: 30, place_lat: reykjavik.lat, place_lng: reykjavik.lng })
|
||||
const home = { lat: 50.11, lng: 8.68 }
|
||||
const blueLagoon = { lat: 63.88, lng: -22.45 }
|
||||
const hallgrimskirkja = { lat: 64.145, lng: -21.93 }
|
||||
|
||||
it('drops the check-in anchor when the day reaches past a day trip from the hotel (#2157)', () => {
|
||||
// The optimizer half of the same bug. Pinning both ends to a hotel you have not
|
||||
// reached yet made one click on "Optimize route" sort Home between the two Icelandic
|
||||
// stops, because that is the shortest loop, and the user had to drag it back by hand.
|
||||
expect(getAccommodationAnchors(days[0], days, [stay()], [home, blueLagoon, hallgrimskirkja]))
|
||||
.toEqual({ end: reykjavik })
|
||||
})
|
||||
|
||||
it('drops the check-out anchor when a carrier takes you out of the day (#2157)', () => {
|
||||
// Mirror on the departure day: you woke up here, so the hotel still starts the
|
||||
// route, but the flight home means nothing comes back to it.
|
||||
expect(getAccommodationAnchors(days[2], days, [stay()], [hallgrimskirkja], true))
|
||||
.toEqual({ start: reykjavik })
|
||||
})
|
||||
|
||||
it('keeps the loop when every stop is a day out of the hotel (#2009 preserved)', () => {
|
||||
expect(getAccommodationAnchors(days[0], days, [stay()], [blueLagoon, hallgrimskirkja]))
|
||||
.toEqual({ start: reykjavik, end: reykjavik })
|
||||
})
|
||||
|
||||
it('has nothing to weigh the anchors against when the stop list is empty', () => {
|
||||
expect(getAccommodationAnchors(days[0], days, [stay()], []))
|
||||
.toEqual({ start: reykjavik, end: reykjavik })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDayBookendHotels', () => {
|
||||
@@ -253,7 +286,7 @@ describe('shouldDrawMorningLeg', () => {
|
||||
expect(shouldDrawMorningLeg(bookends, checkInDay, { isPlace: true, time: '06:00' }, true)).toBe(true)
|
||||
})
|
||||
|
||||
it('the no-time default does not reach an edge place out of drive range (#2157)', () => {
|
||||
it('the no-time default does not reach an edge place out of day-trip range (#2157)', () => {
|
||||
// "Home", an ocean away from the hotel, with no carrier recorded at all: the
|
||||
// closed loop is a guess, and a guess does not get a 2400 km leg.
|
||||
const noCheckIn = { morning: into({ check_in: null }), morningIsSleptHere: false }
|
||||
@@ -261,6 +294,18 @@ describe('shouldDrawMorningLeg', () => {
|
||||
// A nearby untimed place keeps the #2009 loop.
|
||||
expect(shouldDrawMorningLeg(noCheckIn, checkInDay, { isPlace: true, time: null, lat: 48.2, lng: 11.6 })).toBe(true)
|
||||
})
|
||||
|
||||
it('a drivable edge place is still out of range when no day out of the hotel reaches it (#2157)', () => {
|
||||
// The issue's literal repro, driven rather than flown: hotel in Warnemünde, "Home"
|
||||
// in Frankfurt, 508 km apart. The router answers that pair happily, so a
|
||||
// reachability limit (2000 km, all of Europe) never fired and the arrival day kept
|
||||
// starting at a hotel nobody had reached yet. What rules it out is that no day out
|
||||
// of that hotel goes to Frankfurt and back.
|
||||
const noCheckIn = { morning: into({ check_in: null, place_lat: 54.18, place_lng: 12.08 }), morningIsSleptHere: false }
|
||||
expect(shouldDrawMorningLeg(noCheckIn, checkInDay, { isPlace: true, time: null, lat: 50.11, lng: 8.68 })).toBe(false)
|
||||
// The day trip that hotel does send you on (Rostock zoo, 11 km) keeps its leg.
|
||||
expect(shouldDrawMorningLeg(noCheckIn, checkInDay, { isPlace: true, time: null, lat: 54.08, lng: 12.10 })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldDrawEveningLeg', () => {
|
||||
@@ -348,9 +393,18 @@ describe('shouldDrawEveningLeg', () => {
|
||||
expect(shouldDrawEveningLeg(bookends, checkOutDay, { isPlace: true, time: '23:00' }, true)).toBe(true)
|
||||
})
|
||||
|
||||
it('the no-time default does not reach an edge place out of drive range (#2157)', () => {
|
||||
it('the no-time default does not reach an edge place out of day-trip range (#2157)', () => {
|
||||
const noCheckOut = { evening: out({ check_out: null }), eveningIsOvernight: false }
|
||||
expect(shouldDrawEveningLeg(noCheckOut, checkOutDay, { isPlace: true, time: null, lat: 21.28, lng: -157.83 })).toBe(false)
|
||||
expect(shouldDrawEveningLeg(noCheckOut, checkOutDay, { isPlace: true, time: null, lat: 48.2, lng: 11.6 })).toBe(true)
|
||||
})
|
||||
|
||||
it('a drivable edge place is still out of range when no day out of the hotel reaches it (#2157)', () => {
|
||||
// Mirror of the morning case: the reporter drives home from Warnemünde to Frankfurt
|
||||
// on the last day, and nothing routes those 508 km back to the hotel he checked
|
||||
// out of that morning.
|
||||
const noCheckOut = { evening: out({ check_out: null, place_lat: 54.18, place_lng: 12.08 }), eveningIsOvernight: false }
|
||||
expect(shouldDrawEveningLeg(noCheckOut, checkOutDay, { isPlace: true, time: null, lat: 50.11, lng: 8.68 })).toBe(false)
|
||||
expect(shouldDrawEveningLeg(noCheckOut, checkOutDay, { isPlace: true, time: null, lat: 54.08, lng: 12.10 })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Day, Accommodation, RouteAnchors } from '../types'
|
||||
import { parseTimeToMinutes } from './dayMerge'
|
||||
import { withinDriveRange } from './geo'
|
||||
import { haversineKm, withinDayTripRange } from './geo'
|
||||
|
||||
/**
|
||||
* Set on an edge waypoint that is the endpoint of a booking which CARRIES you out of
|
||||
@@ -15,6 +15,23 @@ export type CarrierEdge = 'departure' | 'arrival' | null
|
||||
export const getDayOrder = (day: Day, days: Day[]): number =>
|
||||
day.day_number ?? days.indexOf(day)
|
||||
|
||||
type EdgeStop = { isPlace: boolean; time?: string | null; carrierEdge?: CarrierEdge; lat?: number; lng?: number }
|
||||
|
||||
/**
|
||||
* The #2009 default on a stay that records no check-in/check-out time: the loop closes,
|
||||
* because a day out of a hotel comes back to it. It is a guess about a day nobody timed,
|
||||
* and two things disprove it (#2157). A carrier on the day means you changed geography by
|
||||
* air/rail/sea, so the hotel is one end of the day and not both. And an edge stop past
|
||||
* day-trip range is where you travelled from or to, not somewhere the hotel sent you out
|
||||
* to: the reported 500 km drive home is as much a disproof as the 2400 km flight, which
|
||||
* is why the radius is a day's driving and not the router's reachability limit.
|
||||
*/
|
||||
const noTimeLoopHolds = (hotel: Accommodation, stop?: EdgeStop, dayHasCarrier?: boolean): boolean => {
|
||||
if (dayHasCarrier) return false
|
||||
if (stop?.lat == null || stop.lng == null || hotel.place_lat == null || hotel.place_lng == null) return true
|
||||
return withinDayTripRange({ lat: hotel.place_lat, lng: hotel.place_lng }, { lat: stop.lat, lng: stop.lng })
|
||||
}
|
||||
|
||||
// The two hotels that bookend a day: the one you woke up in (morning) and the one you sleep in
|
||||
// tonight (evening). On a transfer day these differ; on any other day both are the single hotel.
|
||||
// The morning hotel is keyed off "checked in on an earlier day and still in range" (i.e. you slept
|
||||
@@ -55,16 +72,36 @@ export const getDayBookendHotels = (
|
||||
// Derives route anchors from the accommodation(s) active on a day. A single hotel is the day's home
|
||||
// base, so the route is a loop that starts and ends there. A transfer day — checking out of one hotel
|
||||
// and into another — instead runs from the morning hotel to the evening one.
|
||||
//
|
||||
// `stops` are the located stops the optimizer is about to reorder, and they decide the same way the
|
||||
// drawn bookend legs do: an anchor the map would not draw a leg to is not an anchor the optimizer
|
||||
// gets to pin the day on either, or it sorts a "Home" 500 km out into the middle of the day (#2157).
|
||||
// Since the order is what the optimizer is still deciding, no stop is the day's edge yet, so the one
|
||||
// farthest from the hotel stands in: a day reaching past what the hotel could send you out to and
|
||||
// back from is not a day that hotel bookends. Passing no stops means no evidence, and the anchors
|
||||
// stay where they have always been.
|
||||
export const getAccommodationAnchors = (
|
||||
day: Day,
|
||||
days: Day[],
|
||||
accommodations: Accommodation[],
|
||||
stops?: { lat: number; lng: number }[],
|
||||
dayHasCarrier?: boolean,
|
||||
): RouteAnchors => {
|
||||
const { morning, evening } = getDayBookendHotels(day, days, accommodations)
|
||||
const bookends = getDayBookendHotels(day, days, accommodations)
|
||||
const { morning, evening } = bookends
|
||||
if (!morning || !evening) return {}
|
||||
const at = (h: Accommodation) => ({ lat: h.place_lat as number, lng: h.place_lng as number })
|
||||
// The optimizer only ever moves untimed stops, so the stand-in carries no time.
|
||||
const farthestFrom = (h: Accommodation): EdgeStop | undefined => {
|
||||
if (!stops?.length) return undefined
|
||||
const from = at(h)
|
||||
const far = stops.reduce((a, b) => (haversineKm(from, b) > haversineKm(from, a) ? b : a))
|
||||
return { isPlace: true, time: null, lat: far.lat, lng: far.lng }
|
||||
}
|
||||
const hasStops = !!stops?.length
|
||||
return {
|
||||
start: { lat: morning.place_lat as number, lng: morning.place_lng as number },
|
||||
end: { lat: evening.place_lat as number, lng: evening.place_lng as number },
|
||||
start: !hasStops || shouldDrawMorningLeg(bookends, day, farthestFrom(morning), dayHasCarrier) ? at(morning) : undefined,
|
||||
end: !hasStops || shouldDrawEveningLeg(bookends, day, farthestFrom(evening), dayHasCarrier) ? at(evening) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +112,11 @@ export const getAccommodationAnchors = (
|
||||
// transport arrival (you flew in, weren't at the hotel yet, #1321), or an un-timed place ("Home"
|
||||
// before driving out, #1597) all mean no leg — mirroring the evening rule below. With no
|
||||
// check-in time recorded the loop closes by default (#2009), except on a day a carrier
|
||||
// takes you there or towards an edge place out of drive range (#2157).
|
||||
// takes you there or towards an edge place too far out to be a day trip (#2157).
|
||||
export const shouldDrawMorningLeg = (
|
||||
bookends: { morning?: Accommodation; morningIsSleptHere?: boolean },
|
||||
day: Day,
|
||||
firstStop?: { isPlace: boolean; time?: string | null; carrierEdge?: CarrierEdge; lat?: number; lng?: number },
|
||||
firstStop?: EdgeStop,
|
||||
dayHasCarrier?: boolean,
|
||||
): boolean => {
|
||||
// You landed here. Whatever hotel the day belongs to, nobody drove out of it to the
|
||||
@@ -91,21 +128,13 @@ export const shouldDrawMorningLeg = (
|
||||
if (!m || m.start_day_id !== day.id || !firstStop?.isPlace) return false
|
||||
const checkIn = parseTimeToMinutes(m.check_in)
|
||||
// No check-in time on the stay means there is no bar to clear, and "no
|
||||
// information" was being read as "proof against" — which silently opened the
|
||||
// information" was being read as "proof against", which silently opened the
|
||||
// loop at the start of every arrival day, since the hotel picker leaves the
|
||||
// time blank by default (#2009). With a time set, #1465 still holds: an
|
||||
// earlier stop is a place you reached before the hotel and draws no leg.
|
||||
if (checkIn == null) {
|
||||
// ...but the closed loop is a guess, and two things disprove it (#2157): a
|
||||
// carrier on the day means you arrive at this hotel by air/rail/sea — on its
|
||||
// check-in day the hotel is where the day ENDS, not where it starts — and an
|
||||
// edge place no drive of the hotel could reach ("Home", an ocean away) was
|
||||
// where you came FROM, not somewhere the hotel sent you.
|
||||
if (dayHasCarrier) return false
|
||||
if (firstStop.lat != null && firstStop.lng != null && m.place_lat != null && m.place_lng != null
|
||||
&& !withinDriveRange({ lat: m.place_lat, lng: m.place_lng }, { lat: firstStop.lat, lng: firstStop.lng })) return false
|
||||
return true
|
||||
}
|
||||
// time blank by default (#2009). That loop is still only a guess, so an arrival
|
||||
// in the wide sense (a carrier, or a first stop too far out to be a day trip)
|
||||
// puts the hotel at the day's END instead (#2157). With a time set, #1465 still
|
||||
// holds: an earlier stop is a place you reached before the hotel and draws no leg.
|
||||
if (checkIn == null) return noTimeLoopHolds(m, firstStop, dayHasCarrier)
|
||||
const stop = parseTimeToMinutes(firstStop.time)
|
||||
return stop != null && stop >= checkIn
|
||||
}
|
||||
@@ -118,7 +147,7 @@ export const shouldDrawMorningLeg = (
|
||||
export const shouldDrawEveningLeg = (
|
||||
bookends: { evening?: Accommodation; eveningIsOvernight?: boolean },
|
||||
day: Day,
|
||||
lastStop?: { isPlace: boolean; time?: string | null; carrierEdge?: CarrierEdge; lat?: number; lng?: number },
|
||||
lastStop?: EdgeStop,
|
||||
dayHasCarrier?: boolean,
|
||||
): boolean => {
|
||||
// Mirror: you took off from here, so no drive leads from it back to tonight's hotel —
|
||||
@@ -129,16 +158,10 @@ export const shouldDrawEveningLeg = (
|
||||
if (!e || e.end_day_id !== day.id || !lastStop?.isPlace) return false
|
||||
const checkOut = parseTimeToMinutes(e.check_out)
|
||||
// Mirror of the morning rule: with no check-out time recorded there is nothing
|
||||
// to have missed, so the return leg is drawn and the loop closes (#2009).
|
||||
if (checkOut == null) {
|
||||
// Same two disproofs as the morning (#2157): a carrier on the check-out day
|
||||
// means the hotel is where the day STARTS and nothing drives back to it, and
|
||||
// an edge place out of drive range is where you went instead of returning.
|
||||
if (dayHasCarrier) return false
|
||||
if (lastStop.lat != null && lastStop.lng != null && e.place_lat != null && e.place_lng != null
|
||||
&& !withinDriveRange({ lat: e.place_lat, lng: e.place_lng }, { lat: lastStop.lat, lng: lastStop.lng })) return false
|
||||
return true
|
||||
}
|
||||
// to have missed, so the return leg is drawn and the loop closes (#2009), unless
|
||||
// the day disproves it (#2157). A carrier, or a last stop too far out to be a day
|
||||
// trip, is a departure, and the hotel is then where the day STARTS.
|
||||
if (checkOut == null) return noTimeLoopHolds(e, lastStop, dayHasCarrier)
|
||||
const stop = parseTimeToMinutes(lastStop.time)
|
||||
return stop != null && stop <= checkOut
|
||||
}
|
||||
|
||||
@@ -31,3 +31,20 @@ export const MAX_DRIVE_KM = 2000
|
||||
export function withinDriveRange(a: { lat: number; lng: number }, b: { lat: number; lng: number }): boolean {
|
||||
return haversineKm(a, b) <= MAX_DRIVE_KM
|
||||
}
|
||||
|
||||
/**
|
||||
* How far a day out of a hotel and back plausibly reaches, straight-line.
|
||||
*
|
||||
* Consulted where a stay records no check-in/check-out time and the shape of the day is
|
||||
* therefore a guess (#2157): a stop inside this radius is somewhere the hotel sent you out
|
||||
* to for the day, a stop outside it is where you travelled from or to. It has to be a
|
||||
* road-day radius rather than the booking-geometry MAX_DRIVE_KM above: 2000 km spans the
|
||||
* whole of Europe, so every car journey on the continent passed as a day trip and kept the
|
||||
* phantom hotel leg the issue reported.
|
||||
*/
|
||||
export const MAX_DAY_TRIP_KM = 150
|
||||
|
||||
/** Whether a stop is near enough a hotel to be a day out from it and back. */
|
||||
export function withinDayTripRange(a: { lat: number; lng: number }, b: { lat: number; lng: number }): boolean {
|
||||
return haversineKm(a, b) <= MAX_DAY_TRIP_KM
|
||||
}
|
||||
|
||||
@@ -914,4 +914,56 @@ describe('useRouteCalculation', () => {
|
||||
// The one real journey — you woke at the hotel and travelled home — stays.
|
||||
expect(legs).toContainEqual([`${hotel.lat},${hotel.lng}`, `${home.lat},${home.lng}`]);
|
||||
});
|
||||
|
||||
it('FE-HOOK-ROUTE-033: #2157 the reporter drives home, and 508 km is still no way back to the hotel', async () => {
|
||||
// FE-HOOK-ROUTE-032 with the trip the issue actually describes: a car holiday
|
||||
// inside Germany, no booking of any kind, and a last day that ends at "Home" in
|
||||
// Frankfurt, 508 km from the Warnemünde hotel. Every road router happily answers
|
||||
// that pair, which is why a reachability limit left the reported leg on the map.
|
||||
const hotel = { lat: 54.18, lng: 12.08 }; // Warnemünde
|
||||
const home = buildPlace({ lat: 50.11, lng: 8.68 }); // untimed "Home"
|
||||
const a1 = buildAssignment({ day_id: 2, order_index: 0, place: home });
|
||||
const accommodations = [{ id: 1, start_day_id: 1, end_day_id: 2, place_lat: hotel.lat, place_lng: hotel.lng }];
|
||||
const store = { assignments: { '2': [a1] } } as unknown as TripStoreState;
|
||||
useTripStore.setState({
|
||||
assignments: store.assignments,
|
||||
reservations: [],
|
||||
days: [{ id: 1, day_number: 1 }, { id: 2, day_number: 2 }],
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useRouteCalculation(store, 2, true, 'driving', accommodations as any)
|
||||
);
|
||||
await act(async () => {});
|
||||
|
||||
const legs = (result.current.route ?? []).map(run => run.map(p => `${p[0]},${p[1]}`));
|
||||
expect(legs).not.toContainEqual([`${home.lat},${home.lng}`, `${hotel.lat},${hotel.lng}`]);
|
||||
expect(legs).toContainEqual([`${hotel.lat},${hotel.lng}`, `${home.lat},${home.lng}`]);
|
||||
});
|
||||
|
||||
it('FE-HOOK-ROUTE-034: #2157 the same drive on the arrival day starts at Home, not at the hotel', async () => {
|
||||
// The reporter's second screenshot: day one of the car holiday, "Home" untimed, and
|
||||
// a hotel checking in tonight with no check-in time. The day leaves Frankfurt for
|
||||
// Warnemünde, so nothing drove out of that hotel this morning.
|
||||
const hotel = { lat: 54.18, lng: 12.08 }; // Warnemünde
|
||||
const home = buildPlace({ lat: 50.11, lng: 8.68 }); // untimed "Home"
|
||||
const a1 = buildAssignment({ day_id: 1, order_index: 0, place: home });
|
||||
const accommodations = [{ id: 1, start_day_id: 1, end_day_id: 2, place_lat: hotel.lat, place_lng: hotel.lng }];
|
||||
const store = { assignments: { '1': [a1] } } as unknown as TripStoreState;
|
||||
useTripStore.setState({
|
||||
assignments: store.assignments,
|
||||
reservations: [],
|
||||
days: [{ id: 1, day_number: 1 }, { id: 2, day_number: 2 }],
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useRouteCalculation(store, 1, true, 'driving', accommodations as any)
|
||||
);
|
||||
await act(async () => {});
|
||||
|
||||
const legs = (result.current.route ?? []).map(run => run.map(p => `${p[0]},${p[1]}`));
|
||||
expect(legs).not.toContainEqual([`${hotel.lat},${hotel.lng}`, `${home.lat},${home.lng}`]);
|
||||
// Tonight's hotel is still where the day ends.
|
||||
expect(legs).toContainEqual([`${home.lat},${home.lng}`, `${hotel.lat},${hotel.lng}`]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user