diff --git a/client/src/components/Roadtrip/RoadtripSidebar.tsx b/client/src/components/Roadtrip/RoadtripSidebar.tsx
index f7cbb3f47..41a798387 100644
--- a/client/src/components/Roadtrip/RoadtripSidebar.tsx
+++ b/client/src/components/Roadtrip/RoadtripSidebar.tsx
@@ -1609,6 +1609,11 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
{/* Folded to the header, and with it off the map: what is not listed here is not
drawn there either, which is the whole point of putting a day away. */}
+ {/* The drive in from yesterday, above the first stop, because that is where it
+ happens. Without it a morning that starts at 08:11 after a stay that ended at
+ 08:00 looks like eleven minutes went missing. A day whose first stop crossed
+ over instead gets that road under its own block, so this is left out there. */}
+ {day.arrivingLeg ? : null}
{runs.map(run => (
run.spill ? (
diff --git a/shared/src/roadtrip/assemble.ts b/shared/src/roadtrip/assemble.ts
index ac9986639..54f267967 100644
--- a/shared/src/roadtrip/assemble.ts
+++ b/shared/src/roadtrip/assemble.ts
@@ -112,14 +112,17 @@ export function assembleRoadtrip({
const routed = chain.stops.slice(0, -1).map((s, i) => legFor(s, chain.stops[i + 1]!));
const inboundAt = new Map();
+ let arrivingLeg: RouteSegment | undefined;
if (connectDays && !automaticSchedule) {
for (const spill of chain.spills) {
inboundAt.set(spill.at, { seg: spill.leg, line: spill.line, drawnAs: spill.fromDayNumber });
}
const joined = inboundAt.has(0) ? undefined : previousStop && legFor(previousStop, chain.stops[0]!);
- if (joined)
+ if (joined) {
inboundAt.set(0, { seg: joined.seg, line: joined.line, drawnAs: previousDayNumber ?? chain.dayNumber });
+ arrivingLeg = joined.seg;
+ }
}
previousStop = chain.stops[chain.stops.length - 1]! ?? previousStop;
previousDayNumber = chain.stops.length ? chain.dayNumber : previousDayNumber;
@@ -228,6 +231,9 @@ export function assembleRoadtrip({
title: chain.title,
avoidMissed: missedByDay[chain.dayId],
spills: chain.spills,
+ // Only where no stop actually crossed over: a crossing already draws its own band,
+ // with this same road under it, and a second one would be the drive twice.
+ arrivingLeg: chain.spills.length ? undefined : arrivingLeg,
stops,
legs,
legVias,
diff --git a/shared/src/roadtrip/nightSpill.spec.ts b/shared/src/roadtrip/nightSpill.spec.ts
index 000dfb910..585a26c7e 100644
--- a/shared/src/roadtrip/nightSpill.spec.ts
+++ b/shared/src/roadtrip/nightSpill.spec.ts
@@ -293,8 +293,10 @@ describe('spillChains', () => {
const chains = spillChains(plan, [], everyLeg(11));
const dayTwo = chains.find((c) => c.dayNumber === 2)!;
- expect(dayTwo.schedule.entries[0]!.arrival).toBe('08:00');
- expect(dayTwo.schedule.entries[1]!.arrival).toBe('08:11');
+ // 08:00 is when the stay ENDS; getting to the first stop still takes the eleven
+ // minutes the road takes.
+ expect(dayTwo.schedule.entries[0]!.arrival).toBe('08:11');
+ expect(dayTwo.schedule.entries[1]!.arrival).toBe('08:22');
});
it('FE-NIGHTSPILL-014: an ordinary evening hands on nothing', () => {
@@ -321,7 +323,31 @@ describe('spillChains', () => {
// Never moved: the hour is the traveller's.
expect(dayTwo.schedule.entries[0]!.arrival).toBe('07:00');
- expect(dayTwo.schedule.warnings).toContainEqual({ index: 0, code: 'late', minutes: 60 });
+ // 71 minutes short, not 60: the stay ends at eight and the road takes eleven.
+ expect(dayTwo.schedule.warnings).toContainEqual({ index: 0, code: 'late', minutes: 71 });
+ });
+
+ it('FE-NIGHTSPILL-017: the stop after the stay moves to the day it is reached on', () => {
+ // Both stops sit on day one, but the second is reached a full day after the first.
+ // The clock reads later, not earlier, so the backwards-jump rule never sees it; the
+ // schedule's own day count does.
+ const plan = [
+ day(1, 1, [
+ stop({ assignmentId: 1, name: 'Hamburg', time: '07:56' }),
+ stop({ assignmentId: 2, name: 'Mercure', time: '08:00', dwellMinutes: 24 * 60 }),
+ stop({ assignmentId: 3, name: 'Panorama' }),
+ ]),
+ day(2, 2, []),
+ ];
+
+ const chains = spillChains(plan, [], everyLeg(10));
+
+ expect(chains.find((c) => c.dayNumber === 1)!.stops.map((s) => s.name)).toEqual(['Hamburg', 'Mercure']);
+ const dayTwo = chains.find((c) => c.dayNumber === 2)!;
+ expect(dayTwo.stops.map((s) => s.name)).toEqual(['Panorama']);
+ expect(dayTwo.schedule.entries[0]!.arrival).toBe('08:10');
+ // Marked as having set off yesterday, so the card can say where it came from.
+ expect(dayTwo.spills[0]).toMatchObject({ at: 0, count: 1, fromDayNumber: 1 });
});
it('FE-NIGHTSPILL-016: a stay of two whole days walks the clock forward day by day', () => {
@@ -335,7 +361,7 @@ describe('spillChains', () => {
// Day two is still inside the stay, so it cannot begin at all.
expect(chains.find((c) => c.dayNumber === 2)!.schedule.entries[0]!.arrival).toBeNull();
- expect(chains.find((c) => c.dayNumber === 3)!.schedule.entries[0]!.arrival).toBe('08:00');
+ expect(chains.find((c) => c.dayNumber === 3)!.schedule.entries[0]!.arrival).toBe('08:11');
});
});
});
diff --git a/shared/src/roadtrip/nightSpill.ts b/shared/src/roadtrip/nightSpill.ts
index 9c1264395..4fb6d65cb 100644
--- a/shared/src/roadtrip/nightSpill.ts
+++ b/shared/src/roadtrip/nightSpill.ts
@@ -78,10 +78,18 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
* evening hands on nothing, because the night between them is not a wait, it is a
* night.
*/
- let carriedInto: { dayNumber: number; minute: number } | null = null;
+ let carriedInto: { dayNumber: number; minute: number; from: RoadtripStop } | null = null;
for (const d of all) {
- const notBefore = carriedInto?.dayNumber === d.dayNumber ? carriedInto.minute : null;
+ // Plus the drive from where the stay ends to where this day starts. The carried
+ // minute is a DEPARTURE, and arriving at the same instant would mean the road
+ // between them takes no time at all.
+ const arriving = carriedInto?.dayNumber === d.dayNumber && d.stops[0]
+ ? legFor(carriedInto.from, d.stops[0])?.seg.duration
+ : undefined;
+ const notBefore = carriedInto?.dayNumber === d.dayNumber
+ ? carriedInto.minute + Math.round((arriving ?? 0) / 60)
+ : null;
const routed = d.stops.slice(0, -1).map((s, i) => legFor(s, d.stops[i + 1]!));
const legs = routed.map((l) => l?.seg);
const schedule = computeSchedule(
@@ -95,7 +103,7 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
// Where this day's last clock lands, carried to whichever day that turns out to be.
const ends = schedule.endsAt ?? null;
if (ends !== null && ends > 1440) {
- carriedInto = { dayNumber: d.dayNumber + Math.floor(ends / 1440), minute: ends % 1440 };
+ carriedInto = { dayNumber: d.dayNumber + Math.floor(ends / 1440), minute: ends % 1440, from: d.stops[d.stops.length - 1]! };
} else if (carriedInto && carriedInto.dayNumber <= d.dayNumber) {
// Spent: this is the day it was pointing at.
carriedInto = null;
@@ -106,6 +114,7 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
let day = 0;
let previous: number | null = null;
+ const baseOffset = schedule.entries.find((e) => e.arrival !== null)?.dayOffset ?? 0;
d.stops.forEach((stop, i) => {
const entry = schedule.entries[i]!;
const clock = parseClock(entry?.arrival);
@@ -113,7 +122,16 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
if (previous !== null && clock < previous) day += 1;
previous = clock;
}
- const offset = day;
+ // How many days on from where this one's clock STARTS, not the schedule's absolute
+ // count: a day whose first arrivals came out negative is lifted whole, and the
+ // lift shows up in every offset, so reading them raw moved an early stop to
+ // tomorrow and left the late one behind. Differences survive the lift.
+ //
+ // The backwards-clock fallback covers stops with no time at all, and stays as the
+ // floor for the rest. On its own it cannot see a stop reached after a stay that
+ // ran through the night: twenty-four hours on puts you in tomorrow at the same
+ // reading on the clock, never an earlier one.
+ const offset = entry?.arrival != null ? Math.max(entry.dayOffset - baseOffset, day) : day;
const marks = schedule.warnings.filter((w) => w.index === i);
const reachable = offset > 0 && numbers.has(d.dayNumber + offset);
diff --git a/shared/src/roadtrip/planning-types.ts b/shared/src/roadtrip/planning-types.ts
index 92d946aa3..0b9014243 100644
--- a/shared/src/roadtrip/planning-types.ts
+++ b/shared/src/roadtrip/planning-types.ts
@@ -100,6 +100,17 @@ export interface RoadtripDay {
legs: (RouteSegment | undefined)[];
+ /**
+ * The drive from where the day before ended to where this one starts.
+ *
+ * Only when the days are connected, and only when no stop actually crossed over —
+ * a crossing is already drawn as a spill, with this same road under it. Separate
+ * from `legs`, which are the roads BETWEEN this day's own stops: this one arrives
+ * before the first of them, and without it a morning that begins at 08:11 looks
+ * like it began out of nowhere.
+ */
+ arrivingLeg?: RouteSegment;
+
schedule: Schedule;
legVias: RouteVia[][];