diff --git a/client/src/components/Roadtrip/useRoadtripCorridor.test.ts b/client/src/components/Roadtrip/useRoadtripCorridor.test.ts index 85b4fd522..0b1fbc798 100644 --- a/client/src/components/Roadtrip/useRoadtripCorridor.test.ts +++ b/client/src/components/Roadtrip/useRoadtripCorridor.test.ts @@ -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) => 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) + }) + }) }) diff --git a/client/src/components/Roadtrip/useRoadtripCorridor.ts b/client/src/components/Roadtrip/useRoadtripCorridor.ts index 31e6549c9..691756e37 100644 --- a/client/src/components/Roadtrip/useRoadtripCorridor.ts +++ b/client/src/components/Roadtrip/useRoadtripCorridor.ts @@ -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) => 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) => { + // 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], ) /** diff --git a/client/src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx b/client/src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx index ae73c8448..71e6f4eef 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx +++ b/client/src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx @@ -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() diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts index 21e5d98b8..24764ceba 100644 --- a/client/src/pages/tripPlanner/useTripPlanner.ts +++ b/client/src/pages/tripPlanner/useTripPlanner.ts @@ -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,