fix(roadtrip): put a corridor hit on the drive from the day before ahead of the first stop

The corridor searches the whole line a card draws, and a card that opens
with a drive from the day before (a night carried over, or the joined road
on connected days) draws that drive ahead of its first stop. The insert
index still came from insertIndexForAlong, which never answers 0, so a
charger found on that stretch was added after the first stop and the day
drove there, turned back to the charger and came the same way again.

insertIndexFor now answers 0 for a hit before the first stop when the card
arrives from an earlier stop, and dropping a hit on the map asks it instead
of computing its own index, so a click and a drop cannot disagree.
This commit is contained in:
Maurice
2026-09-14 22:43:46 +02:00
parent 2ffe4d6054
commit 60cbb41d5f
4 changed files with 85 additions and 4 deletions
@@ -259,4 +259,58 @@ describe('useRoadtripCorridor', () => {
expect(result.current.visible).toBe(hits)
})
describe('a card that opens with the drive from the day before', () => {
/**
* Three stops, so "before the first" and "after the first" are different answers:
* with two, every hit lands between them whatever the rule is. The line starts where
* the previous day ended, 250-odd km before this card's first stop.
*/
const stopAt = (index: number, lat: number, lng: number) => ({
assignmentId: 10 + index, ownerDayId: 2, ownerIndex: index, placeId: 100 + index, name: `S${index}`,
lat, lng, time: null, dwellMinutes: null, legMode: null, incomingLegMode: null, stopType: null,
})
const spine = [
{ lat: 53.5, lng: 9.9 }, { lat: 53.0, lng: 11.0 }, { lat: 52.5, lng: 13.4 }, { lat: 52.0, lng: 14.5 }, { lat: 51.5, lng: 15.0 },
]
const arriving = (over: Partial<RoadtripDay>) => day({
dayId: 2,
dayNumber: 2,
stops: [stopAt(0, 52.5, 13.4), stopAt(1, 52.0, 14.5), stopAt(2, 51.5, 15.0)],
geometry: spine.map(p => [p.lat, p.lng] as [number, number]),
...over,
})
const beforeFirstStop = poi({ osm_id: 'early', name: 'Rasthof', alongKm: 60 })
const pastFirstStop = poi({ osm_id: 'late', name: 'Autohof', alongKm: 300 })
beforeEach(() => {
useCorridorPois.mockReturnValue({ ...searchWith([beforeFirstStop, pastFirstStop]), spine })
})
it('FE-ROADTRIP-CORRIDORSTATE-035: a hit on the drive from the connected day before goes in ahead of the first stop', () => {
// Filed after the first stop, the day drove to that stop, turned back to the hit
// and then came the same way again.
const previousLast = { ...stopAt(4, 53.5, 9.9), ownerDayId: 1 }
const { result } = renderHook(() => useRoadtripCorridor(routes([arriving({ arrivingFrom: previousLast })])))
expect(result.current.insertIndexFor(beforeFirstStop)).toBe(0)
expect(result.current.insertIndexFor(pastFirstStop)).toBe(1)
})
it('FE-ROADTRIP-CORRIDORSTATE-036: the same holds for the drive a night carried over', () => {
const previousLast = { ...stopAt(4, 53.5, 9.9), ownerDayId: 1 }
const spill = { at: 0, count: 1, fromDayNumber: 1, departure: null, leg: undefined, line: [], fromStop: previousLast }
const { result } = renderHook(() => useRoadtripCorridor(routes([arriving({ spills: [spill] })])))
expect(result.current.insertIndexFor(beforeFirstStop)).toBe(0)
})
it('FE-ROADTRIP-CORRIDORSTATE-037: a day nothing arrives on keeps its first stop first', () => {
// Its line starts at that stop, so a hit measured ahead of it is a projection
// wobble, and a new first stop would move where the whole day sets off from.
const { result } = renderHook(() => useRoadtripCorridor(routes([arriving({})])))
expect(result.current.insertIndexFor(beforeFirstStop)).toBe(1)
})
})
})
@@ -52,7 +52,7 @@ export interface RoadtripCorridor {
* Which position in the day's chain a hit belongs at, so adding one lands it in the
* order it will actually be driven past rather than at the end of the day.
*/
insertIndexFor: (poi: CorridorPoi) => number
insertIndexFor: (poi: Pick<CorridorPoi, 'alongKm'>) => number
/** How far along the drive each of the day's stops sits, in the same units as a hit. */
stopsAlongKm: number[]
/**
@@ -227,8 +227,17 @@ export function useRoadtripCorridor(routes: RoadtripRoutes, tripId?: number | st
}, [search.results, nameFilter, socketFilter, minKw, anchorKm, sectionKm])
const insertIndexFor = useCallback(
(poi: CorridorPoi) => insertIndexForAlong(stopsAlongKm, poi.alongKm),
[stopsAlongKm],
(poi: Pick<CorridorPoi, 'alongKm'>) => {
// A card can open with a drive that left from a stop on the day before: the one a
// night carried over, or on connected days the road from where yesterday ended. A
// hit on that stretch is passed before this card's first stop, so it goes in ahead
// of it. `insertIndexForAlong` never answers 0, which is right for every other card,
// whose line starts at its first stop.
const arrivesFromEarlier = day?.spills?.find(sp => sp.at === 0)?.fromStop ?? day?.arrivingFrom
if (arrivesFromEarlier && stopsAlongKm.length > 1 && poi.alongKm < stopsAlongKm[0]!) return 0
return insertIndexForAlong(stopsAlongKm, poi.alongKm)
},
[day, stopsAlongKm],
)
/**
@@ -1231,6 +1231,24 @@ describe('useTripPlanner road trip: dropping a hit where it belongs', () => {
expect(result.current.stopDraft?.poi.osm_id).toBe('node/7')
})
it('FE-TP-ROAD-089: a dropped hit is placed by the same rule as a clicked one', async () => {
// Computed on the side, a drop on the drive from the day before went in after this
// card's first stop while the click on the same hit went in ahead of it.
corridorWithHit()
rt.corridor.day = {
dayId: 6,
dayNumber: 2,
stops: [drawn(1301, 52.52, 13.4, 6, 0), drawn(1302, 52.0, 14.5, 6, 1)],
}
rt.corridor.insertIndexFor.mockReturnValue(0)
const { result } = await renderRoadtrip()
act(() => { result.current.dropPoiOnRoute('node/7', 53.0, 11.5) })
expect(rt.corridor.insertIndexFor).toHaveBeenCalledWith(expect.objectContaining({ alongKm: expect.any(Number) }))
expect(result.current.stopDraft).toMatchObject({ dayId: 6, position: 0 })
})
it('FE-TP-ROAD-054: a drop nowhere near the drive is ignored rather than guessed at', async () => {
corridorWithHit()
const { result } = await renderRoadtrip()
@@ -1728,7 +1728,7 @@ export function useTripPlanner() {
if (!hit || !day) return
const at = projectOntoRoute({ lat, lng }, roadtripCorridor.search.spine)
if (!at || at.offRouteKm > roadtripCorridor.widthKm) return
const insert = roadtripInsertion(day, insertIndexForAlong(roadtripCorridor.stopsAlongKm, at.alongKm))
const insert = roadtripInsertion(day, roadtripCorridor.insertIndexFor(at))
if (!insert) return
setStopDraft({
poi: hit,