mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
fix(roadtrip): the range figure keeps counting until something actually refuels
Reported off a real day: Berlin said 245 km since the last fill-up and Sanssouci said 121, as if the tank had been filled in between. It had not. The budget was being zeroed at every warning, so the first figure was right and every one after it was the length of one leg rather than the distance on the tank. A warning is not a fill-up. Only a fuel or charging stop resets it now, which is what makes putting one in the answer rather than a formality: the same day reads 125, 370, 519 without one, and collapses to a single mark with one. Nothing is flagged on a fuel stop either. Reaching a petrol station on an empty tank is the plan working, and a warning there would sit on the one stop that answers it. Also: the stop number sits level with the middle of its row rather than at the top, the way the corridor list already places its badges. A number pinned to the first line drifts off as soon as a stop carries a stay and a walk under its name.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Fuel, Zap, ParkingSquare, Tent, Utensils, Camera, Search, Plus, RotateCw,
|
||||
AlertTriangle, MapPin, X, type LucideIcon,
|
||||
Search, Plus, RotateCw, AlertTriangle, X, BedDouble, MapPin,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
@@ -90,18 +89,26 @@ function ResultRow({ poi, onAdd }: { poi: CorridorPoi; onAdd?: () => void }): Re
|
||||
</div>
|
||||
{/* Always there, quiet until the row is under the pointer: a button that only
|
||||
exists on hover is one a keyboard user has to find by faith. */}
|
||||
{onAdd ? (
|
||||
<Tooltip label={t('roadtrip.poi.add')}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
aria-label={t('roadtrip.poi.add')}
|
||||
className="grid h-[26px] w-[26px] shrink-0 place-items-center rounded-lg text-content-faint transition-colors hover:bg-accent hover:text-accent-text focus-visible:bg-accent focus-visible:text-accent-text focus-visible:outline-none"
|
||||
>
|
||||
<Plus size={15} strokeWidth={2.2} aria-hidden />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{onAdd ? (() => {
|
||||
// One button, two meanings. Somewhere to sleep ends the day rather than
|
||||
// interrupting the drive, so the icon and the label say that before the dialog
|
||||
// opens instead of after.
|
||||
const night = poi.category === 'hotel'
|
||||
const label = night ? t('roadtrip.stay.nightAction') : t('roadtrip.poi.add')
|
||||
const Icon = night ? BedDouble : Plus
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
aria-label={label}
|
||||
className="grid h-[26px] w-[26px] shrink-0 place-items-center rounded-lg text-content-faint transition-colors hover:bg-accent hover:text-accent-text focus-visible:bg-accent focus-visible:text-accent-text focus-visible:outline-none"
|
||||
>
|
||||
<Icon size={15} strokeWidth={2.2} aria-hidden />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
})() : null}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ function Arrival({ entry }: { entry: ScheduleEntry }): React.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
function Stop({ stop, number, entry, late, driveFindings, selected, continues, onSelect, onMove, canMove, onEditStay }: {
|
||||
function Stop({ stop, number, entry, late, driveFindings, selected, continues, starts, onSelect, onMove, canMove, onEditStay }: {
|
||||
stop: RoadtripStop
|
||||
/** Position within the day — the same count the map badges its markers with. */
|
||||
number: number
|
||||
@@ -587,6 +587,8 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, o
|
||||
selected: boolean
|
||||
/** Whether the chain goes on below, so the marker keeps hold of the line. */
|
||||
continues: boolean
|
||||
/** First row of the day: no line above it, because the chain starts here. */
|
||||
starts?: boolean
|
||||
onSelect?: () => void
|
||||
/** Moves this stop by one place. Absent means the chain is read-only. */
|
||||
onMove?: (delta: number) => void
|
||||
@@ -614,16 +616,20 @@ function Stop({ stop, number, entry, late, driveFindings, selected, continues, o
|
||||
className="group grid w-full rounded-lg text-start focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent"
|
||||
style={RAIL_GRID}
|
||||
>
|
||||
{/* The marker and the line below it share one column, so the chain runs unbroken
|
||||
from stop to stop rather than restarting under whatever the row happens to hold. */}
|
||||
{/* The marker sits level with the middle of the row rather than at its top, the
|
||||
way the corridor list already places its own badges: a number pinned to the
|
||||
first line drifts away from the row as soon as a stop carries a stay and a walk
|
||||
under its name. The line grows above and below it, so the chain still runs
|
||||
unbroken from stop to stop. */}
|
||||
<span className="flex flex-col items-center">
|
||||
{starts ? null : <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden />}
|
||||
<span
|
||||
className={`${DISC} bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
|
||||
className={`${DISC} my-1 bg-surface-tertiary font-geist font-semibold tabular-nums text-content-secondary`}
|
||||
style={{ fontSize: FS.marker }}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
{continues ? <span className="mt-1 w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden /> : null}
|
||||
{continues ? <span className="w-[1.5px] flex-1 rounded-sm bg-edge" aria-hidden /> : null}
|
||||
</span>
|
||||
|
||||
{/* The fill stops at the rail: the number is part of the chain, not part of the row
|
||||
@@ -810,6 +816,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
|
||||
driveFindings={day.driveWarnings.filter(w => w.index === i)}
|
||||
selected={selectedAssignmentId === stop.assignmentId}
|
||||
continues={i < last}
|
||||
starts={i === 0}
|
||||
onSelect={onSelectStop ? () => onSelectStop(stop.placeId, stop.assignmentId) : undefined}
|
||||
onMove={onReorderStop ? delta => onReorderStop(day.dayId, stop.assignmentId, i + delta) : undefined}
|
||||
canMove={{ up: i > 0, down: i < last }}
|
||||
|
||||
@@ -289,14 +289,28 @@ describe('deriveDriveWarnings', () => {
|
||||
{ legMinutes: null, dayMinutes: null, rangeKm: 100 },
|
||||
0,
|
||||
)
|
||||
// 21.5 + 103.4 on arrival at HEM, then 244.7 on arrival at Berlin, because the budget
|
||||
// starts over after each finding.
|
||||
// 21.5 + 103.4 on arrival at HEM, and then it keeps counting, because nothing on this
|
||||
// trip fills up.
|
||||
expect(out.warnings).toEqual([
|
||||
{ index: 2, code: 'range', sinceKm: 125 },
|
||||
{ index: 3, code: 'range', sinceKm: 245 },
|
||||
{ index: 3, code: 'range', sinceKm: 370 },
|
||||
])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-070: arriving at a petrol station is the plan, not a warning', () => {
|
||||
// Same trip, but HEM is marked as fuel. Reaching it with an empty tank is exactly
|
||||
// what it is there for, so nothing is flagged on it; the budget still starts over,
|
||||
// and the next stretch is measured from there.
|
||||
const out = deriveDriveWarnings(
|
||||
[leg(20, 21.5), leg(62, 103.4), leg(155, 244.7)],
|
||||
[false, false, true, false],
|
||||
{ legMinutes: null, dayMinutes: null, rangeKm: 100 },
|
||||
0,
|
||||
)
|
||||
expect(out.warnings).toEqual([{ index: 3, code: 'range', sinceKm: 245 }])
|
||||
// 245 and not 370: HEM filled the tank, so Berlin counts from there.
|
||||
})
|
||||
|
||||
const noLimits = { legMinutes: null, dayMinutes: null, rangeKm: null }
|
||||
const leg = (minutes: number, km: number) => ({ duration: minutes * 60, distance: km * 1000 })
|
||||
|
||||
@@ -329,20 +343,34 @@ describe('deriveDriveWarnings', () => {
|
||||
expect(out.day).toEqual({ code: 'dayDriving', minutes: 400, limitMinutes: 360 })
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-063: the range runs out once and then starts over', () => {
|
||||
// Without the reset, a 1800 km trip on a 600 km range flags every leg after the first
|
||||
// overrun, which is a column of red nobody reads. With it the marks stand one range
|
||||
// apart and each means "fill up around here".
|
||||
it('FE-ROADTRIP-MODEL-063: the figure keeps counting until something actually refuels', () => {
|
||||
// A warning is not a fill-up. Zeroing the budget at one made every figure after the
|
||||
// first wrong: the second stop would claim a fresh tank while the same one is still
|
||||
// in the car.
|
||||
const out = deriveDriveWarnings(
|
||||
[leg(60, 300), leg(60, 400), leg(60, 300), leg(60, 400)],
|
||||
[false, false, false, false, false],
|
||||
{ ...noLimits, rangeKm: 600 },
|
||||
0,
|
||||
)
|
||||
expect(out.warnings.map(w => w.code)).toEqual(['range', 'range'])
|
||||
// 300 + 400 on arrival at stop 2, which is exactly what adding up the two drive bands
|
||||
// above it gives.
|
||||
expect(out.warnings[0]).toEqual({ index: 2, code: 'range', sinceKm: 700 })
|
||||
expect(out.warnings).toEqual([
|
||||
{ index: 2, code: 'range', sinceKm: 700 },
|
||||
{ index: 3, code: 'range', sinceKm: 1000 },
|
||||
{ index: 4, code: 'range', sinceKm: 1400 },
|
||||
])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-071: adding a fuel stop is what stops the run of warnings', () => {
|
||||
// The same drive with a charger at stop 2. Nothing is flagged there, because reaching
|
||||
// it on an empty tank is what it is for; everything after counts from zero again, so
|
||||
// the three-warning run above collapses to one.
|
||||
const out = deriveDriveWarnings(
|
||||
[leg(60, 300), leg(60, 400), leg(60, 300), leg(60, 400)],
|
||||
[false, false, true, false, false],
|
||||
{ ...noLimits, rangeKm: 600 },
|
||||
0,
|
||||
)
|
||||
expect(out.warnings).toEqual([{ index: 4, code: 'range', sinceKm: 700 }])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-064: filling up starts the budget over, resting does not', () => {
|
||||
|
||||
@@ -304,10 +304,13 @@ export function sumLegSeconds(legSeconds: (number | undefined)[]): number {
|
||||
* an hour of the day would simply never fire. All three limits are therefore durations and
|
||||
* kilometres, which exist as soon as a leg has routed.
|
||||
*
|
||||
* The range budget carries across midnight, because a tank does not empty overnight, and
|
||||
* resets to zero after a finding: without that reset, one trip of 1800 km on a 600 km
|
||||
* range flags every leg after the first overrun, which is a column of red nobody reads.
|
||||
* With it, the marks stand one range apart and each one means "fill up around here".
|
||||
* The range budget resets at a fuel or charging stop and NOWHERE ELSE. It carries across
|
||||
* midnight, because a tank does not empty overnight, and it keeps counting past a finding,
|
||||
* because a warning is not a fill-up. Zeroing it there made the second figure wrong and
|
||||
* every one after it: a day reading 245 km at one stop and 121 at the next claims the 121
|
||||
* is a fresh tank, when it is 366 on the same one. A run of warnings down a stretch with
|
||||
* no fuel on it is not noise; it is the answer, and it stops the moment a fuel or
|
||||
* charging stop is put in.
|
||||
*
|
||||
* An unrouted leg gives the budget up entirely rather than guessing, the same way the
|
||||
* schedule abandons its cursor: a distance we do not have cannot be added to one we do.
|
||||
@@ -341,9 +344,12 @@ export function deriveDriveWarnings(
|
||||
budget = null
|
||||
} else if (budget !== null) {
|
||||
budget += metres / 1000
|
||||
if (limits.rangeKm && budget > limits.rangeKm) {
|
||||
// Not on a stop that fills up: arriving at a petrol station with an empty tank is
|
||||
// the plan working, not a problem, and a warning there would sit on the one stop
|
||||
// that answers it. No reset either — a warning is not a fill-up, and the figure has
|
||||
// to keep counting until a fuel or charging stop actually puts one in.
|
||||
if (limits.rangeKm && budget > limits.rangeKm && !refuelsAt[at]) {
|
||||
warnings.push({ index: at, code: 'range', sinceKm: Math.round(budget) })
|
||||
budget = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user