diff --git a/client/src/components/Map/useMergedMapPois.test.ts b/client/src/components/Map/useMergedMapPois.test.ts
index 1ecb7a2cd..63d880bb3 100644
--- a/client/src/components/Map/useMergedMapPois.test.ts
+++ b/client/src/components/Map/useMergedMapPois.test.ts
@@ -57,3 +57,30 @@ describe('useMergedMapPois', () => {
expect(result.current).toBe(first)
})
})
+
+describe('useMergedMapPois — the refuel offers', () => {
+ const poi = (id: string): Poi => ({ osm_id: id, name: id, lat: 0, lng: 0 } as Poi)
+
+ it('FE-MAP-MERGEPOI-007: an offered station is drawn even with no search running', () => {
+ // In road trip mode this is the only channel a POI reaches the map through, so
+ // without it somebody is asked to accept a stop they cannot see.
+ const { result } = renderHook(() => useMergedMapPois([], [], [poi('offer')]))
+ expect(result.current.map(p => p.osm_id)).toEqual(['offer'])
+ })
+
+ it('FE-MAP-MERGEPOI-008: an offer already on the route is one pin, not two', () => {
+ const { result } = renderHook(() => useMergedMapPois([poi('a')], [], [poi('a'), poi('b')]))
+ expect(result.current.map(p => p.osm_id)).toEqual(['a', 'b'])
+ })
+
+ it('FE-MAP-MERGEPOI-009: nothing offered keeps the same array, so no pin is rebuilt', () => {
+ const corridor = [poi('a')]
+ const { result, rerender } = renderHook(({ o }: { o: Poi[] }) => useMergedMapPois(corridor, [], o), {
+ initialProps: { o: [] as Poi[] },
+ })
+ const first = result.current
+ rerender({ o: [] })
+ expect(result.current).toBe(first)
+ expect(result.current).toBe(corridor)
+ })
+})
diff --git a/client/src/components/Map/useMergedMapPois.ts b/client/src/components/Map/useMergedMapPois.ts
index b6253671a..a82209ba6 100644
--- a/client/src/components/Map/useMergedMapPois.ts
+++ b/client/src/components/Map/useMergedMapPois.ts
@@ -13,16 +13,30 @@ import type { Poi } from './poiCategories'
* on: a hotel found both along the route and in the viewport is one pin, and the corridor's
* copy wins because it carries how far along the drive it sits.
*
+ * `offered` is the third case: the handful of filling stations proposed where a tank runs
+ * out. They belong on the map for a plain reason — somebody is being asked to accept a
+ * stop, and a stop you cannot see is not one you can judge — but they are not a search
+ * result and must not survive the panel being closed, which is why they arrive separately
+ * rather than being folded into the corridor's own list.
+ *
* Returns the input array itself whenever there is nothing to merge. The map redraws off
* this reference, so handing it a fresh array on every render would rebuild every pin.
*/
-export function useMergedMapPois(corridor: Poi[] | null, explore: Poi[]): Poi[] {
+export function useMergedMapPois(corridor: Poi[] | null, explore: Poi[], offered: Poi[] = NONE): Poi[] {
return useMemo(() => {
- if (!corridor) return explore
- if (!explore.length) return corridor
- if (!corridor.length) return explore
- const seen = new Set(corridor.map(p => p.osm_id))
- const extra = explore.filter(p => !seen.has(p.osm_id))
- return extra.length ? [...corridor, ...extra] : corridor
- }, [corridor, explore])
+ const base = corridor === null ? explore : merge(corridor, explore)
+ return merge(base, offered)
+ }, [corridor, explore, offered])
+}
+
+/** Nothing offered, as one stable array, so the default never changes identity. */
+const NONE: Poi[] = []
+
+/** `a` with everything from `b` that it does not already carry, by `osm_id`. */
+function merge(a: Poi[], b: Poi[]): Poi[] {
+ if (!b.length) return a
+ if (!a.length) return b
+ const seen = new Set(a.map(p => p.osm_id))
+ const extra = b.filter(p => !seen.has(p.osm_id))
+ return extra.length ? [...a, ...extra] : a
}
diff --git a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx
index da58cb498..82baaf3b4 100644
--- a/client/src/components/Roadtrip/RoadtripLimitsCard.tsx
+++ b/client/src/components/Roadtrip/RoadtripLimitsCard.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react'
-import { Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship } from 'lucide-react'
+import { Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship, Car, Zap } from 'lucide-react'
import Modal from '../shared/Modal'
+import CustomSelect from '../shared/CustomSelect'
import { useTranslation } from '../../i18n/TranslationContext'
import { useSettingsStore } from '../../store/settingsStore'
import { convertDistance, formatDistance } from '../../utils/units'
@@ -103,6 +104,13 @@ function AvoidRow({ icon: Icon, label, on, disabled, onToggle }: {
)
}
+/** Not said, petrol, electric. "Not said" first, because it is what everybody starts on. */
+const VEHICLES: { key: 'combustion' | 'electric' | null; labelKey: string; Icon: typeof Car }[] = [
+ { key: null, labelKey: 'roadtrip.limit.vehicleAny', Icon: Car },
+ { key: 'combustion', labelKey: 'roadtrip.limit.vehicleCombustion', Icon: Fuel },
+ { key: 'electric', labelKey: 'roadtrip.limit.vehicleElectric', Icon: Zap },
+]
+
export default function RoadtripLimitsCard({ onSave }: {
/**
* Persists one setting. Absent leaves the dialog read-only.
@@ -123,6 +131,7 @@ export default function RoadtripLimitsCard({ onSave }: {
// Parsed rather than trusted: a per-user setting gets no server-side validation, and
// an unknown word here would become a costing option the router does not have.
+ const vehicle = settings.roadtrip_vehicle ?? ''
const avoiding = parseAvoid(settings.roadtrip_avoid)
// No second engine, no avoidance. An instance pointed at its own OSRM has one, and its
// car profile is built without excludable classes on every public host.
@@ -234,6 +243,25 @@ export default function RoadtripLimitsCard({ onSave }: {
placeholder={t('roadtrip.limit.off')}
onChange={v => onSave?.('roadtrip_day_minutes', v)}
/>
+ {/* Above the range, because it decides what the range even means: with no
+ vehicle named, a petrol station and a charger both fill the tank, which
+ is arithmetic that is wrong for everybody who drives just one of them. */}
+
s.settings.distance_unit)
const key = `${dayId}:${dry.legIndex}`
const open = refuel.openFor === key
+ const settled = open && !refuel.loading && refuel.outcome
return (
-
+ {/* Built as a drive band, because that is what it belongs to: the leg the fuel
+ runs out on. Same height, same type, same rail. The warning ring is the only
+ difference, and it is the whole message — this is the band where the tank
+ ends. */}
+
+
+
+
+ {t('roadtrip.refuel.dry')}
+
+ {/* How far INTO this leg, where a drive band keeps its figures. Not the range
+ that was crossed: that is the traveller's own setting, says nothing about
+ where, and made every band on a day read the same number. */}
+
+ {t('roadtrip.refuel.after', { distance: formatDistance(Math.round(dry.intoLegKm), distanceUnit) })}
+
+ {/* An icon, the size of the shuffle mark a drive band carries, because the
+ band has to stay one line high beside its neighbours. What it does is in
+ the tooltip and in the label a screen reader gets. */}
+ {open && (refuel.loading || refuel.results.length) ? (
+
+
+
+ ) : (
+ /* An answer that found nothing leaves the button, not a dead end. The place
+ search is a shared public service that does time out, and "it did not
+ answer" with no way to ask again reads as broken rather than as busy. */
+
+
+
+ )}
+
+ {/* Three at most. This is an offer beside a plan, not a list to browse;
+ the corridor panel is where somebody goes to see all of them. */}
+ {refuel.results.slice(0, 3).map(poi => (
+
+
+ {poi.name}
+
+
+ {/* What is left when the car draws level, the figure that decides
+ whether this one is any use. The detour is already in it. */}
+ {t('roadtrip.refuel.spare', { distance: formatDistance(Math.round(poi.spareKm), distanceUnit) })}
+
+ {onAccept ? (
+
+ ) : null}
+
+ ))}
+
+ ) : (
+ /* Three different sentences for three different facts. "Nothing on this
+ stretch" after a request that failed or was cut short states something
+ that was never checked, which is worse than saying nothing. */
+
+ {refuel.outcome === 'none'
+ ? t('roadtrip.refuel.none')
+ : refuel.outcome === 'incomplete'
+ ? t('roadtrip.refuel.incomplete')
+ : t('roadtrip.refuel.failed')}
+
+ )
+ ) : null}
+
- {/* Three at most. This is an offer beside a plan, not a list to browse; the
- corridor panel is where somebody goes to see all of them. */}
- {refuel.results.slice(0, 3).map(poi => (
-
-
- {poi.name}
-
-
- {/* What is left when the car draws level, which is the figure that
- decides whether this one is any use. The detour is in it already. */}
- {t('roadtrip.refuel.spare', { distance: formatDistance(Math.round(poi.spareKm), distanceUnit) })}
-
- {onAccept ? (
-
- ) : null}
-
- ))}
-
- ) : (
- /* Three different sentences for three different facts. "Nothing on this
- stretch" after a request that failed or was cut short states something that
- was never checked, and that is worse than saying nothing. */
-
- {refuel.outcome === 'none'
- ? t('roadtrip.refuel.none')
- : refuel.outcome === 'incomplete'
- ? t('roadtrip.refuel.incomplete')
- : t('roadtrip.refuel.failed')}
-
- )
- ) : null}
)
}
@@ -956,6 +985,17 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
const { t, language } = useTranslation()
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
const last = day.stops.length - 1
+ /**
+ * The findings a stop wears, minus the one the refuel band already says better.
+ *
+ * The range warning marks the stop somebody FINDS OUT at and counts the whole tank;
+ * the band sits on the leg the fuel actually ends on and offers somewhere to stop. Both
+ * at once says the same thing twice, in the wrong order — the answer above the problem
+ * — so the warning stands down wherever the band is showing.
+ */
+ const showingBand = !!refuel && !loading && !!day.dryPoints?.length
+ const findingsFor = (i: number) =>
+ day.driveWarnings.filter(w => w.index === i && !(showingBand && w.code === 'range'))
// The running number a stop wears, with the service stops passed over — so a day with a
// charger halfway through still counts one, two, three the way its map pins do.
let counted = 0
@@ -1091,7 +1131,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
stop={stop}
entry={day.schedule.entries[i]}
late={marks.find(w => w.code === 'late')}
- driveFindings={day.driveWarnings.filter(w => w.index === i)}
+ driveFindings={findingsFor(i)}
selected={selectedAssignmentId === stop.assignmentId}
onSelect={onSelectStop ? () => onSelectStop(stop.placeId, stop.assignmentId) : undefined}
onEditStay={onEditStay ? () => onEditStay({ placeId: stop.placeId, name: stop.name, minutes: stop.dwellMinutes, arrival: day.schedule.entries[i]?.arrival ?? null }) : undefined}
@@ -1103,7 +1143,7 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onReorderStop, on
number={counted}
entry={day.schedule.entries[i]}
late={marks.find(w => w.code === 'late')}
- driveFindings={day.driveWarnings.filter(w => w.index === i)}
+ driveFindings={findingsFor(i)}
selected={selectedAssignmentId === stop.assignmentId}
continues={i < last}
starts={i === 0}
diff --git a/client/src/components/Roadtrip/refuelSuggestion.test.ts b/client/src/components/Roadtrip/refuelSuggestion.test.ts
index befbfc46e..886518769 100644
--- a/client/src/components/Roadtrip/refuelSuggestion.test.ts
+++ b/client/src/components/Roadtrip/refuelSuggestion.test.ts
@@ -77,8 +77,11 @@ describe('outcomeOf', () => {
// failed request states a fact that was never checked.
expect(outcomeOf([], null)).toBe('failed')
expect(outcomeOf([], { truncated: true })).toBe('incomplete')
- expect(outcomeOf([], { clamped: true })).toBe('incomplete')
expect(outcomeOf([], {})).toBe('none')
+ // `clamped` is not the same thing: it means the server searched a smaller circle
+ // than the box described, and the caller sizes the box so that never happens.
+ // Treating it as "could not check" turned every empty answer into a shrug.
+ expect(outcomeOf([], { clamped: true })).toBe('none')
})
it('FE-ROADTRIP-REFUEL-008: a hit is a hit even when the answer was cut short', () => {
diff --git a/client/src/components/Roadtrip/refuelSuggestion.ts b/client/src/components/Roadtrip/refuelSuggestion.ts
index 21721c413..9daf24b80 100644
--- a/client/src/components/Roadtrip/refuelSuggestion.ts
+++ b/client/src/components/Roadtrip/refuelSuggestion.ts
@@ -86,7 +86,10 @@ export function outcomeOf(
): RefuelOutcome {
if (!answer) return 'failed'
if (candidates.length) return 'found'
- return answer.truncated || answer.clamped ? 'incomplete' : 'none'
+ // `truncated` alone. `clamped` only means the server searched a smaller circle than
+ // the box described, which the caller now sizes so it never happens — and reporting it
+ // as "we could not check" turned every empty answer into a shrug.
+ return answer.truncated ? 'incomplete' : 'none'
}
/**
diff --git a/client/src/components/Roadtrip/roadtripModel.test.ts b/client/src/components/Roadtrip/roadtripModel.test.ts
index 5d9d2a2e4..f42cf94f2 100644
--- a/client/src/components/Roadtrip/roadtripModel.test.ts
+++ b/client/src/components/Roadtrip/roadtripModel.test.ts
@@ -11,6 +11,7 @@ import {
parseClock,
splitIntoRuns,
sumLegSeconds,
+ refuelStopTypeFor,
} from './roadtripModel'
describe('formatDurationShort', () => {
@@ -687,3 +688,41 @@ describe('deriveDriveWarnings — where the tank actually runs dry', () => {
expect(out.carryKm).toBeNull()
})
})
+
+describe('refuelsRange — what fills which tank', () => {
+ it('FE-ROADTRIP-MODEL-096: with no vehicle named, either kind fills up', () => {
+ // What TREK did before the setting existed, and the right answer for somebody who
+ // never opened the dialog. Anything else would quietly change their warnings.
+ expect(refuelsRange('fuel')).toBe(true)
+ expect(refuelsRange('charging')).toBe(true)
+ expect(refuelsRange('fuel', null)).toBe(true)
+ expect(refuelsRange('charging', null)).toBe(true)
+ })
+
+ it('FE-ROADTRIP-MODEL-097: a petrol station does not charge a battery', () => {
+ // The bug this exists for: an electric car pausing at a petrol station had its
+ // battery refilled on paper, the warnings went quiet for the rest of the day, and
+ // the driver was told nothing.
+ expect(refuelsRange('charging', 'electric')).toBe(true)
+ expect(refuelsRange('fuel', 'electric')).toBe(false)
+ })
+
+ it('FE-ROADTRIP-MODEL-098: and a charger does not fill a tank', () => {
+ expect(refuelsRange('fuel', 'combustion')).toBe(true)
+ expect(refuelsRange('charging', 'combustion')).toBe(false)
+ })
+
+ it('FE-ROADTRIP-MODEL-099: nothing else refuels anything, whatever is driven', () => {
+ for (const vehicle of [null, 'combustion', 'electric'] as const) {
+ expect(refuelsRange('rest_area', vehicle)).toBe(false)
+ expect(refuelsRange('restaurant', vehicle)).toBe(false)
+ expect(refuelsRange(null, vehicle)).toBe(false)
+ }
+ })
+
+ it('FE-ROADTRIP-MODEL-100: the search looks for what the vehicle actually takes', () => {
+ expect(refuelStopTypeFor('combustion')).toEqual(['fuel'])
+ expect(refuelStopTypeFor('electric')).toEqual(['charging'])
+ expect(refuelStopTypeFor(null)).toEqual(['fuel', 'charging'])
+ })
+})
diff --git a/client/src/components/Roadtrip/roadtripModel.ts b/client/src/components/Roadtrip/roadtripModel.ts
index 85e834fa5..91fed9b37 100644
--- a/client/src/components/Roadtrip/roadtripModel.ts
+++ b/client/src/components/Roadtrip/roadtripModel.ts
@@ -123,9 +123,32 @@ export interface ScheduleWarning {
*/
export const REFUELLING_STOP_TYPES = ['fuel', 'charging'] as const
-/** Whether stopping here starts the range budget over. */
-export function refuelsRange(stopType: string | null | undefined): boolean {
- return (REFUELLING_STOP_TYPES as readonly string[]).includes(stopType ?? '')
+/**
+ * What the traveller drives, which decides what actually fills the tank.
+ *
+ * Absent means both count, which is what TREK did before the setting existed and is the
+ * right answer for somebody who never said. It is only wrong once a vehicle IS named:
+ * a petrol station does not charge a battery, and a charger does not fill a tank.
+ */
+export type VehicleKind = 'combustion' | 'electric'
+
+/** The stop kind that refills the named vehicle, for filtering a search. */
+export function refuelStopTypeFor(vehicle: VehicleKind | null | undefined): readonly string[] {
+ if (vehicle === 'combustion') return ['fuel']
+ if (vehicle === 'electric') return ['charging']
+ return REFUELLING_STOP_TYPES
+}
+
+/**
+ * Whether stopping here starts the range budget over.
+ *
+ * With no vehicle named, either kind does — a plain reading of "I filled up". Once one is
+ * named the other stops counting, because the alternative is arithmetic that is simply
+ * wrong: an electric car pausing at a petrol station had its battery refilled on paper,
+ * the warnings went quiet for the rest of the day, and the driver was told nothing.
+ */
+export function refuelsRange(stopType: string | null | undefined, vehicle?: VehicleKind | null): boolean {
+ return refuelStopTypeFor(vehicle).includes(stopType ?? '')
}
/**
diff --git a/client/src/components/Roadtrip/useRefuelSearch.ts b/client/src/components/Roadtrip/useRefuelSearch.ts
index faf83735e..1d6ad2413 100644
--- a/client/src/components/Roadtrip/useRefuelSearch.ts
+++ b/client/src/components/Roadtrip/useRefuelSearch.ts
@@ -1,8 +1,10 @@
-import { useCallback, useRef, useState } from 'react'
+import { useCallback, useMemo, useRef, useState } from 'react'
import { mapsApi } from '../../api/client'
import { useTranslation } from '../../i18n'
import { isEffectivelyOffline } from '../../sync/networkMode'
-import { boxAround, type LatLng } from './corridor'
+import { useSettingsStore } from '../../store/settingsStore'
+import { refuelStopTypeFor, type VehicleKind } from './roadtripModel'
+import { boxAround, pointAtMeters, type LatLng } from './corridor'
import { reachableRefuels, outcomeOf, type RefuelCandidate, type RefuelOutcome } from './refuelSuggestion'
/**
@@ -21,8 +23,27 @@ import { reachableRefuels, outcomeOf, type RefuelCandidate, type RefuelOutcome }
* One request, one day, no shared state touched.
*/
-/** How far around the dry point to look, in kilometres. */
-const LOOK_KM = 25
+/**
+ * How far around the search point to look, in kilometres.
+ *
+ * Fourteen and not more, because the server turns a box into a circle of half its
+ * diagonal and silently narrows anything past 20 km. A box of ±14 asks for 19.8, which
+ * is the largest question it answers in full — ±25 asked for 35 and came back flagged
+ * as narrowed every single time, which then read on screen as "we could not check".
+ */
+const LOOK_KM = 14
+
+/**
+ * How far BEFORE the dry point to centre that circle, in kilometres.
+ *
+ * Only the road already driven is any use: a station past the point is on the far side
+ * of an empty tank. Centring on the dry point spends half the circle on road nobody can
+ * reach, so it is pulled back far enough that the useful half becomes the whole of it.
+ */
+const LOOK_BACK_KM = 12
+
+/** Nothing on offer, as one stable array, so an idle search never redraws the map. */
+const NONE: RefuelCandidate[] = []
export interface RefuelSearch {
/** Which dry point is being answered, as `:`, or null when idle. */
@@ -30,6 +51,15 @@ export interface RefuelSearch {
loading: boolean
outcome: RefuelOutcome | null
results: RefuelCandidate[]
+ /**
+ * The ones on offer, for the map to draw.
+ *
+ * The same three the rail lists and no more: somebody is being asked to accept a stop,
+ * and a stop that cannot be seen is not one that can be judged. Empty while nothing is
+ * open, and the same array each time, because the map rebuilds every pin when this
+ * reference changes.
+ */
+ offered: RefuelCandidate[]
/** Runs the one request. `key` identifies the dry point, so only one is open at a time. */
ask: (key: string, at: LatLng, line: LatLng[], dryAlongKm: number, existing: LatLng[]) => Promise
close: () => void
@@ -37,6 +67,9 @@ export interface RefuelSearch {
export function useRefuelSearch(): RefuelSearch {
const { locale } = useTranslation()
+ // What to look for. Somebody who said they drive an electric car has no use for a
+ // petrol station in the list, and the other way round; with nothing said, both.
+ const vehicle = useSettingsStore(s => s.settings.roadtrip_vehicle)
const [openFor, setOpenFor] = useState(null)
const [loading, setLoading] = useState(false)
const [outcome, setOutcome] = useState(null)
@@ -76,7 +109,14 @@ export function useRefuelSearch(): RefuelSearch {
setLoading(true)
try {
- const answer = await mapsApi.pois('fuel,charging', boxAround(at, LOOK_KM), locale, controller.signal)
+ const kind: VehicleKind | null =
+ vehicle === 'combustion' || vehicle === 'electric' ? vehicle : null
+ const wanted = refuelStopTypeFor(kind).join(',')
+ // Pulled back along the road, because only what lies before the dry point can be
+ // reached. `at` stays the dry point itself: it is what the band names and what the
+ // map is asked to show.
+ const centre = pointAtMeters(line, Math.max(0, (dryAlongKm - LOOK_BACK_KM) * 1000)) ?? at
+ const answer = await mapsApi.pois(wanted, boxAround(centre, LOOK_KM), locale, controller.signal)
if (controller.signal.aborted) return
const candidates = reachableRefuels(answer.pois, line, dryAlongKm, { existing })
setResults(candidates)
@@ -86,7 +126,9 @@ export function useRefuelSearch(): RefuelSearch {
} finally {
if (!controller.signal.aborted) setLoading(false)
}
- }, [locale])
+ }, [locale, vehicle])
- return { openFor, loading, outcome, results, ask, close }
+ const offered = useMemo(() => (openFor ? results.slice(0, 3) : NONE), [openFor, results])
+
+ return { openFor, loading, outcome, results, offered, ask, close }
}
diff --git a/client/src/components/Roadtrip/useRoadtripRoutes.ts b/client/src/components/Roadtrip/useRoadtripRoutes.ts
index b0d1e310a..99042655d 100644
--- a/client/src/components/Roadtrip/useRoadtripRoutes.ts
+++ b/client/src/components/Roadtrip/useRoadtripRoutes.ts
@@ -3,7 +3,7 @@ import { calculateRouteWithLegs, RoutingRefusedError } from '../Map/RouteCalcula
import { resolveLegMode } from '../Planner/legMode'
import {
computeSchedule, deriveDriveWarnings, isServiceStopType, legIndexForAlong, refuelsRange, splitIntoRuns,
- type DayWarning, type DriveLimits, type Schedule, type ScheduleWarning, parseAvoid, type DryPoint,} from './roadtripModel'
+ type DayWarning, type DriveLimits, type Schedule, type ScheduleWarning, parseAvoid, type DryPoint, type VehicleKind } from './roadtripModel'
import { projectOntoRoute, pointAtMeters} from './corridor'
import { useSettingsStore } from '../../store/settingsStore'
import type { Assignment, AssignmentsMap, Day, RouteAvoidClass, RouteSegment, RouteVia, SnappedWaypoint } from '../../types'
@@ -283,8 +283,13 @@ export function useRoadtripRoutes(
* any value — and an unknown word here would become a costing option the router does
* not have.
*/
+ const vehicle = useSettingsStore(s => s.settings.roadtrip_vehicle)
const avoidSetting = useSettingsStore(s => s.settings.roadtrip_avoid)
const avoid = useMemo(() => parseAvoid(avoidSetting), [avoidSetting])
+ // Validated rather than trusted, like the avoidance above: a per-user setting gets no
+ // server-side check, and an unknown word here would silently stop both kinds counting.
+ const vehicleKind: VehicleKind | null =
+ vehicle === 'combustion' || vehicle === 'electric' ? vehicle : null
const avoidKey = avoid.join(',')
// Zero and absent both mean "no limit": zero is a legal thing to type and says the
// same thing, so it is folded here rather than guarded at every reading.
@@ -546,7 +551,7 @@ export function useRoadtripRoutes(
legs,
// One longer than the legs: the last stop of the day counts too, because filling
// up on arrival is what makes the next morning start full.
- stops.map(s => refuelsRange(s.stopType)),
+ stops.map(s => refuelsRange(s.stopType, vehicleKind)),
limits,
carryKm,
)
diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx
index 514999e89..8d0ce48b9 100644
--- a/client/src/pages/TripPlannerPage.tsx
+++ b/client/src/pages/TripPlannerPage.tsx
@@ -267,7 +267,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
setRoadtripStopKind,
saveRoadtripLimit,
roadtripVias, addRoadtripVia, moveRoadtripVia, removeRoadtripVia,
- routeAlternatives, askRouteAlternatives, refuel, askRefuel, acceptRefuel, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints,
+ routeAlternatives, askRouteAlternatives, refuel, askRefuel, acceptRefuel, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints, mapFocusPoints,
stayDraft, setStayDraft, setRoadtripStay,
highlightedAlternative, setHighlightedAlternative,
moveRoadtripStopToDay,
@@ -325,7 +325,10 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
// drive ("is there a hotel at tonight's stop" versus "what is along the way"), and the
// two answers are drawn side by side rather than one hiding the other.
const poiPillEnabled = useSettingsStore(s => s.settings.map_poi_pill_enabled) !== false
- const mapPois = useMergedMapPois(roadtripActive ? roadtripCorridor.visible : null, poi.pois)
+ // The refuel offers ride the same channel: in road trip mode this is the only way a
+ // POI reaches the map, so without them somebody is asked to accept a stop they cannot
+ // see. They vanish with the offer rather than lingering as a search result.
+ const mapPois = useMergedMapPois(roadtripActive ? roadtripCorridor.visible : null, poi.pois, refuel.offered)
// Costs expense editor opened from a booking modal (save-then-open). Lives at the
// page level so it has tripMembers / base currency / current user available.
@@ -443,7 +446,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
onRouteClick={roadtripActive && can('day_edit', trip) ? addRoadtripVia : undefined}
roadtripVias={roadtripActive ? roadtripVias.byDay : undefined}
alternativeRoutes={alternativeOverlays}
- focusPoints={alternativeFocusPoints}
+ focusPoints={mapFocusPoints}
clusterLoosely={roadtripActive}
activeAlternative={highlightedAlternative}
onChooseAlternative={chooseRouteAlternative}
diff --git a/client/src/pages/TripPlannerPage.wiring.test.tsx b/client/src/pages/TripPlannerPage.wiring.test.tsx
index 1c7b09788..1cc0b59ba 100644
--- a/client/src/pages/TripPlannerPage.wiring.test.tsx
+++ b/client/src/pages/TripPlannerPage.wiring.test.tsx
@@ -150,6 +150,9 @@ function baseState(): HookState {
roadtripActive: false,
roadtripRoutes: { lines: [], segments: [], days: [], loading: false, totalStops: 0 },
roadtripCorridor: { visible: [], day: null, dayId: '', setDayId: vi.fn(), categories: [], toggleCategory: vi.fn(), widthKm: 5, setWidthKm: vi.fn(), search: vi.fn(), nameFilter: '', setNameFilter: vi.fn(), insertIndexFor: vi.fn() },
+ refuel: { openFor: null, loading: false, outcome: null, results: [], offered: [], ask: vi.fn(), close: vi.fn() },
+ askRefuel: vi.fn(),
+ acceptRefuel: vi.fn(),
followTrack: { dayId: null, open: vi.fn(), close: vi.fn(), tracks: [], busy: false, round: 0, error: null, outcome: null, apply: vi.fn(), clear: vi.fn(), viaCount: 0, available: false },
roadtripViaCounts: {},
stopDraft: null,
diff --git a/client/src/pages/tripPlanner/useTripPlanner.ts b/client/src/pages/tripPlanner/useTripPlanner.ts
index 5a87b0139..5dc36c36e 100644
--- a/client/src/pages/tripPlanner/useTripPlanner.ts
+++ b/client/src/pages/tripPlanner/useTripPlanner.ts
@@ -1075,6 +1075,21 @@ export function useTripPlanner() {
[alternativeOverlays],
)
+ /**
+ * What the map should bring into view.
+ *
+ * Refuel offers win while they are open, and for the reason they exist at all: somebody
+ * is being asked to accept a stop, and a stop off the edge of the map cannot be judged.
+ * They are the newer, smaller and more specific answer, so they take the view from the
+ * alternatives rather than being averaged with them into a frame that shows neither.
+ */
+ const mapFocusPoints = useMemo<[number, number][]>(
+ () => (refuel.offered.length
+ ? refuel.offered.map(p => [p.lat, p.lng] as [number, number])
+ : alternativeFocusPoints),
+ [refuel.offered, alternativeFocusPoints],
+ )
+
/** Asks the router for other ways of driving one leg of one day. */
const askRouteAlternatives = useCallback((dayId: number, legIndex: number) => {
const day = roadtripRoutes.days.find(d => d.dayId === dayId)
@@ -1870,7 +1885,7 @@ export function useTripPlanner() {
saveRoadtripLimit,
roadtripVias, addRoadtripVia, moveRoadtripVia, removeRoadtripVia,
refuel, askRefuel, acceptRefuel,
- routeAlternatives, askRouteAlternatives, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints,
+ routeAlternatives, askRouteAlternatives, chooseRouteAlternative, alternativeOverlays, alternativeFocusPoints, mapFocusPoints,
stayDraft, setStayDraft, setRoadtripStay,
highlightedAlternative, setHighlightedAlternative,
moveRoadtripStopToDay,
diff --git a/client/src/store/settingsStore.ts b/client/src/store/settingsStore.ts
index d9c2f0169..58282177e 100644
--- a/client/src/store/settingsStore.ts
+++ b/client/src/store/settingsStore.ts
@@ -64,6 +64,8 @@ export const DEFAULT_SETTINGS: Settings = {
// Empty = the public Valhalla, unless routing_base_url names an own router, in which
// case no second engine is asked at all. See valhallaBase().
valhalla_base_url: '',
+ // Empty = not said, and then a stop of either kind fills up. See refuelsRange().
+ roadtrip_vehicle: '',
dark_mode: false,
// Empty = no personal display currency, so Costs falls back to the trip's own.
default_currency: '',
diff --git a/client/src/types.ts b/client/src/types.ts
index 67a48079c..d5b014097 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -159,6 +159,14 @@ export interface Settings {
* because it goes to the router as one request either way.
*/
roadtrip_avoid?: string
+ /**
+ * What the traveller drives: 'combustion', 'electric', or absent for "did not say".
+ *
+ * Absent has to mean BOTH kinds refill, which is what the range budget did before this
+ * existed. Anything else would quietly change the warnings of every traveller who never
+ * opened the dialog.
+ */
+ roadtrip_vehicle?: string
/** CARTO basemaps watermark keyless tiles; the key is appended as ?key= (#2054). */
carto_api_key?: string
mapbox_access_token?: string
diff --git a/shared/src/i18n/ar/roadtrip.ts b/shared/src/i18n/ar/roadtrip.ts
index 8dc5a24d1..3d999bda8 100644
--- a/shared/src/i18n/ar/roadtrip.ts
+++ b/shared/src/i18n/ar/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': 'تجنُّب {count}',
'roadtrip.avoid.missed': 'لا مفر من {classes}',
'roadtrip.avoid.missedHint': 'لا يوجد في هذا اليوم طريق بديل، لذلك تمر الرحلة منه. أما بقية الأيام فتظل تتجنب ما تستطيع.',
- 'roadtrip.refuel.dry': 'ينفد الخزان في هذه المرحلة، بعد {distance}',
+ 'roadtrip.refuel.dry': 'ينفد الخزان هنا',
+ 'roadtrip.refuel.after': 'بعد {distance}',
+ 'roadtrip.limit.vehicleLabel': 'بماذا تقود',
+ 'roadtrip.limit.vehicleAny': 'كلاهما',
+ 'roadtrip.limit.vehicleCombustion': 'وقود',
+ 'roadtrip.limit.vehicleElectric': 'كهرباء',
'roadtrip.refuel.find': 'ابحث عن وقود',
+ 'roadtrip.refuel.again': 'حاول مجددًا',
'roadtrip.refuel.looking': 'جارٍ البحث على طول الطريق…',
'roadtrip.refuel.spare': 'يتبقى {distance}',
'roadtrip.refuel.add': 'إضافة {name} كمحطة تزود بالوقود',
diff --git a/shared/src/i18n/br/roadtrip.ts b/shared/src/i18n/br/roadtrip.ts
index 4333b3b7c..e684d0781 100644
--- a/shared/src/i18n/br/roadtrip.ts
+++ b/shared/src/i18n/br/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} evitados',
'roadtrip.avoid.missed': '{classes} inevitável',
'roadtrip.avoid.missedHint': 'Este dia não tem rota que desvie, então o trajeto usa. Os outros dias continuam evitando o que dá.',
- 'roadtrip.refuel.dry': 'O tanque acaba neste trecho, após {distance}',
+ 'roadtrip.refuel.dry': 'Aqui o tanque acaba',
+ 'roadtrip.refuel.after': 'após {distance}',
+ 'roadtrip.limit.vehicleLabel': 'O que você dirige',
+ 'roadtrip.limit.vehicleAny': 'Ambos',
+ 'roadtrip.limit.vehicleCombustion': 'Combustível',
+ 'roadtrip.limit.vehicleElectric': 'Elétrico',
'roadtrip.refuel.find': 'Buscar combustível',
+ 'roadtrip.refuel.again': 'Tentar de novo',
'roadtrip.refuel.looking': 'Procurando ao longo da rota…',
'roadtrip.refuel.spare': 'restam {distance}',
'roadtrip.refuel.add': 'Adicionar {name} como parada de abastecimento',
diff --git a/shared/src/i18n/ca/roadtrip.ts b/shared/src/i18n/ca/roadtrip.ts
index 910f9018b..ff6826f23 100644
--- a/shared/src/i18n/ca/roadtrip.ts
+++ b/shared/src/i18n/ca/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} evitats',
'roadtrip.avoid.missed': '{classes} inevitable',
'roadtrip.avoid.missedHint': 'Aquest dia no té cap ruta que ho eviti, així que el trajecte ho fa servir. La resta de dies continuen evitant el que poden.',
- 'roadtrip.refuel.dry': 'El dipòsit s’esgota en aquesta etapa, després de {distance}',
+ 'roadtrip.refuel.dry': 'Aquí s’esgota el dipòsit',
+ 'roadtrip.refuel.after': 'després de {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Què condueixes',
+ 'roadtrip.limit.vehicleAny': 'Tots dos',
+ 'roadtrip.limit.vehicleCombustion': 'Benzina',
+ 'roadtrip.limit.vehicleElectric': 'Elèctric',
'roadtrip.refuel.find': 'Cerca combustible',
+ 'roadtrip.refuel.again': 'Torna-ho a provar',
'roadtrip.refuel.looking': 'Cercant al llarg de la ruta…',
'roadtrip.refuel.spare': 'queden {distance}',
'roadtrip.refuel.add': 'Afegeix {name} com a parada de combustible',
diff --git a/shared/src/i18n/cs/roadtrip.ts b/shared/src/i18n/cs/roadtrip.ts
index 9e9a9568d..b065b4d41 100644
--- a/shared/src/i18n/cs/roadtrip.ts
+++ b/shared/src/i18n/cs/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} vynecháno',
'roadtrip.avoid.missed': '{classes} nelze objet',
'roadtrip.avoid.missedHint': 'Tento den nemá objízdnou trasu, takže tudy jede. Ostatní dny se dál vyhýbají, čemu můžou.',
- 'roadtrip.refuel.dry': 'Na tomto úseku dojde palivo, po {distance}',
+ 'roadtrip.refuel.dry': 'Tady dojde palivo',
+ 'roadtrip.refuel.after': 'po {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Čím jezdíš',
+ 'roadtrip.limit.vehicleAny': 'Obojí',
+ 'roadtrip.limit.vehicleCombustion': 'Palivo',
+ 'roadtrip.limit.vehicleElectric': 'Elektro',
'roadtrip.refuel.find': 'Najít palivo',
+ 'roadtrip.refuel.again': 'Zkusit znovu',
'roadtrip.refuel.looking': 'Hledám podél trasy…',
'roadtrip.refuel.spare': 'zbývá {distance}',
'roadtrip.refuel.add': 'Přidat {name} jako zastávku na palivo',
diff --git a/shared/src/i18n/de/roadtrip.ts b/shared/src/i18n/de/roadtrip.ts
index 9d45278c3..e9461f66d 100644
--- a/shared/src/i18n/de/roadtrip.ts
+++ b/shared/src/i18n/de/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} gemieden',
'roadtrip.avoid.missed': '{classes} unvermeidbar',
'roadtrip.avoid.missedHint': 'An diesem Tag führt kein Weg daran vorbei, also fährt die Route darüber. Alle anderen Tage meiden weiterhin, was sie können.',
- 'roadtrip.refuel.dry': 'Auf dieser Etappe geht der Tank leer, nach {distance}',
+ 'roadtrip.refuel.dry': 'Hier ist der Tank leer',
+ 'roadtrip.refuel.after': 'nach {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Womit du fährst',
+ 'roadtrip.limit.vehicleAny': 'Beides',
+ 'roadtrip.limit.vehicleCombustion': 'Sprit',
+ 'roadtrip.limit.vehicleElectric': 'Strom',
'roadtrip.refuel.find': 'Tankstelle suchen',
+ 'roadtrip.refuel.again': 'Nochmal',
'roadtrip.refuel.looking': 'Suche entlang der Strecke…',
'roadtrip.refuel.spare': 'noch {distance}',
'roadtrip.refuel.add': '{name} als Tankstopp übernehmen',
diff --git a/shared/src/i18n/en/roadtrip.ts b/shared/src/i18n/en/roadtrip.ts
index 249dfbc1a..e28a88682 100644
--- a/shared/src/i18n/en/roadtrip.ts
+++ b/shared/src/i18n/en/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} avoided',
'roadtrip.avoid.missed': '{classes} unavoidable',
'roadtrip.avoid.missedHint': 'This day has no route around it, so the drive uses it. Every other day still avoids what it can.',
- 'roadtrip.refuel.dry': 'The tank runs out on this leg, {distance} in',
+ 'roadtrip.refuel.dry': 'Tank runs out here',
+ 'roadtrip.refuel.after': 'after {distance}',
+ 'roadtrip.limit.vehicleLabel': 'What you drive',
+ 'roadtrip.limit.vehicleAny': 'Either',
+ 'roadtrip.limit.vehicleCombustion': 'Petrol',
+ 'roadtrip.limit.vehicleElectric': 'Electric',
'roadtrip.refuel.find': 'Find fuel',
+ 'roadtrip.refuel.again': 'Try again',
'roadtrip.refuel.looking': 'Looking along the route…',
'roadtrip.refuel.spare': '{distance} to spare',
'roadtrip.refuel.add': 'Add {name} as a fuel stop',
diff --git a/shared/src/i18n/es/roadtrip.ts b/shared/src/i18n/es/roadtrip.ts
index b9d907570..67ae0e0cb 100644
--- a/shared/src/i18n/es/roadtrip.ts
+++ b/shared/src/i18n/es/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} evitados',
'roadtrip.avoid.missed': '{classes} inevitable(s)',
'roadtrip.avoid.missedHint': 'Este día no tiene ninguna ruta que lo evite, así que el trayecto lo usa. Los demás días siguen evitando lo que pueden.',
- 'roadtrip.refuel.dry': 'El depósito se vacía en este tramo, tras {distance}',
+ 'roadtrip.refuel.dry': 'Aquí se vacía el depósito',
+ 'roadtrip.refuel.after': 'tras {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Qué conduces',
+ 'roadtrip.limit.vehicleAny': 'Ambos',
+ 'roadtrip.limit.vehicleCombustion': 'Gasolina',
+ 'roadtrip.limit.vehicleElectric': 'Eléctrico',
'roadtrip.refuel.find': 'Buscar combustible',
+ 'roadtrip.refuel.again': 'Reintentar',
'roadtrip.refuel.looking': 'Buscando a lo largo de la ruta…',
'roadtrip.refuel.spare': 'quedan {distance}',
'roadtrip.refuel.add': 'Añadir {name} como parada de repostaje',
diff --git a/shared/src/i18n/fr/roadtrip.ts b/shared/src/i18n/fr/roadtrip.ts
index 6b40b2232..32f9e2f57 100644
--- a/shared/src/i18n/fr/roadtrip.ts
+++ b/shared/src/i18n/fr/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} évité(s)',
'roadtrip.avoid.missed': '{classes} inévitable(s)',
'roadtrip.avoid.missedHint': 'Cette journée n’a aucun itinéraire de contournement, le trajet l’emprunte donc. Les autres journées évitent toujours ce qu’elles peuvent.',
- 'roadtrip.refuel.dry': 'Le réservoir se vide sur cette étape, après {distance}',
+ 'roadtrip.refuel.dry': 'Réservoir vide ici',
+ 'roadtrip.refuel.after': 'après {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Ce que vous conduisez',
+ 'roadtrip.limit.vehicleAny': 'Les deux',
+ 'roadtrip.limit.vehicleCombustion': 'Carburant',
+ 'roadtrip.limit.vehicleElectric': 'Électrique',
'roadtrip.refuel.find': 'Chercher du carburant',
+ 'roadtrip.refuel.again': 'Réessayer',
'roadtrip.refuel.looking': 'Recherche le long du trajet…',
'roadtrip.refuel.spare': 'reste {distance}',
'roadtrip.refuel.add': 'Ajouter {name} comme arrêt carburant',
diff --git a/shared/src/i18n/gr/roadtrip.ts b/shared/src/i18n/gr/roadtrip.ts
index 87ce149a4..e8852eaaf 100644
--- a/shared/src/i18n/gr/roadtrip.ts
+++ b/shared/src/i18n/gr/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} αποφεύγονται',
'roadtrip.avoid.missed': '{classes} αναπόφευκτα',
'roadtrip.avoid.missedHint': 'Αυτή η ημέρα δεν έχει διαδρομή που να τα παρακάμπτει, οπότε η οδήγηση τα χρησιμοποιεί. Οι άλλες ημέρες εξακολουθούν να αποφεύγουν ό,τι μπορούν.',
- 'roadtrip.refuel.dry': 'Το ρεζερβουάρ αδειάζει σε αυτό το σκέλος, μετά από {distance}',
+ 'roadtrip.refuel.dry': 'Εδώ αδειάζει το ρεζερβουάρ',
+ 'roadtrip.refuel.after': 'μετά από {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Τι οδηγείς',
+ 'roadtrip.limit.vehicleAny': 'Και τα δύο',
+ 'roadtrip.limit.vehicleCombustion': 'Βενζίνη',
+ 'roadtrip.limit.vehicleElectric': 'Ηλεκτρικό',
'roadtrip.refuel.find': 'Εύρεση καυσίμων',
+ 'roadtrip.refuel.again': 'Ξανά',
'roadtrip.refuel.looking': 'Αναζήτηση κατά μήκος της διαδρομής…',
'roadtrip.refuel.spare': 'μένουν {distance}',
'roadtrip.refuel.add': 'Προσθήκη {name} ως στάση ανεφοδιασμού',
diff --git a/shared/src/i18n/hu/roadtrip.ts b/shared/src/i18n/hu/roadtrip.ts
index e3445d086..0c07ed8c1 100644
--- a/shared/src/i18n/hu/roadtrip.ts
+++ b/shared/src/i18n/hu/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} kerülve',
'roadtrip.avoid.missed': '{classes} nem kerülhető',
'roadtrip.avoid.missedHint': 'Ezen a napon nincs kerülőút, így az útvonal ezt használja. A többi nap továbbra is kerüli, amit tud.',
- 'roadtrip.refuel.dry': 'Ezen a szakaszon fogy ki a tank, {distance} után',
+ 'roadtrip.refuel.dry': 'Itt fogy ki a tank',
+ 'roadtrip.refuel.after': '{distance} után',
+ 'roadtrip.limit.vehicleLabel': 'Mivel utazol',
+ 'roadtrip.limit.vehicleAny': 'Mindkettő',
+ 'roadtrip.limit.vehicleCombustion': 'Üzemanyag',
+ 'roadtrip.limit.vehicleElectric': 'Elektromos',
'roadtrip.refuel.find': 'Üzemanyag keresése',
+ 'roadtrip.refuel.again': 'Újra',
'roadtrip.refuel.looking': 'Keresés az útvonal mentén…',
'roadtrip.refuel.spare': 'marad {distance}',
'roadtrip.refuel.add': '{name} hozzáadása tankolási megállóként',
diff --git a/shared/src/i18n/id/roadtrip.ts b/shared/src/i18n/id/roadtrip.ts
index f6071b000..169b4a2f2 100644
--- a/shared/src/i18n/id/roadtrip.ts
+++ b/shared/src/i18n/id/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} dihindari',
'roadtrip.avoid.missed': '{classes} tak terhindarkan',
'roadtrip.avoid.missedHint': 'Hari ini tidak ada rute memutarnya, jadi perjalanan melewatinya. Hari lain tetap menghindari yang bisa dihindari.',
- 'roadtrip.refuel.dry': 'Tangki habis di ruas ini, setelah {distance}',
+ 'roadtrip.refuel.dry': 'Tangki habis di sini',
+ 'roadtrip.refuel.after': 'setelah {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Kamu mengemudi apa',
+ 'roadtrip.limit.vehicleAny': 'Keduanya',
+ 'roadtrip.limit.vehicleCombustion': 'Bensin',
+ 'roadtrip.limit.vehicleElectric': 'Listrik',
'roadtrip.refuel.find': 'Cari bahan bakar',
+ 'roadtrip.refuel.again': 'Coba lagi',
'roadtrip.refuel.looking': 'Mencari di sepanjang rute…',
'roadtrip.refuel.spare': 'sisa {distance}',
'roadtrip.refuel.add': 'Tambahkan {name} sebagai perhentian isi bahan bakar',
diff --git a/shared/src/i18n/it/roadtrip.ts b/shared/src/i18n/it/roadtrip.ts
index ba741a196..bccf14109 100644
--- a/shared/src/i18n/it/roadtrip.ts
+++ b/shared/src/i18n/it/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} evitati',
'roadtrip.avoid.missed': '{classes} inevitabili',
'roadtrip.avoid.missedHint': 'Questa giornata non ha percorsi alternativi, quindi il viaggio li usa. Le altre giornate continuano a evitare quel che possono.',
- 'roadtrip.refuel.dry': 'Il serbatoio si esaurisce in questa tappa, dopo {distance}',
+ 'roadtrip.refuel.dry': 'Qui il serbatoio si esaurisce',
+ 'roadtrip.refuel.after': 'dopo {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Cosa guidi',
+ 'roadtrip.limit.vehicleAny': 'Entrambi',
+ 'roadtrip.limit.vehicleCombustion': 'Benzina',
+ 'roadtrip.limit.vehicleElectric': 'Elettrico',
'roadtrip.refuel.find': 'Cerca carburante',
+ 'roadtrip.refuel.again': 'Riprova',
'roadtrip.refuel.looking': 'Ricerca lungo il percorso…',
'roadtrip.refuel.spare': 'restano {distance}',
'roadtrip.refuel.add': 'Aggiungi {name} come sosta carburante',
diff --git a/shared/src/i18n/ja/roadtrip.ts b/shared/src/i18n/ja/roadtrip.ts
index 70808dd28..d592111b0 100644
--- a/shared/src/i18n/ja/roadtrip.ts
+++ b/shared/src/i18n/ja/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} 件を回避',
'roadtrip.avoid.missed': '{classes} は回避できず',
'roadtrip.avoid.missedHint': 'この日は迂回路がないため、そのまま通ります。ほかの日は引き続き避けられるものを避けます。',
- 'roadtrip.refuel.dry': 'この区間で燃料が尽きます({distance} 走行後)',
+ 'roadtrip.refuel.dry': 'ここで燃料切れ',
+ 'roadtrip.refuel.after': '{distance} 後',
+ 'roadtrip.limit.vehicleLabel': '何で走るか',
+ 'roadtrip.limit.vehicleAny': '両方',
+ 'roadtrip.limit.vehicleCombustion': 'ガソリン',
+ 'roadtrip.limit.vehicleElectric': '電気',
'roadtrip.refuel.find': '給油所を探す',
+ 'roadtrip.refuel.again': 'もう一度',
'roadtrip.refuel.looking': 'ルート沿いを検索中…',
'roadtrip.refuel.spare': '残り {distance}',
'roadtrip.refuel.add': '{name} を給油の停車地として追加',
diff --git a/shared/src/i18n/ko/roadtrip.ts b/shared/src/i18n/ko/roadtrip.ts
index 780ca16c7..c4cfe59d8 100644
--- a/shared/src/i18n/ko/roadtrip.ts
+++ b/shared/src/i18n/ko/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count}개 회피',
'roadtrip.avoid.missed': '{classes} 회피 불가',
'roadtrip.avoid.missedHint': '이 날은 우회로가 없어 그대로 지나갑니다. 다른 날은 계속 피할 수 있는 것을 피합니다.',
- 'roadtrip.refuel.dry': '이 구간에서 연료가 떨어집니다 ({distance} 주행 후)',
+ 'roadtrip.refuel.dry': '여기서 연료 소진',
+ 'roadtrip.refuel.after': '{distance} 뒤',
+ 'roadtrip.limit.vehicleLabel': '무엇으로 이동하나요',
+ 'roadtrip.limit.vehicleAny': '둘 다',
+ 'roadtrip.limit.vehicleCombustion': '연료',
+ 'roadtrip.limit.vehicleElectric': '전기',
'roadtrip.refuel.find': '주유소 찾기',
+ 'roadtrip.refuel.again': '다시 시도',
'roadtrip.refuel.looking': '경로를 따라 찾는 중…',
'roadtrip.refuel.spare': '{distance} 여유',
'roadtrip.refuel.add': '{name}을(를) 주유 정차지로 추가',
diff --git a/shared/src/i18n/nl/roadtrip.ts b/shared/src/i18n/nl/roadtrip.ts
index 007ab2e76..9fc025ba5 100644
--- a/shared/src/i18n/nl/roadtrip.ts
+++ b/shared/src/i18n/nl/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} vermeden',
'roadtrip.avoid.missed': '{classes} onvermijdelijk',
'roadtrip.avoid.missedHint': 'Deze dag heeft er geen route omheen, dus de rit gebruikt het. Alle andere dagen vermijden nog steeds wat ze kunnen.',
- 'roadtrip.refuel.dry': 'De tank raakt leeg op dit traject, na {distance}',
+ 'roadtrip.refuel.dry': 'Hier is de tank leeg',
+ 'roadtrip.refuel.after': 'na {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Wat je rijdt',
+ 'roadtrip.limit.vehicleAny': 'Beide',
+ 'roadtrip.limit.vehicleCombustion': 'Benzine',
+ 'roadtrip.limit.vehicleElectric': 'Elektrisch',
'roadtrip.refuel.find': 'Tankstation zoeken',
+ 'roadtrip.refuel.again': 'Opnieuw',
'roadtrip.refuel.looking': 'Zoeken langs de route…',
'roadtrip.refuel.spare': 'nog {distance}',
'roadtrip.refuel.add': '{name} als tankstop toevoegen',
diff --git a/shared/src/i18n/pl/roadtrip.ts b/shared/src/i18n/pl/roadtrip.ts
index 398b36ed2..f402ce6fd 100644
--- a/shared/src/i18n/pl/roadtrip.ts
+++ b/shared/src/i18n/pl/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} omijane',
'roadtrip.avoid.missed': '{classes} nie do ominięcia',
'roadtrip.avoid.missedHint': 'Tego dnia nie ma trasy w objazd, więc przejazd z tego korzysta. Pozostałe dni nadal omijają, co się da.',
- 'roadtrip.refuel.dry': 'Na tym odcinku kończy się paliwo, po {distance}',
+ 'roadtrip.refuel.dry': 'Tu kończy się paliwo',
+ 'roadtrip.refuel.after': 'po {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Czym jeździsz',
+ 'roadtrip.limit.vehicleAny': 'Oba',
+ 'roadtrip.limit.vehicleCombustion': 'Paliwo',
+ 'roadtrip.limit.vehicleElectric': 'Elektryk',
'roadtrip.refuel.find': 'Znajdź paliwo',
+ 'roadtrip.refuel.again': 'Ponów',
'roadtrip.refuel.looking': 'Szukam wzdłuż trasy…',
'roadtrip.refuel.spare': 'zostaje {distance}',
'roadtrip.refuel.add': 'Dodaj {name} jako postój na paliwo',
diff --git a/shared/src/i18n/ru/roadtrip.ts b/shared/src/i18n/ru/roadtrip.ts
index f57ca820d..12649b452 100644
--- a/shared/src/i18n/ru/roadtrip.ts
+++ b/shared/src/i18n/ru/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} в обход',
'roadtrip.avoid.missed': '{classes} не обойти',
'roadtrip.avoid.missedHint': 'В этот день объезда нет, поэтому поездка идёт через это. Остальные дни по-прежнему обходят, что могут.',
- 'roadtrip.refuel.dry': 'На этом участке кончится топливо, через {distance}',
+ 'roadtrip.refuel.dry': 'Здесь кончится топливо',
+ 'roadtrip.refuel.after': 'через {distance}',
+ 'roadtrip.limit.vehicleLabel': 'На чём вы едете',
+ 'roadtrip.limit.vehicleAny': 'Оба',
+ 'roadtrip.limit.vehicleCombustion': 'Бензин',
+ 'roadtrip.limit.vehicleElectric': 'Электро',
'roadtrip.refuel.find': 'Найти заправку',
+ 'roadtrip.refuel.again': 'Ещё раз',
'roadtrip.refuel.looking': 'Ищу вдоль маршрута…',
'roadtrip.refuel.spare': 'останется {distance}',
'roadtrip.refuel.add': 'Добавить {name} как остановку для заправки',
diff --git a/shared/src/i18n/sv/roadtrip.ts b/shared/src/i18n/sv/roadtrip.ts
index 8d48aa86e..4bc554425 100644
--- a/shared/src/i18n/sv/roadtrip.ts
+++ b/shared/src/i18n/sv/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} undviks',
'roadtrip.avoid.missed': '{classes} går inte att undvika',
'roadtrip.avoid.missedHint': 'Den här dagen har ingen väg runt, så körningen använder det. Övriga dagar undviker fortfarande det de kan.',
- 'roadtrip.refuel.dry': 'Tanken tar slut på den här etappen, efter {distance}',
+ 'roadtrip.refuel.dry': 'Här tar tanken slut',
+ 'roadtrip.refuel.after': 'efter {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Vad du kör',
+ 'roadtrip.limit.vehicleAny': 'Båda',
+ 'roadtrip.limit.vehicleCombustion': 'Bensin',
+ 'roadtrip.limit.vehicleElectric': 'El',
'roadtrip.refuel.find': 'Hitta bränsle',
+ 'roadtrip.refuel.again': 'Försök igen',
'roadtrip.refuel.looking': 'Söker längs rutten…',
'roadtrip.refuel.spare': '{distance} kvar',
'roadtrip.refuel.add': 'Lägg till {name} som tankstopp',
diff --git a/shared/src/i18n/tr/roadtrip.ts b/shared/src/i18n/tr/roadtrip.ts
index 235c83036..8508a5f58 100644
--- a/shared/src/i18n/tr/roadtrip.ts
+++ b/shared/src/i18n/tr/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} kaçınılıyor',
'roadtrip.avoid.missed': '{classes} kaçınılmaz',
'roadtrip.avoid.missedHint': 'Bu günün etrafından dolaşan bir rota yok, bu yüzden sürüş oradan geçiyor. Diğer günler kaçınabildiğinden kaçınmayı sürdürüyor.',
- 'roadtrip.refuel.dry': 'Bu etapta depo bitiyor, {distance} sonra',
+ 'roadtrip.refuel.dry': 'Depo burada bitiyor',
+ 'roadtrip.refuel.after': '{distance} sonra',
+ 'roadtrip.limit.vehicleLabel': 'Ne kullanıyorsun',
+ 'roadtrip.limit.vehicleAny': 'İkisi de',
+ 'roadtrip.limit.vehicleCombustion': 'Benzin',
+ 'roadtrip.limit.vehicleElectric': 'Elektrik',
'roadtrip.refuel.find': 'Yakıt bul',
+ 'roadtrip.refuel.again': 'Yeniden dene',
'roadtrip.refuel.looking': 'Rota boyunca aranıyor…',
'roadtrip.refuel.spare': '{distance} kalır',
'roadtrip.refuel.add': '{name} yakıt molası olarak eklensin',
diff --git a/shared/src/i18n/uk/roadtrip.ts b/shared/src/i18n/uk/roadtrip.ts
index 9f4fa7552..6707817bf 100644
--- a/shared/src/i18n/uk/roadtrip.ts
+++ b/shared/src/i18n/uk/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '{count} в обхід',
'roadtrip.avoid.missed': '{classes} не обійти',
'roadtrip.avoid.missedHint': 'Цього дня об’їзду немає, тому поїздка йде через це. Решта днів і далі обходять те, що можуть.',
- 'roadtrip.refuel.dry': 'На цій ділянці закінчиться пальне, через {distance}',
+ 'roadtrip.refuel.dry': 'Тут закінчиться пальне',
+ 'roadtrip.refuel.after': 'через {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Чим ви їдете',
+ 'roadtrip.limit.vehicleAny': 'Обидва',
+ 'roadtrip.limit.vehicleCombustion': 'Бензин',
+ 'roadtrip.limit.vehicleElectric': 'Електро',
'roadtrip.refuel.find': 'Знайти заправку',
+ 'roadtrip.refuel.again': 'Ще раз',
'roadtrip.refuel.looking': 'Шукаю вздовж маршруту…',
'roadtrip.refuel.spare': 'лишиться {distance}',
'roadtrip.refuel.add': 'Додати {name} як зупинку для заправки',
diff --git a/shared/src/i18n/vi/roadtrip.ts b/shared/src/i18n/vi/roadtrip.ts
index 3bc408bce..1bec30741 100644
--- a/shared/src/i18n/vi/roadtrip.ts
+++ b/shared/src/i18n/vi/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': 'tránh {count}',
'roadtrip.avoid.missed': '{classes} không tránh được',
'roadtrip.avoid.missedHint': 'Ngày này không có tuyến đường vòng, nên chặng đi qua đó. Những ngày khác vẫn tránh những gì có thể.',
- 'roadtrip.refuel.dry': 'Hết xăng ở chặng này, sau {distance}',
+ 'roadtrip.refuel.dry': 'Hết xăng ở đây',
+ 'roadtrip.refuel.after': 'sau {distance}',
+ 'roadtrip.limit.vehicleLabel': 'Bạn lái xe gì',
+ 'roadtrip.limit.vehicleAny': 'Cả hai',
+ 'roadtrip.limit.vehicleCombustion': 'Xăng',
+ 'roadtrip.limit.vehicleElectric': 'Điện',
'roadtrip.refuel.find': 'Tìm trạm xăng',
+ 'roadtrip.refuel.again': 'Thử lại',
'roadtrip.refuel.looking': 'Đang tìm dọc tuyến đường…',
'roadtrip.refuel.spare': 'còn {distance}',
'roadtrip.refuel.add': 'Thêm {name} làm điểm đổ xăng',
diff --git a/shared/src/i18n/zh-TW/roadtrip.ts b/shared/src/i18n/zh-TW/roadtrip.ts
index 60c93ce28..8900e3656 100644
--- a/shared/src/i18n/zh-TW/roadtrip.ts
+++ b/shared/src/i18n/zh-TW/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '避開 {count} 項',
'roadtrip.avoid.missed': '無法避開{classes}',
'roadtrip.avoid.missedHint': '這一天沒有繞行路線,因此仍會經過。其他日期依舊盡量避開。',
- 'roadtrip.refuel.dry': '本段將耗盡油量,行駛 {distance} 後',
+ 'roadtrip.refuel.dry': '此處將耗盡油量',
+ 'roadtrip.refuel.after': '{distance} 後',
+ 'roadtrip.limit.vehicleLabel': '你開什麼車',
+ 'roadtrip.limit.vehicleAny': '兩者',
+ 'roadtrip.limit.vehicleCombustion': '燃油',
+ 'roadtrip.limit.vehicleElectric': '電動',
'roadtrip.refuel.find': '尋找加油站',
+ 'roadtrip.refuel.again': '重試',
'roadtrip.refuel.looking': '正在沿路線尋找…',
'roadtrip.refuel.spare': '剩餘 {distance}',
'roadtrip.refuel.add': '將 {name} 加入為加油停靠點',
diff --git a/shared/src/i18n/zh/roadtrip.ts b/shared/src/i18n/zh/roadtrip.ts
index 81012d2c6..9697bafac 100644
--- a/shared/src/i18n/zh/roadtrip.ts
+++ b/shared/src/i18n/zh/roadtrip.ts
@@ -38,8 +38,14 @@ const roadtrip: TranslationStrings = {
'roadtrip.avoid.badge': '避开 {count} 项',
'roadtrip.avoid.missed': '无法避开{classes}',
'roadtrip.avoid.missedHint': '这一天没有绕行路线,因此仍会经过。其他日期依旧尽量避开。',
- 'roadtrip.refuel.dry': '本段将耗尽油量,行驶 {distance} 后',
+ 'roadtrip.refuel.dry': '此处将耗尽油量',
+ 'roadtrip.refuel.after': '{distance} 后',
+ 'roadtrip.limit.vehicleLabel': '你开什么车',
+ 'roadtrip.limit.vehicleAny': '两者',
+ 'roadtrip.limit.vehicleCombustion': '燃油',
+ 'roadtrip.limit.vehicleElectric': '电动',
'roadtrip.refuel.find': '查找加油站',
+ 'roadtrip.refuel.again': '重试',
'roadtrip.refuel.looking': '正在沿路线查找…',
'roadtrip.refuel.spare': '剩余 {distance}',
'roadtrip.refuel.add': '将 {name} 添加为加油停靠点',