mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(roadtrip): narrow the corridor to one stretch of the drive
A seven-hundred-kilometre day turns up seventy hits spread over the whole of it, and the traveller wanting a break near Berlin has to read all seventy and work out from "245 km along the route" which ones are anywhere near. That is the wrong way round: you decide roughly where to stop, and then look at what is there. Picking a point narrows the list to what lies within 25, 50 or 100 km of it. This is a subtraction rather than a second search, because the stops and the hits are already measured along the same thinned line. The midpoints of the legs are offered alongside the stops, and they are the point of the feature: a break on a five-hour drive belongs in the middle of the drive, not at either end. Asking only about stops would answer "what is near Berlin" when the question is "what is near the halfway mark". The chosen point is stored as which anchor it is rather than as a distance. alongKm is measured against a line thinned with a tolerance derived from the corridor width, so a kilometre figure kept here would quietly drift when the width goes from 5 to 10; resolved through the stops it lands on the same place again. It also names its day, so switching days cannot apply yesterday's break to today's drive.
This commit is contained in:
@@ -91,6 +91,11 @@ function corridor(over: Partial<RoadtripCorridor> = {}, search: Partial<Roadtrip
|
||||
widthKm: 5,
|
||||
setWidthKm: vi.fn(),
|
||||
nameFilter: '',
|
||||
anchors: [],
|
||||
section: null,
|
||||
setSection: vi.fn(),
|
||||
sectionKm: 50,
|
||||
setSectionKm: vi.fn(),
|
||||
socketFilter: '',
|
||||
setSocketFilter: vi.fn(),
|
||||
minKw: 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ import RoadtripCategoryPicker from './RoadtripCategoryPicker'
|
||||
import { serviceColor } from './roadtripModel'
|
||||
import { CORRIDOR_CATEGORY_BY_KEY } from './stopKinds'
|
||||
import { FS } from './typeScale'
|
||||
import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor'
|
||||
import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_SECTION_KM, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor'
|
||||
import type { CorridorPoi } from './useCorridorPois'
|
||||
import type { RoadtripRoutes } from './useRoadtripRoutes'
|
||||
|
||||
@@ -399,6 +399,55 @@ export default function RoadtripCorridorPanel({ corridor, routes, onAddPoi }: Ro
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Around one point of the drive, which is how a break is actually planned: you
|
||||
decide roughly where to stop and then look at what is there, rather than
|
||||
reading seventy hits spread over seven hundred kilometres. The midpoint of
|
||||
a leg is offered alongside the stops, because a break on a five-hour drive
|
||||
belongs in the middle of it and not at either end. */}
|
||||
{corridor.anchors.length > 1 && corridor.day ? (
|
||||
<div className="mt-2 flex gap-2">
|
||||
<CustomSelect
|
||||
value={corridor.section ? `${corridor.section.kind}:${corridor.section.index}` : ''}
|
||||
onChange={value => {
|
||||
const raw = String(value)
|
||||
if (!raw) { corridor.setSection(null); return }
|
||||
const [kind, index] = raw.split(':')
|
||||
corridor.setSection({
|
||||
dayId: corridor.day!.dayId,
|
||||
kind: kind as 'stop' | 'leg',
|
||||
index: Number(index),
|
||||
})
|
||||
}}
|
||||
options={[
|
||||
{ value: '', label: t('roadtrip.poi.wholeDay') },
|
||||
...corridor.anchors.map(a => ({
|
||||
value: `${a.kind}:${a.index}`,
|
||||
label: a.kind === 'stop'
|
||||
? (corridor.day!.stops[a.index]?.name ?? t('roadtrip.poi.wholeDay'))
|
||||
: t('roadtrip.poi.midLeg', {
|
||||
from: corridor.day!.stops[a.index]?.name ?? '',
|
||||
to: corridor.day!.stops[a.index + 1]?.name ?? '',
|
||||
}),
|
||||
})),
|
||||
]}
|
||||
style={{ flex: 1 }}
|
||||
size="sm"
|
||||
/>
|
||||
{corridor.section ? (
|
||||
<CustomSelect
|
||||
value={String(corridor.sectionKm)}
|
||||
onChange={value => corridor.setSectionKm(Number(value))}
|
||||
options={CORRIDOR_SECTION_KM.map(km => ({
|
||||
value: String(km),
|
||||
label: `± ${formatDistance(km, distanceUnit)}`,
|
||||
}))}
|
||||
style={{ width: 110 }}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Only where there is a charger to narrow. Two answers OSM actually carries
|
||||
often enough to filter on: which plug, and how fast. A station that states
|
||||
neither stays in the list, because roughly two thirds of them state no
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deriveDriveWarnings,
|
||||
legIndexForAlong,
|
||||
refuelsRange,
|
||||
sectionAnchors,
|
||||
parseClock,
|
||||
splitIntoRuns,
|
||||
sumLegSeconds,
|
||||
@@ -421,3 +422,22 @@ describe('deriveDriveWarnings', () => {
|
||||
expect(out.carryKm).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sectionAnchors', () => {
|
||||
it('FE-ROADTRIP-MODEL-080: every stop, and the middle of every leg between them', () => {
|
||||
// The leg midpoints are the point of the feature: a break on a five-hour drive is
|
||||
// planned in the middle of the drive, not at either end of it.
|
||||
expect(sectionAnchors([0, 100, 250])).toEqual([
|
||||
{ kind: 'stop', index: 0, alongKm: 0 },
|
||||
{ kind: 'leg', index: 0, alongKm: 50 },
|
||||
{ kind: 'stop', index: 1, alongKm: 100 },
|
||||
{ kind: 'leg', index: 1, alongKm: 175 },
|
||||
{ kind: 'stop', index: 2, alongKm: 250 },
|
||||
])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-MODEL-081: a single stop has no leg to sit in the middle of', () => {
|
||||
expect(sectionAnchors([42])).toEqual([{ kind: 'stop', index: 0, alongKm: 42 }])
|
||||
expect(sectionAnchors([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -373,6 +373,32 @@ export function legIndexForAlong(legEndMeters: number[], alongMeters: number): n
|
||||
return legEndMeters.length - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* The places on a day somebody would plan a break around.
|
||||
*
|
||||
* Every stop, and the middle of every leg between two of them. The leg midpoints are the
|
||||
* point of the whole thing: a break on a five-hour drive is planned in the middle of the
|
||||
* drive, not at either end of it, and offering only the stops would mean asking "what is
|
||||
* near Berlin" when the question is "what is near the halfway mark".
|
||||
*/
|
||||
export interface SectionAnchor {
|
||||
kind: 'stop' | 'leg'
|
||||
/** Index of the stop, or of the stop the leg leaves. */
|
||||
index: number
|
||||
alongKm: number
|
||||
}
|
||||
|
||||
export function sectionAnchors(stopsAlongKm: number[]): SectionAnchor[] {
|
||||
const out: SectionAnchor[] = []
|
||||
for (let i = 0; i < stopsAlongKm.length; i++) {
|
||||
out.push({ kind: 'stop', index: i, alongKm: stopsAlongKm[i] })
|
||||
if (i + 1 < stopsAlongKm.length) {
|
||||
out.push({ kind: 'leg', index: i, alongKm: (stopsAlongKm[i] + stopsAlongKm[i + 1]) / 2 })
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function insertIndexForAlong(stopsAlongKm: number[], hitAlongKm: number): number {
|
||||
if (stopsAlongKm.length < 2) return stopsAlongKm.length
|
||||
let i = 0
|
||||
|
||||
@@ -142,4 +142,33 @@ describe('useRoadtripCorridor', () => {
|
||||
// Both map renderers rebuild every marker when this array's identity changes.
|
||||
expect(result.current.visible).toBe(before)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-CORRIDORSTATE-020: narrowing to one point keeps only what is near it', () => {
|
||||
// A seven-hundred-kilometre day turns up hits along the whole of it. Picking the point
|
||||
// you mean to break at is a subtraction, because both figures are measured against
|
||||
// the same thinned line rather than being asked for separately.
|
||||
useCorridorPois.mockReturnValue(searchWith([
|
||||
poi({ osm_id: 'near', name: 'Near', alongKm: 30 }),
|
||||
poi({ osm_id: 'far', name: 'Far', alongKm: 600 }),
|
||||
]))
|
||||
const { result } = renderHook(() => useRoadtripCorridor(routes([day()])))
|
||||
|
||||
act(() => { result.current.setSectionKm(50) })
|
||||
act(() => { result.current.setSection({ dayId: 1, kind: 'stop', index: 0 }) })
|
||||
|
||||
// Stop 0 sits at the start of the drive, so only the hit within 50 km survives.
|
||||
expect(result.current.visible.map(p => p.osm_id)).toEqual(['near'])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-CORRIDORSTATE-021: an anchor belonging to another day narrows nothing', () => {
|
||||
// The section names a day as well as an index, so switching days cannot silently
|
||||
// apply yesterday's break to today's drive.
|
||||
const hits = [poi({ osm_id: 'a', name: 'A', alongKm: 10 }), poi({ osm_id: 'b', name: 'B', alongKm: 600 })]
|
||||
useCorridorPois.mockReturnValue(searchWith(hits))
|
||||
const { result } = renderHook(() => useRoadtripCorridor(routes([day()])))
|
||||
|
||||
act(() => { result.current.setSection({ dayId: 99, kind: 'stop', index: 0 }) })
|
||||
|
||||
expect(result.current.visible).toBe(hits)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCorridorPois, type CorridorPoi, type CorridorSearch } from './useCorridorPois'
|
||||
import { insertIndexForAlong } from './roadtripModel'
|
||||
import { sectionAnchors, insertIndexForAlong } from './roadtripModel'
|
||||
import type { SectionAnchor } from './roadtripModel'
|
||||
import { projectOntoRoute, type LatLng } from './corridor'
|
||||
import type { RoadtripDay, RoadtripRoutes } from './useRoadtripRoutes'
|
||||
|
||||
@@ -11,6 +12,9 @@ export { CORRIDOR_CATEGORY_KEYS } from './stopKinds'
|
||||
/** Corridor widths offered, in kilometres. */
|
||||
export const CORRIDOR_WIDTHS_KM = [2, 5, 10]
|
||||
|
||||
/** How far either side of a chosen point the list is narrowed to, in kilometres. */
|
||||
export const CORRIDOR_SECTION_KM = [25, 50, 100]
|
||||
|
||||
export interface RoadtripCorridor {
|
||||
/** Which day's drive is being searched. */
|
||||
dayId: string
|
||||
@@ -23,6 +27,14 @@ export interface RoadtripCorridor {
|
||||
search: CorridorSearch
|
||||
/** Narrows what was found by name or brand. Empty means everything. */
|
||||
nameFilter: string
|
||||
/** The stops and leg midpoints a break can be planned around. */
|
||||
anchors: SectionAnchor[]
|
||||
/** Which of them the list is narrowed to, or null for the whole day. */
|
||||
section: { dayId: number; kind: 'stop' | 'leg'; index: number } | null
|
||||
setSection: (value: { dayId: number; kind: 'stop' | 'leg'; index: number } | null) => void
|
||||
/** How far either side of it, in kilometres. */
|
||||
sectionKm: number
|
||||
setSectionKm: (value: number) => void
|
||||
/** Socket family the charging hits are narrowed to, or empty for any. */
|
||||
socketFilter: string
|
||||
setSocketFilter: (value: string) => void
|
||||
@@ -56,6 +68,17 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
|
||||
const [categories, setCategories] = useState<string[]>(['fuel'])
|
||||
const [widthKm, setWidthKm] = useState<number>(5)
|
||||
const [nameFilter, setNameFilter] = useState('')
|
||||
/**
|
||||
* Which point of the day the list is narrowed around, if any.
|
||||
*
|
||||
* Stored as the anchor's identity rather than as a distance, on purpose: `alongKm` is
|
||||
* measured against the thinned spine, whose length depends on the corridor width
|
||||
* (`useCorridorPois` thins with a tolerance derived from it). A kilometre figure kept
|
||||
* here would quietly shift when the width changes from 5 to 10; resolved through
|
||||
* `stopsAlongKm` it lands on the same stop again.
|
||||
*/
|
||||
const [section, setSection] = useState<{ dayId: number; kind: 'stop' | 'leg'; index: number } | null>(null)
|
||||
const [sectionKm, setSectionKm] = useState(50)
|
||||
/** A socket family, or empty for any. Only ever applied to charging hits. */
|
||||
const [socketFilter, setSocketFilter] = useState('')
|
||||
/** Minimum kW, or 0 for any. */
|
||||
@@ -92,37 +115,6 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
|
||||
wasSearching.current = searching
|
||||
}, [searching])
|
||||
|
||||
/**
|
||||
* Memoised on purpose, not filtered where it is drawn: both map renderers tear down and
|
||||
* rebuild every POI marker whenever the array's identity changes, so a `.filter()` in a
|
||||
* render body would make the pins flicker on each keystroke and on every unrelated
|
||||
* re-render.
|
||||
*
|
||||
* Brand counts as a name here because the server folds `operator` into `brand`, and
|
||||
* "Shell" is what someone types when the OSM name is "Shell Autohof Nord".
|
||||
*/
|
||||
const visible = useMemo(() => {
|
||||
const needle = nameFilter.trim().toLowerCase()
|
||||
// The same array when nothing narrows it, not a copy of it: the map redraws off this
|
||||
// reference, and handing it a fresh array on every render moves every pin.
|
||||
if (!needle && !socketFilter && !minKw) return search.results
|
||||
return search.results.filter(p => {
|
||||
if (needle && !(p.name.toLowerCase().includes(needle) || (p.brand ?? '').toLowerCase().includes(needle))) {
|
||||
return false
|
||||
}
|
||||
// The charging filters only ever hide charging hits. A rest area does not have a
|
||||
// socket and is not answering the question, so filtering the whole list by one
|
||||
// would empty it of everything the search also found.
|
||||
if (p.category !== 'charging') return true
|
||||
if (socketFilter && !p.charging?.sockets.some(s => s.type === socketFilter)) return false
|
||||
// A station that does not state its power is kept. Roughly two thirds of them do
|
||||
// not, and reading silence as "too slow" would throw away most of the map.
|
||||
if (minKw && p.charging?.sockets.some(s => s.kw != null) && !p.charging.sockets.some(s => (s.kw ?? 0) >= minKw)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [search.results, nameFilter, socketFilter, minKw])
|
||||
|
||||
/**
|
||||
* Where a hit belongs in the day's chain, as an index among its stops.
|
||||
@@ -142,6 +134,55 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
|
||||
})
|
||||
}, [day, search.spine])
|
||||
|
||||
/**
|
||||
* The stops and leg midpoints of the current day, and the distance of the chosen one.
|
||||
*
|
||||
* Resolved here rather than stored: `section` names an anchor, and its kilometre figure
|
||||
* is looked up fresh, so changing the corridor width does not move the point the list
|
||||
* is narrowed around.
|
||||
*/
|
||||
const anchors = useMemo(() => sectionAnchors(stopsAlongKm), [stopsAlongKm])
|
||||
const anchorKm = useMemo(() => {
|
||||
if (!section || !day || section.dayId !== day.dayId) return null
|
||||
return anchors.find(a => a.kind === section.kind && a.index === section.index)?.alongKm ?? null
|
||||
}, [section, day, anchors])
|
||||
|
||||
/**
|
||||
* Memoised on purpose, not filtered where it is drawn: both map renderers tear down and
|
||||
* rebuild every POI marker whenever the array's identity changes, so a `.filter()` in a
|
||||
* render body would make the pins flicker on each keystroke and on every unrelated
|
||||
* re-render.
|
||||
*
|
||||
* Brand counts as a name here because the server folds `operator` into `brand`, and
|
||||
* "Shell" is what someone types when the OSM name is "Shell Autohof Nord".
|
||||
*/
|
||||
const visible = useMemo(() => {
|
||||
const needle = nameFilter.trim().toLowerCase()
|
||||
// The same array when nothing narrows it, not a copy of it: the map redraws off this
|
||||
// reference, and handing it a fresh array on every render moves every pin.
|
||||
if (!needle && !socketFilter && !minKw && anchorKm === null) return search.results
|
||||
return search.results.filter(p => {
|
||||
// Around one point of the drive, when one has been picked. The two figures are
|
||||
// measured against the same thinned line, so this really is a subtraction and not
|
||||
// a second search.
|
||||
if (anchorKm !== null && Math.abs(p.alongKm - anchorKm) > sectionKm) return false
|
||||
if (needle && !(p.name.toLowerCase().includes(needle) || (p.brand ?? '').toLowerCase().includes(needle))) {
|
||||
return false
|
||||
}
|
||||
// The charging filters only ever hide charging hits. A rest area does not have a
|
||||
// socket and is not answering the question, so filtering the whole list by one
|
||||
// would empty it of everything the search also found.
|
||||
if (p.category !== 'charging') return true
|
||||
if (socketFilter && !p.charging?.sockets.some(s => s.type === socketFilter)) return false
|
||||
// A station that does not state its power is kept. Roughly two thirds of them do
|
||||
// not, and reading silence as "too slow" would throw away most of the map.
|
||||
if (minKw && p.charging?.sockets.some(s => s.kw != null) && !p.charging.sockets.some(s => (s.kw ?? 0) >= minKw)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [search.results, nameFilter, socketFilter, minKw, anchorKm, sectionKm])
|
||||
|
||||
const insertIndexFor = useCallback(
|
||||
(poi: CorridorPoi) => insertIndexForAlong(stopsAlongKm, poi.alongKm),
|
||||
[stopsAlongKm],
|
||||
@@ -157,6 +198,11 @@ export function useRoadtripCorridor(routes: RoadtripRoutes): RoadtripCorridor {
|
||||
setWidthKm,
|
||||
search,
|
||||
nameFilter,
|
||||
anchors,
|
||||
section,
|
||||
setSection,
|
||||
sectionKm,
|
||||
setSectionKm,
|
||||
socketFilter,
|
||||
setSocketFilter,
|
||||
minKw,
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'أي قابس',
|
||||
'roadtrip.poi.anyPower': 'أي قدرة',
|
||||
'roadtrip.poi.free': 'مجاني',
|
||||
'roadtrip.poi.wholeDay': 'اليوم كله',
|
||||
'roadtrip.poi.midLeg': 'في المنتصف، {from} إلى {to}',
|
||||
'roadtrip.poi.rest': 'استراحة',
|
||||
'roadtrip.poi.campsite': 'مخيم',
|
||||
'roadtrip.poi.food': 'طعام',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Qualquer tomada',
|
||||
'roadtrip.poi.anyPower': 'Qualquer potência',
|
||||
'roadtrip.poi.free': 'Gratuito',
|
||||
'roadtrip.poi.wholeDay': 'O dia todo',
|
||||
'roadtrip.poi.midLeg': 'Na metade, {from} ate {to}',
|
||||
'roadtrip.poi.rest': 'Área de descanso',
|
||||
'roadtrip.poi.campsite': 'Camping',
|
||||
'roadtrip.poi.food': 'Comida',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Qualsevol connector',
|
||||
'roadtrip.poi.anyPower': 'Qualsevol potència',
|
||||
'roadtrip.poi.free': 'Gratuït',
|
||||
'roadtrip.poi.wholeDay': 'Tot el dia',
|
||||
'roadtrip.poi.midLeg': 'A mig cami, {from} a {to}',
|
||||
'roadtrip.poi.rest': 'Àrea de descans',
|
||||
'roadtrip.poi.campsite': 'Càmping',
|
||||
'roadtrip.poi.food': 'Menjar',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Libovolná zásuvka',
|
||||
'roadtrip.poi.anyPower': 'Libovolný výkon',
|
||||
'roadtrip.poi.free': 'Zdarma',
|
||||
'roadtrip.poi.wholeDay': 'Cely den',
|
||||
'roadtrip.poi.midLeg': 'V polovine, {from} do {to}',
|
||||
'roadtrip.poi.rest': 'Odpočívadlo',
|
||||
'roadtrip.poi.campsite': 'Kemp',
|
||||
'roadtrip.poi.food': 'Jídlo',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Alle Stecker',
|
||||
'roadtrip.poi.anyPower': 'Alle Leistungen',
|
||||
'roadtrip.poi.free': 'Kostenlos',
|
||||
'roadtrip.poi.wholeDay': 'Ganzer Tag',
|
||||
'roadtrip.poi.midLeg': 'Auf halber Strecke, {from} bis {to}',
|
||||
'roadtrip.poi.rest': 'Rastplatz',
|
||||
'roadtrip.poi.campsite': 'Campingplatz',
|
||||
'roadtrip.poi.food': 'Essen',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Any plug',
|
||||
'roadtrip.poi.anyPower': 'Any power',
|
||||
'roadtrip.poi.free': 'Free',
|
||||
'roadtrip.poi.wholeDay': 'The whole day',
|
||||
'roadtrip.poi.midLeg': 'Halfway, {from} to {to}',
|
||||
'roadtrip.poi.rest': 'Rest area',
|
||||
'roadtrip.poi.campsite': 'Campsite',
|
||||
'roadtrip.poi.food': 'Food',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Cualquier conector',
|
||||
'roadtrip.poi.anyPower': 'Cualquier potencia',
|
||||
'roadtrip.poi.free': 'Gratis',
|
||||
'roadtrip.poi.wholeDay': 'Todo el dia',
|
||||
'roadtrip.poi.midLeg': 'A mitad, {from} a {to}',
|
||||
'roadtrip.poi.rest': 'Área de descanso',
|
||||
'roadtrip.poi.campsite': 'Camping',
|
||||
'roadtrip.poi.food': 'Comida',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Toute prise',
|
||||
'roadtrip.poi.anyPower': 'Toute puissance',
|
||||
'roadtrip.poi.free': 'Gratuit',
|
||||
'roadtrip.poi.wholeDay': 'Toute la journee',
|
||||
'roadtrip.poi.midLeg': 'A mi-chemin, {from} vers {to}',
|
||||
'roadtrip.poi.rest': 'Aire de repos',
|
||||
'roadtrip.poi.campsite': 'Camping',
|
||||
'roadtrip.poi.food': 'Restauration',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Κάθε βύσμα',
|
||||
'roadtrip.poi.anyPower': 'Κάθε ισχύς',
|
||||
'roadtrip.poi.free': 'Δωρεάν',
|
||||
'roadtrip.poi.wholeDay': 'Ολη η ημερα',
|
||||
'roadtrip.poi.midLeg': 'Στα μισα, {from} προς {to}',
|
||||
'roadtrip.poi.rest': 'Χώρος ανάπαυσης',
|
||||
'roadtrip.poi.campsite': 'Κάμπινγκ',
|
||||
'roadtrip.poi.food': 'Φαγητό',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Bármely csatlakozó',
|
||||
'roadtrip.poi.anyPower': 'Bármely teljesítmény',
|
||||
'roadtrip.poi.free': 'Ingyenes',
|
||||
'roadtrip.poi.wholeDay': 'A teljes nap',
|
||||
'roadtrip.poi.midLeg': 'Felúton, {from} és {to} kozott',
|
||||
'roadtrip.poi.rest': 'Pihenőhely',
|
||||
'roadtrip.poi.campsite': 'Kemping',
|
||||
'roadtrip.poi.food': 'Étkezés',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Semua colokan',
|
||||
'roadtrip.poi.anyPower': 'Semua daya',
|
||||
'roadtrip.poi.free': 'Gratis',
|
||||
'roadtrip.poi.wholeDay': 'Sepanjang hari',
|
||||
'roadtrip.poi.midLeg': 'Di tengah, {from} ke {to}',
|
||||
'roadtrip.poi.rest': 'Tempat istirahat',
|
||||
'roadtrip.poi.campsite': 'Perkemahan',
|
||||
'roadtrip.poi.food': 'Makanan',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Qualsiasi presa',
|
||||
'roadtrip.poi.anyPower': 'Qualsiasi potenza',
|
||||
'roadtrip.poi.free': 'Gratuito',
|
||||
'roadtrip.poi.wholeDay': 'Tutta la giornata',
|
||||
'roadtrip.poi.midLeg': 'A meta strada, {from} verso {to}',
|
||||
'roadtrip.poi.rest': 'Area di sosta',
|
||||
'roadtrip.poi.campsite': 'Campeggio',
|
||||
'roadtrip.poi.food': 'Ristoro',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'すべてのプラグ',
|
||||
'roadtrip.poi.anyPower': 'すべての出力',
|
||||
'roadtrip.poi.free': '無料',
|
||||
'roadtrip.poi.wholeDay': '一日全体',
|
||||
'roadtrip.poi.midLeg': '中間地点、{from}から{to}',
|
||||
'roadtrip.poi.rest': 'サービスエリア',
|
||||
'roadtrip.poi.campsite': 'キャンプ場',
|
||||
'roadtrip.poi.food': '食事',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': '모든 플러그',
|
||||
'roadtrip.poi.anyPower': '모든 출력',
|
||||
'roadtrip.poi.free': '무료',
|
||||
'roadtrip.poi.wholeDay': '하루 전체',
|
||||
'roadtrip.poi.midLeg': '중간 지점, {from}에서 {to}',
|
||||
'roadtrip.poi.rest': '휴게소',
|
||||
'roadtrip.poi.campsite': '캠핑장',
|
||||
'roadtrip.poi.food': '식사',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Elke stekker',
|
||||
'roadtrip.poi.anyPower': 'Elk vermogen',
|
||||
'roadtrip.poi.free': 'Gratis',
|
||||
'roadtrip.poi.wholeDay': 'De hele dag',
|
||||
'roadtrip.poi.midLeg': 'Halverwege, {from} naar {to}',
|
||||
'roadtrip.poi.rest': 'Rustplaats',
|
||||
'roadtrip.poi.campsite': 'Camping',
|
||||
'roadtrip.poi.food': 'Eten',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Dowolna wtyczka',
|
||||
'roadtrip.poi.anyPower': 'Dowolna moc',
|
||||
'roadtrip.poi.free': 'Bezpłatnie',
|
||||
'roadtrip.poi.wholeDay': 'Caly dzien',
|
||||
'roadtrip.poi.midLeg': 'W polowie, {from} do {to}',
|
||||
'roadtrip.poi.rest': 'Miejsce odpoczynku',
|
||||
'roadtrip.poi.campsite': 'Kemping',
|
||||
'roadtrip.poi.food': 'Jedzenie',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Любой разъём',
|
||||
'roadtrip.poi.anyPower': 'Любая мощность',
|
||||
'roadtrip.poi.free': 'Бесплатно',
|
||||
'roadtrip.poi.wholeDay': 'Весь день',
|
||||
'roadtrip.poi.midLeg': 'На полпути, {from} до {to}',
|
||||
'roadtrip.poi.rest': 'Зона отдыха',
|
||||
'roadtrip.poi.campsite': 'Кемпинг',
|
||||
'roadtrip.poi.food': 'Еда',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Alla kontakter',
|
||||
'roadtrip.poi.anyPower': 'Alla effekter',
|
||||
'roadtrip.poi.free': 'Gratis',
|
||||
'roadtrip.poi.wholeDay': 'Hela dagen',
|
||||
'roadtrip.poi.midLeg': 'Halvvags, {from} till {to}',
|
||||
'roadtrip.poi.rest': 'Rastplats',
|
||||
'roadtrip.poi.campsite': 'Campingplats',
|
||||
'roadtrip.poi.food': 'Mat',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Tum soketler',
|
||||
'roadtrip.poi.anyPower': 'Tum gucler',
|
||||
'roadtrip.poi.free': 'Ucretsiz',
|
||||
'roadtrip.poi.wholeDay': 'Tum gun',
|
||||
'roadtrip.poi.midLeg': 'Yolun yarisi, {from} - {to}',
|
||||
'roadtrip.poi.rest': 'Dinlenme alanı',
|
||||
'roadtrip.poi.campsite': 'Kamp alanı',
|
||||
'roadtrip.poi.food': 'Yemek',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Будь-який роз’єм',
|
||||
'roadtrip.poi.anyPower': 'Будь-яка потужність',
|
||||
'roadtrip.poi.free': 'Безкоштовно',
|
||||
'roadtrip.poi.wholeDay': 'Весь день',
|
||||
'roadtrip.poi.midLeg': 'На пiвдорозi, {from} до {to}',
|
||||
'roadtrip.poi.rest': 'Зона відпочинку',
|
||||
'roadtrip.poi.campsite': 'Кемпінг',
|
||||
'roadtrip.poi.food': 'Їжа',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': 'Mọi loại phích',
|
||||
'roadtrip.poi.anyPower': 'Mọi công suất',
|
||||
'roadtrip.poi.free': 'Miễn phí',
|
||||
'roadtrip.poi.wholeDay': 'Cả ngày',
|
||||
'roadtrip.poi.midLeg': 'Giữa đường, {from} đến {to}',
|
||||
'roadtrip.poi.rest': 'Trạm dừng nghỉ',
|
||||
'roadtrip.poi.campsite': 'Khu cắm trại',
|
||||
'roadtrip.poi.food': 'Đồ ăn',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': '任意接口',
|
||||
'roadtrip.poi.anyPower': '任意功率',
|
||||
'roadtrip.poi.free': '免費',
|
||||
'roadtrip.poi.wholeDay': '全天',
|
||||
'roadtrip.poi.midLeg': '中途,{from} 至 {to}',
|
||||
'roadtrip.poi.rest': '休息站',
|
||||
'roadtrip.poi.campsite': '露營地',
|
||||
'roadtrip.poi.food': '餐飲',
|
||||
|
||||
@@ -44,6 +44,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.poi.anySocket': '任意接口',
|
||||
'roadtrip.poi.anyPower': '任意功率',
|
||||
'roadtrip.poi.free': '免费',
|
||||
'roadtrip.poi.wholeDay': '全天',
|
||||
'roadtrip.poi.midLeg': '中途,{from} 至 {to}',
|
||||
'roadtrip.poi.rest': '服务区',
|
||||
'roadtrip.poi.campsite': '露营地',
|
||||
'roadtrip.poi.food': '餐饮',
|
||||
|
||||
Reference in New Issue
Block a user