mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(roadtrip): a stay that outlasts the day takes the road with it
Three things the day boundary was swallowing, all from the same stay. The stop after it stayed on yesterday. It is reached a full day later but at a later reading on the clock, and the only rule watching for a new day was "the clock went backwards" — which that never does. The schedule's own count knows, as long as it is read as a difference: a day whose first arrivals came out negative is lifted whole, and reading the lifted offsets raw once moved an early stop to tomorrow while leaving the late one behind. It now crosses over properly, marked as having set off the day before. The morning after started at the exact minute the stay ended, as if the road to the first stop took no time. The carried minute is a departure; the drive from there is added to it. And that drive was invisible. It was on the map and in the day's total, but the rail jumped straight to a stop at 08:11 after a stay that ended at 08:00. It gets the same band any other drive gets, above the stop it arrives at — except where a stop crossed over, which already draws that road under its own block.
This commit is contained in:
@@ -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. */}
|
||||
<ol className="px-3.5 pb-3 pt-3" hidden={collapsed}>
|
||||
{/* 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 ? <DriveBand leg={day.arrivingLeg} /> : null}
|
||||
{runs.map(run => (
|
||||
run.spill ? (
|
||||
<SpillBlock key={`spill-${run.from}`} spill={run.spill}>
|
||||
|
||||
@@ -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<number, { seg: RouteSegment | undefined; line: [number, number][]; drawnAs: number }>();
|
||||
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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[][];
|
||||
|
||||
Reference in New Issue
Block a user