mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(roadtrip): the hotel at the edge of a day stands, drives and reads right
A hotel row now takes its position and name from the trip's place instead of the stay row, which the planner fetches again only when a stay is edited. A moved pin no longer seats the hotel at its old spot while the server plans from the new one. The drive out of the morning hotel goes the way the first place is reached from it, the mode Days draws that leg in, and it is set in the shared rule so the browser and calculate_roadtrip agree. On the rail, a hotel row that took in the morning marker starts the line. A day that only drives from one stay to the next shows no stop count and takes a dropped stop as its first. The hotel keeps its pin on the desktop map, and the switch explains itself under its label and to screen readers. On the phone, the note about figures set on the desktop now sits above the switch it does not cover. The hint and the refusal on the hotel's drive say what they mean in all 23 languages. The via tools and the wiki say that a via into a booked night is kept but not used, the wiki states the overlap rule Days applies, and the arrived-from branch of the via anchor has tests.
This commit is contained in:
@@ -278,7 +278,7 @@ describe('RoadtripLimitsCard', () => {
|
||||
expect(onSave).toHaveBeenCalledWith('roadtrip_connect_days', false)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-LIMITS-018: starting and ending each day at the stay is off until it is asked for', async () => {
|
||||
it('FE-ROADTRIP-LIMITS-018: starting and ending each day at the stay is off until it is asked for', () => {
|
||||
const onSave = vi.fn()
|
||||
open(onSave)
|
||||
|
||||
@@ -288,11 +288,16 @@ describe('RoadtripLimitsCard', () => {
|
||||
fireEvent.click(toggle)
|
||||
expect(onSave).toHaveBeenCalledWith('roadtrip_hotel_bookends', true)
|
||||
expect(onSave).toHaveBeenCalledTimes(1)
|
||||
// What it does is said on the label, where the panel has no room for it.
|
||||
fireEvent.mouseEnter(screen.getByText('Start and end each day at your stay'))
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'After a booked night the day starts where you slept, and before one it ends there.',
|
||||
// What it does is said under it for everybody to read, and the switch is described by
|
||||
// that sentence, so a screen reader hears it with the switch rather than never.
|
||||
const hint = screen.getByText(
|
||||
'After a booked night the day starts at that stay, and before one it ends at the stay booked for that night.',
|
||||
)
|
||||
expect(toggle).toHaveAttribute('aria-describedby', hint.id)
|
||||
expect(hint.id).not.toBe('')
|
||||
expect(screen.queryByRole('tooltip')).toBeNull()
|
||||
// A switch that needs no sentence is described by none.
|
||||
expect(screen.getByRole('button', { name: 'Connect the days' })).not.toHaveAttribute('aria-describedby')
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-LIMITS-019: switched on, it reads on and reports the way back off', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRoadtripSettings } from '../../hooks/useRoadtripSettings'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useId, useState } from 'react'
|
||||
import {
|
||||
Clock, Fuel, CalendarClock, SlidersHorizontal, ChevronRight, Coins, Signpost, Ship, Check,
|
||||
Car, Zap, BatteryCharging, BatteryFull, BatteryWarning, Gauge, Route, Sparkles, Link2, Palette,
|
||||
@@ -18,7 +18,6 @@ import { FS } from './typeScale'
|
||||
import DayWindowFields from './DayWindowFields'
|
||||
import SettingsHint from './SettingsHint'
|
||||
import { dayWindow } from './dayWindow'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import { BOOKEND_ICON } from './nightBookend'
|
||||
import { useHotelBookends } from './useHotelBookends'
|
||||
|
||||
@@ -166,7 +165,9 @@ function LimitRow({ icon: Icon, label, suffix, value, placeholder, step, derived
|
||||
* pointed the instance at their own OSRM has no second engine, and a switch that flips
|
||||
* and changes nothing is worse than one that says why it cannot.
|
||||
*
|
||||
* A `hint` is the sentence a switch needs and its panel has no room for, on the label.
|
||||
* A `hint` is the sentence a switch needs to say what flipping it changes. It stands under
|
||||
* the label, in the caption the panels close on, and the switch is described by it: behind a
|
||||
* hover it was a sentence nobody found, and a screen reader never heard.
|
||||
*/
|
||||
function AvoidRow({ icon: Icon, label, hint, on, disabled, onToggle }: {
|
||||
icon: typeof Coins
|
||||
@@ -176,14 +177,17 @@ function AvoidRow({ icon: Icon, label, hint, on, disabled, onToggle }: {
|
||||
disabled: boolean
|
||||
onToggle: () => void
|
||||
}): React.ReactElement {
|
||||
const text = <span tabIndex={hint ? 0 : undefined} className="min-w-0 flex-1 text-body text-content-secondary">{label}</span>
|
||||
const hintId = useId()
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${disabled ? 'opacity-50' : ''}`}>
|
||||
<Icon size={16} className="shrink-0 text-content-faint" aria-hidden />
|
||||
{hint ? <Tooltip label={hint} placement="top">{text}</Tooltip> : text}
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="text-body text-content-secondary">{label}</span>
|
||||
{hint ? <span id={hintId} className="text-caption leading-snug text-content-faint">{hint}</span> : null}
|
||||
</span>
|
||||
{disabled
|
||||
? <span className="text-caption text-content-faint">{'—'}</span>
|
||||
: <ToggleSwitch on={on} onToggle={onToggle} label={label} />}
|
||||
: <ToggleSwitch on={on} onToggle={onToggle} label={label} describedBy={hint ? hintId : undefined} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1545,4 +1545,61 @@ describe('RoadtripSidebar with a booked night at the edge of the day', () => {
|
||||
const hotelRow = screen.getByText('Back to Hotel Alpenblick').closest('li')!
|
||||
expect(within(hotelRow).getByText('+30 min')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
/** Whether a row's disc has the rail running into it from above. */
|
||||
const lineAbove = (text: string): boolean => {
|
||||
const rail = screen.getByText(text).closest('button')!.firstElementChild!
|
||||
return rail.firstElementChild!.className.includes('w-[1.5px]')
|
||||
}
|
||||
|
||||
it('FE-ROADTRIP-SIDEBAR-071: the hotel row the morning marker went into starts the rail, with no line running in from nowhere', () => {
|
||||
const resume = stop({
|
||||
assignmentId: -2_000_000_003,
|
||||
name: 'Continue journey',
|
||||
lat: 47.2,
|
||||
lng: 11.4,
|
||||
automaticNight: { phase: 'start', fromDayNumber: 1 },
|
||||
})
|
||||
const stops = [resume, bookend('morning', { checkingOut: true }, { ownerIndex: 0 }), stop({ assignmentId: 1, name: 'Lookout', ownerIndex: 0 })]
|
||||
wrap(<RoadtripSidebar routes={routes({ days: [day({ stops, legs: [leg({ distance: 0 }), leg()] })] })} />)
|
||||
expect(lineAbove('Check-out · Hotel Alpenblick')).toBe(false)
|
||||
// Below it the rail runs on as always.
|
||||
expect(lineAbove('Lookout')).toBe(true)
|
||||
})
|
||||
|
||||
/** A transfer day: out of one stay and into the next, with nothing stored in between. */
|
||||
const transfer = () => day({
|
||||
dayId: 2,
|
||||
dayNumber: 2,
|
||||
stops: [
|
||||
bookend('morning', { checkingOut: true }, { ownerIndex: 0 }),
|
||||
bookend('evening', { accommodationId: 6, checkingIn: true }, { ownerIndex: 0, name: 'Wallinga', lat: 48, lng: 12 }),
|
||||
],
|
||||
legs: [leg()],
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-SIDEBAR-072: a day that only drives from one stay to the next says its drive and no count of stops', () => {
|
||||
wrap(<RoadtripSidebar routes={routes({ days: [transfer()] })} />)
|
||||
expect(screen.getByText('Check-in · Wallinga')).toBeInTheDocument()
|
||||
const header = screen.getByText('Day 2').closest('header')!
|
||||
expect(within(header).getByText(/100 km/)).toBeInTheDocument()
|
||||
expect(within(header).queryByText(/stops?$/)).toBeNull()
|
||||
expect(screen.queryByText('0 stops')).toBeNull()
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-SIDEBAR-073: a stop dragged onto such a day lands as its first stop, dropped on either hotel', () => {
|
||||
const onMoveStopToDay = vi.fn()
|
||||
const { container } = wrap(
|
||||
<RoadtripSidebar routes={routes({ days: [day(), transfer()] })} onReorderStop={vi.fn()} onMoveStopToDay={onMoveStopToDay} />,
|
||||
)
|
||||
const hotelRow = screen.getByText('Check-in · Wallinga').closest('li')!
|
||||
const rows = container.querySelectorAll('li[draggable="true"]')
|
||||
fireEvent.dragStart(rows[1], { dataTransfer: { effectAllowed: '', setData: vi.fn() } })
|
||||
fireEvent.dragOver(hotelRow)
|
||||
expect(hotelRow.className).toContain('ring-accent')
|
||||
fireEvent.drop(hotelRow)
|
||||
|
||||
// From day 1, its second stop (assignment 2), onto day 2 as its first.
|
||||
expect(onMoveStopToDay).toHaveBeenCalledWith(1, 2, 2, 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ import StopKindPicker from './StopKindPicker'
|
||||
import StopFillPicker from './StopFillPicker'
|
||||
import { useVehicleRange } from './useVehicleRange'
|
||||
import { MAX_TRIP_DAYS, type RoadtripStopType } from '@trek/shared'
|
||||
import { isCarrierMode, undatedRides, type CarrierTerminal } from '@trek/shared/roadtrip'
|
||||
import { isCarrierMode, isStoredStop, undatedRides, type CarrierTerminal } from '@trek/shared/roadtrip'
|
||||
import type { QuietDay, RoadtripDay, RoadtripRoutes, RoadtripStop } from './useRoadtripRoutes'
|
||||
import type { SpillMark } from './nightSpill'
|
||||
import { ARRIVING_DRIVE, openOn, type LegAlternatives, type RailDrive } from './useRouteAlternatives'
|
||||
@@ -1611,6 +1611,24 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onOpenBooking, ca
|
||||
// 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
|
||||
// A day of nothing but its hotels, the drive from one stay to the next. The hotels make it
|
||||
// a day, so it is no quiet row a stop can be dropped on, and it has no stored stop to drop
|
||||
// onto either: its hotel rows take the drop instead, as the day's first stop.
|
||||
const dropOnHotels: React.LiHTMLAttributes<HTMLLIElement> = onMoveStopToDay && from && !day.stops.some(isStoredStop)
|
||||
? {
|
||||
onDragOver: e => {
|
||||
e.preventDefault()
|
||||
if (dropAt?.dayId !== day.dayId || dropAt.index !== -1) setDropAt({ dayId: day.dayId, index: -1 })
|
||||
},
|
||||
onDrop: e => {
|
||||
e.preventDefault()
|
||||
setFrom(null)
|
||||
setDropAt(null)
|
||||
if (from.dayId !== day.dayId) onMoveStopToDay(from.dayId, from.assignmentId, day.dayId, 0)
|
||||
},
|
||||
className: `rounded-lg ${dropAt?.dayId === day.dayId && dropAt.index === -1 ? 'ring-2 ring-inset ring-accent' : ''}`,
|
||||
}
|
||||
: {}
|
||||
|
||||
/**
|
||||
* One row of the chain: the stop, the drive leaving it, whatever hangs off that drive.
|
||||
@@ -1672,14 +1690,15 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onOpenBooking, ca
|
||||
if (booking !== null && onOpenBooking) open = () => onOpenBooking(booking)
|
||||
else if (onSelectStop) open = () => onSelectStop(stop.placeId)
|
||||
return (
|
||||
<li key={stop.assignmentId}>
|
||||
<li key={stop.assignmentId} {...dropOnHotels}>
|
||||
<BookendStop
|
||||
reading={reading}
|
||||
entry={day.schedule.entries[i]}
|
||||
late={lateness}
|
||||
driveFindings={findingsFor(i)}
|
||||
continues={i < last}
|
||||
starts={i === 0}
|
||||
// The first row drawn, also when the morning marker before it went into it.
|
||||
starts={i === 0 || (i === 1 && resumeFoldsIntoBookend(day, 0))}
|
||||
onOpen={open}
|
||||
/>
|
||||
{i < last && (!day.stops[i + 1].automaticNight || day.legs[i]?.distance !== 0) ? <DriveBand leg={day.legs[i]} /> : null}
|
||||
@@ -1935,7 +1954,9 @@ function DaySection({ day, selectedAssignmentId, onSelectStop, onOpenBooking, ca
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{day.legs.length > 0 ? (
|
||||
{/* Nor does a day whose drive runs from one hotel to the next with no stop of its
|
||||
own: "0 stops" beside the drive reads as the stops having gone missing. */}
|
||||
{day.legs.length > 0 && destinationCount(day) > 0 ? (
|
||||
<span className={`${DAY_BADGE} bg-surface-card`} style={{ fontSize: FS.label }}>
|
||||
{t('roadtrip.day.stopCount', { count: destinationCount(day) })}
|
||||
</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BedDouble } from 'lucide-react'
|
||||
import { BOOKEND_ICON, bookendBooking, bookendMeta, bookendTitle } from './nightBookend'
|
||||
import { BOOKEND_ICON, bookendBooking, bookendMeta, bookendTitle, staysAtTheirPlaces } from './nightBookend'
|
||||
import type { BookendReading } from './roadtripRowModel'
|
||||
|
||||
// FE-BOOKEND-001 to FE-BOOKEND-005
|
||||
// FE-BOOKEND-001 to FE-BOOKEND-006
|
||||
|
||||
const reading = (over: Partial<BookendReading> = {}): BookendReading => ({
|
||||
phase: 'morning',
|
||||
@@ -50,3 +50,29 @@ describe('a booked night as the rail and the phone say it', () => {
|
||||
expect(BOOKEND_ICON).toBe(BedDouble)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the hotel a booked night stands for', () => {
|
||||
it('FE-BOOKEND-006: is read off the trip place as it is now, and the stay row stands in only where there is none', () => {
|
||||
const moved = { id: 1, place_id: 10, place_name: 'Old name', place_lat: 47.2, place_lng: 11.4 }
|
||||
const unknown = { id: 2, place_id: 20, place_name: 'Elsewhere', place_lat: 48.1, place_lng: 11.6 }
|
||||
const placeless = { id: 3, place_id: null, place_name: null, place_lat: null, place_lng: null }
|
||||
const places = [
|
||||
{ id: 10, name: 'Hotel Alpenblick', lat: 47.3, lng: 11.5 },
|
||||
{ id: 30, name: 'Unrelated', lat: 1, lng: 2 },
|
||||
]
|
||||
|
||||
const read = staysAtTheirPlaces([moved, unknown, placeless], places)
|
||||
expect(read[0]).toEqual({ id: 1, place_id: 10, place_name: 'Hotel Alpenblick', place_lat: 47.3, place_lng: 11.5 })
|
||||
expect(read[1]).toBe(unknown)
|
||||
expect(read[2]).toBe(placeless)
|
||||
|
||||
// A place that lost its pin takes the bookend with it, as the server's join does.
|
||||
expect(staysAtTheirPlaces([moved], [{ id: 10, name: 'Hotel Alpenblick', lat: null, lng: null }])[0])
|
||||
.toMatchObject({ place_lat: null, place_lng: null })
|
||||
|
||||
// Nothing to change hands the list back as it came, so nothing downstream recomputes.
|
||||
const current = [{ ...moved, place_name: 'Hotel Alpenblick', place_lat: 47.3, place_lng: 11.5 }]
|
||||
expect(staysAtTheirPlaces(current, places)).toBe(current)
|
||||
expect(staysAtTheirPlaces(current, [])).toBe(current)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { BookendReading } from './roadtripRowModel'
|
||||
* One module for both shells, the way `carrierRide` is for a ride: the two would otherwise
|
||||
* carry the same sentence and the same decision each, and the duplication budget does not
|
||||
* stretch to that. What the row IS comes from the row model (`bookendReading`); this only
|
||||
* says it.
|
||||
* says it, and reads the hotel it stands for off the trip's places (`staysAtTheirPlaces`).
|
||||
*/
|
||||
|
||||
/** The bed the day plan's stay chips wear, so the hotel looks like itself here too. */
|
||||
@@ -29,6 +29,49 @@ export function bookendMeta(reading: BookendReading, t: TranslationFn, is12h: bo
|
||||
return reading.until ? t('roadtrip.stay.until', { time: formatClockTime(reading.until, is12h) }) : null
|
||||
}
|
||||
|
||||
/** The fields of a trip place the hotel of a stay is read from. */
|
||||
export interface HotelPlace {
|
||||
id: number
|
||||
name: string
|
||||
lat?: number | null
|
||||
lng?: number | null
|
||||
}
|
||||
|
||||
/** The fields of a stay row that name its hotel and where it stands. */
|
||||
export interface StayAtPlace {
|
||||
place_id?: number | null
|
||||
place_name?: string | null
|
||||
place_lat?: number | null
|
||||
place_lng?: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The stays with their hotel where the trip's places have it now.
|
||||
*
|
||||
* A stay row carries its place's name and position as they were when the stays were last
|
||||
* fetched, and the planner fetches them again on a stay's own edit only. A pin moved on the
|
||||
* hotel reaches its visits at once (`mergeAssignmentPlace`) and the stay row not at all, so a
|
||||
* bookend seated from the row stood at the old spot beside its own stop at the new one: the
|
||||
* day drove from the one to the other, and the server, which joins the place afresh, planned
|
||||
* another drive. The place wins; the row stands in only for a place the list does not hold.
|
||||
* Rows that need nothing come back as they were, and so does the list when none does.
|
||||
*/
|
||||
export function staysAtTheirPlaces<S extends StayAtPlace>(stays: readonly S[], places: readonly HotelPlace[]): readonly S[] {
|
||||
if (!stays.length || !places.length) return stays
|
||||
const byId = new Map(places.map(place => [place.id, place]))
|
||||
let changed = false
|
||||
const out = stays.map(stay => {
|
||||
const place = stay.place_id == null ? undefined : byId.get(stay.place_id)
|
||||
if (!place) return stay
|
||||
const lat = place.lat ?? null
|
||||
const lng = place.lng ?? null
|
||||
if (stay.place_lat === lat && stay.place_lng === lng && stay.place_name === place.name) return stay
|
||||
changed = true
|
||||
return { ...stay, place_lat: lat, place_lng: lng, place_name: place.name }
|
||||
})
|
||||
return changed ? out : stays
|
||||
}
|
||||
|
||||
/**
|
||||
* The booking a tap on the row opens, or null when it opens the stay's place instead.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,7 @@ vi.mock('../Map/RouteCalculator', async importOriginal => ({
|
||||
|
||||
import { useRoadtripRoutes } from './useRoadtripRoutes'
|
||||
import { DEFAULT_SETTINGS, useSettingsStore } from '../../store/settingsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { lineMetres } from './corridor'
|
||||
|
||||
const HAMBURG: [number, number] = [53.5511, 9.9937]
|
||||
@@ -1165,7 +1166,7 @@ describe('a leg asked for again on demand', () => {
|
||||
})
|
||||
|
||||
/**
|
||||
* FE-ROADTRIP-ROUTES-052..059: a booked night at both ends of the days around it.
|
||||
* FE-ROADTRIP-ROUTES-052..061: a booked night at both ends of the days around it.
|
||||
*
|
||||
* The first cases here that hand the hook the trip's stays. What is pinned is what goes to
|
||||
* the router: the drive from the hotel slept in and to tonight's is part of the day's own
|
||||
@@ -1209,7 +1210,10 @@ describe('a booked night at both ends of its days', () => {
|
||||
calculateRouteWithLegs.mockImplementation(async (points: { lat: number; lng: number }[]) => hourly(points))
|
||||
switchTo(true)
|
||||
})
|
||||
afterEach(() => act(() => useSettingsStore.setState({ settings: { ...DEFAULT_SETTINGS } })))
|
||||
afterEach(() => act(() => {
|
||||
useSettingsStore.setState({ settings: { ...DEFAULT_SETTINGS } })
|
||||
useTripStore.setState({ places: [] })
|
||||
}))
|
||||
|
||||
/** A check-in with two places, then a transfer day that is only the next check-in. */
|
||||
const cam = () => ({
|
||||
@@ -1356,4 +1360,48 @@ describe('a booked night at both ends of its days', () => {
|
||||
await waitFor(() => expect(result.current.days[1].stops[0].bookend?.reservationId).toBe(90))
|
||||
expect(calculateRouteWithLegs).toHaveBeenCalledTimes(calls)
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-ROUTES-060: a hotel whose pin was moved stands where its place is now, not where the stay row last saw it', async () => {
|
||||
// The stay rows are fetched on a stay's own edit only, so after the pin moved they still
|
||||
// carry the old spot while the visit is already at the new one. Seated from the row,
|
||||
// the check-in day drove from the hotel's stop to its old spot and every morning set
|
||||
// off from there, while the server, which joins the place afresh, planned neither.
|
||||
const MOVED: [number, number] = [-33.8, 150.2]
|
||||
const days = [day(1, 1), day(2, 2), day(3, 3)]
|
||||
const assignments = {
|
||||
...map(1, [{ id: 1, at: MOVED }]),
|
||||
...map(2, [{ id: 2, at: LOOKOUT }]),
|
||||
} as AssignmentsMap
|
||||
const stays = [stayOf(1, 1, 1, 3, GETAWAY)]
|
||||
act(() => {
|
||||
useTripStore.setState({ places: [{ id: 10, trip_id: 7, name: 'Getaway Motel', lat: MOVED[0], lng: MOVED[1] }] as never })
|
||||
})
|
||||
const { result } = renderHook(() => useRoadtripRoutes(7, days, assignments, 'driving', {}, [], stays))
|
||||
await waitFor(() => expect(result.current.loading).toBe(false))
|
||||
|
||||
// The check-in day is its hotel alone, with no drive to a second copy of it.
|
||||
expect(result.current.days.map(d => shape(d.stops))).toEqual([
|
||||
['Stop 1'],
|
||||
['morning:1', 'Stop 2', 'evening:1'],
|
||||
])
|
||||
expect(result.current.days[1].stops[0]).toMatchObject({ lat: MOVED[0], lng: MOVED[1], name: 'Getaway Motel' })
|
||||
expect(asked()).toEqual([[MOVED[0], LOOKOUT[0], MOVED[0]]])
|
||||
})
|
||||
|
||||
it('FE-ROADTRIP-ROUTES-061: the drive out of the morning hotel goes the way the first place is reached from it', async () => {
|
||||
// The mode the day plan draws that leg in, and the one the server routes it in.
|
||||
const { days, stays } = simeon()
|
||||
const assignments = {
|
||||
...map(1, [{ id: 10, at: HOTEL }, { id: 11, at: P[0] }, { id: 12, at: P[1] }]),
|
||||
...map(2, [{ id: 13, at: P[2], incoming: 'walking' }, { id: 14, at: P[3] }]),
|
||||
...map(3, [{ id: 15, at: P[4] }, { id: 16, at: P[5] }]),
|
||||
} as AssignmentsMap
|
||||
const { result } = renderHook(() => useRoadtripRoutes(7, days, assignments, 'driving', {}, [], stays))
|
||||
await waitFor(() => expect(result.current.loading).toBe(false))
|
||||
|
||||
const runs = calculateRouteWithLegs.mock.calls.map(c => [c[1].profile, c[0].map((p: { lat: number }) => p.lat)])
|
||||
expect(runs).toContainEqual(['walking', [HOTEL[0], P[2][0]]])
|
||||
expect(runs).toContainEqual(['driving', [P[2][0], P[3][0], HOTEL[0]]])
|
||||
expect(runs).toContainEqual(['driving', [HOTEL[0], P[4][0], P[5][0], HOTEL[0]]])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,12 +6,14 @@ import { resolveLegMode } from '../Planner/legMode'
|
||||
import { splitIntoRuns, parseAvoid, type DriveLimits } from './roadtripModel'
|
||||
import { spillChains } from './nightSpill'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useVehicleRange } from './useVehicleRange'
|
||||
import type { Assignment, AssignmentsMap, Accommodation, Day, Reservation, RouteAvoidClass, SnappedWaypoint } from '../../types'
|
||||
import type { RoadtripVia, RoadtripDayBoundary } from '@trek/shared'
|
||||
import { dayWindow } from './dayWindow'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { stayStartingOn } from '../../utils/dayMerge'
|
||||
import { staysAtTheirPlaces } from './nightBookend'
|
||||
|
||||
export type { RoadtripStop, RoadtripDay, RoadtripRoutes, QuietDay, AccessSpur, RoutedLeg, SnappedPoint } from '@trek/shared/roadtrip'
|
||||
|
||||
@@ -333,11 +335,15 @@ export function useRoadtripRoutes(
|
||||
* Off until the trip switches it on, so a trip drives exactly as it did before.
|
||||
*/
|
||||
const bookendsOn = useRoadtripSettings(hotelBookendsOn, tripId)
|
||||
// The nights as the rule reads them, with the booking behind each. Read only while the
|
||||
// switch is on, so a new booking on a trip without it touches nothing downstream.
|
||||
// Where each hotel stands now. The stay rows keep the position their place had when they
|
||||
// were fetched, while the visits follow the place at once, and the server joins it afresh.
|
||||
const tripPlaces = useTripStore(s => s.places)
|
||||
// The nights as the rule reads them, with the booking behind each and the hotel where its
|
||||
// place is. Read only while the switch is on, so a new booking or a moved pin on a trip
|
||||
// without it touches nothing downstream.
|
||||
const stays = useMemo(
|
||||
() => (bookendsOn ? bookendStaysOf(accommodations, reservations) : []),
|
||||
[bookendsOn, accommodations, reservations],
|
||||
() => (bookendsOn ? bookendStaysOf(staysAtTheirPlaces(accommodations, tripPlaces), reservations) : []),
|
||||
[bookendsOn, accommodations, tripPlaces, reservations],
|
||||
)
|
||||
// The stored days with the night before and the night after seated at their edges, by
|
||||
// the rule the server's calculate_roadtrip runs too. Before a day is asked whether it
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ToggleSwitch({ on, onToggle, label }: { on: boolean; onToggle: () => void; label?: string }) {
|
||||
/**
|
||||
* `describedBy` names the element that explains the switch, for a switch whose label alone
|
||||
* does not say what flipping it changes.
|
||||
*/
|
||||
export default function ToggleSwitch({ on, onToggle, label, describedBy }: { on: boolean; onToggle: () => void; label?: string; describedBy?: string }) {
|
||||
return (
|
||||
<button type="button" onClick={onToggle} aria-pressed={on} aria-label={label}
|
||||
<button type="button" onClick={onToggle} aria-pressed={on} aria-label={label} aria-describedby={describedBy}
|
||||
style={{
|
||||
position: 'relative', width: 44, height: 24, minWidth: 44, flexShrink: 0,
|
||||
borderRadius: 12, border: 'none', padding: 0, cursor: 'pointer',
|
||||
|
||||
@@ -229,7 +229,9 @@ export default function MRoadtripTab({ planner, shell }: MTripTabPanelProps) {
|
||||
})
|
||||
: t('roadtrip.summary.partial')}
|
||||
</MBadge>
|
||||
<MBadge>{t('roadtrip.day.stopCount', { count: rt.stops })}</MBadge>
|
||||
{/* Not on a stage that only drives from one stay to the next, where "0 stops"
|
||||
beside the drive reads as the stops having gone missing. */}
|
||||
{rt.stops > 0 && <MBadge>{t('roadtrip.day.stopCount', { count: rt.stops })}</MBadge>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-end justify-between gap-3">
|
||||
|
||||
@@ -28,8 +28,8 @@ import type { DistanceUnit } from '../../../../types'
|
||||
* The figures cannot be changed here. That is the point rather than a gap: driving limits
|
||||
* are set once, at a table, before anybody is in the car, and a form in the passenger seat is
|
||||
* a way to break a trip while it is happening. So there is no field, no slider and no
|
||||
* greyed-out button that looks like one, and the last line says plainly where the settings
|
||||
* are and what else that screen can do that this one cannot.
|
||||
* greyed-out button that looks like one, and the line under the figures says plainly where
|
||||
* they are set and what else that screen can do that this one cannot.
|
||||
*
|
||||
* One switch is the exception: whether a day starts and ends at the stay. It is no figure and
|
||||
* breaks nothing. It moves where a drawn day begins and ends, never a stored stop, and
|
||||
@@ -37,7 +37,8 @@ import type { DistanceUnit } from '../../../../types'
|
||||
* on the road reaches for, when the day on the phone sets off from its first place instead
|
||||
* of the hotel they woke up in. It writes through the desktop dialog's own save
|
||||
* (`useHotelBookends` over the planner's `saveRoadtripLimit`), and stays disabled for a
|
||||
* reader who may not edit days.
|
||||
* reader who may not edit days. It sits below that line rather than above it, so the
|
||||
* sentence about figures set at the desktop never reads as being about the switch too.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -240,8 +241,13 @@ export default function MRtInfoSheet({ planner, shell }: MTripSheetsProps) {
|
||||
</InfoCard>
|
||||
</div>
|
||||
|
||||
{/* Under the figures it speaks of, and above the switch it does not. */}
|
||||
<p className="mt-2.5 font-geist text-[0.6875rem] leading-snug text-m-faint">
|
||||
{t('mobileTrip.rtDesktopNote')}
|
||||
</p>
|
||||
|
||||
{/* ── The one switch: whether a day starts and ends at the stay ── */}
|
||||
<div className="mt-2.5">
|
||||
<div className="mt-3">
|
||||
<InfoCard label={t('roadtrip.line.section')}>
|
||||
<div className="mt-1 flex items-center justify-between gap-3 py-2">
|
||||
<span className="min-w-0 flex-1 text-[0.8125rem] text-m-muted">{t('roadtrip.line.hotelBookends')}</span>
|
||||
@@ -257,10 +263,6 @@ export default function MRtInfoSheet({ planner, shell }: MTripSheetsProps) {
|
||||
</div>
|
||||
</InfoCard>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 font-geist text-[0.6875rem] leading-snug text-m-faint">
|
||||
{t('mobileTrip.rtDesktopNote')}
|
||||
</p>
|
||||
</div>
|
||||
</MSheet>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { roadtripPreferencesRepo } from '../../repo/roadtripPreferencesRepo'
|
||||
// FE-TP-ROAD-001 to FE-TP-ROAD-147
|
||||
// FE-TP-ROAD-001 to FE-TP-ROAD-150
|
||||
import React from 'react'
|
||||
import { renderHook, act, waitFor } from '@testing-library/react'
|
||||
import { TranslationProvider } from '../../i18n/TranslationContext'
|
||||
@@ -3093,7 +3093,7 @@ describe('useTripPlanner road trip: the drive between two days (#2461)', () => {
|
||||
})
|
||||
|
||||
describe('useTripPlanner road trip: a booked night at the edge of a day', () => {
|
||||
const NO_VIA = 'The drive from or to your stay keeps its road.'
|
||||
const NO_VIA = 'No via point on the drive to or from your stay. Add a stop there instead.'
|
||||
const bookend = (phase: 'morning' | 'evening', over: Record<string, unknown>) => ({
|
||||
assignmentId: phase === 'morning' ? -6_000_000_010 : -6_000_000_011,
|
||||
placeId: 900,
|
||||
@@ -3261,4 +3261,89 @@ describe('useTripPlanner road trip: a booked night at the edge of a day', () =>
|
||||
expect(toasts.filter(t => t.message.includes('changed'))).toEqual([])
|
||||
expect(rt.vias.addMany).toHaveBeenCalledWith(5, [], [0])
|
||||
})
|
||||
|
||||
const HAMBURG = { lat: 53.55, lng: 9.99 }
|
||||
const HOTEL = { lat: 53.0, lng: 11.5 }
|
||||
|
||||
it('FE-TP-ROAD-148: the drive into the hotel handed to the next card through the night takes no via, and a place there goes first on that card', async () => {
|
||||
// The hotel was reached after midnight: tonight's hotel row of day 5 heads card 6,
|
||||
// behind the stretch driven from Hamburg, and the morning's hotel row follows it. The
|
||||
// stretch is Hamburg's leg as far as a via is stored, and bending it would bend the
|
||||
// drive to the hotel, which keeps its road.
|
||||
seedTrip({ days: [buildDay({ id: 5, day_number: 1 }), buildDay({ id: 6, day_number: 2 })] })
|
||||
rt.corridor.day = { dayId: 6, dayNumber: 2 }
|
||||
rt.routes.days = [{
|
||||
dayId: 6,
|
||||
dayNumber: 2,
|
||||
stops: [
|
||||
bookend('evening', { ownerDayId: 5, ownerIndex: 2, ...HOTEL }),
|
||||
bookend('morning', { assignmentId: -6_000_000_012, ownerDayId: 6, ownerIndex: 0, ...HOTEL }),
|
||||
{ assignmentId: 21, placeId: 1201, ownerDayId: 6, ownerIndex: 0, lat: 51.05, lng: 13.74, name: 'Dresden' },
|
||||
],
|
||||
spills: [{ at: 0, count: 1, fromDayNumber: 1, fromStop: drawn(1101, HAMBURG.lat, HAMBURG.lng, 5, 1) }],
|
||||
geometry: [[HAMBURG.lat, HAMBURG.lng], [53.3, 10.7], [HOTEL.lat, HOTEL.lng], [52.0, 12.6], [51.05, 13.74]],
|
||||
}]
|
||||
const { result } = await renderRoadtrip()
|
||||
|
||||
await act(async () => { await result.current.addRoadtripVia(53.3, 10.7) })
|
||||
expect(rt.vias.add).not.toHaveBeenCalled()
|
||||
expect(toasts).toContainEqual({ message: NO_VIA, type: 'info' })
|
||||
|
||||
expect(result.current.manualStopTargetFor(53.3, 10.7)).toMatchObject({ dayId: 6, position: 0 })
|
||||
})
|
||||
|
||||
it('FE-TP-ROAD-149: nor does the drive in from yesterday that leaves from its hotel', async () => {
|
||||
// Connected days after a quiet check-out: the drive into card 6 starts at the hotel
|
||||
// day 5 ended at, drawn at the head of card 6. It files nothing, whatever the first
|
||||
// stop behind it is.
|
||||
seedTrip({ days: [buildDay({ id: 5, day_number: 1 }), buildDay({ id: 6, day_number: 2 })] })
|
||||
rt.corridor.day = { dayId: 6, dayNumber: 2 }
|
||||
rt.routes.days = [{
|
||||
dayId: 6,
|
||||
dayNumber: 2,
|
||||
stops: [
|
||||
{ assignmentId: 21, placeId: 1201, ownerDayId: 6, ownerIndex: 0, lat: 51.05, lng: 13.74, name: 'Dresden' },
|
||||
{ assignmentId: 22, placeId: 1202, ownerDayId: 6, ownerIndex: 1, lat: 50.08, lng: 14.43, name: 'Prague' },
|
||||
],
|
||||
spills: [],
|
||||
arrivingFrom: bookend('evening', { ownerDayId: 5, ownerIndex: 2, ...HOTEL }),
|
||||
geometry: [[HOTEL.lat, HOTEL.lng], [52.0, 12.6], [51.05, 13.74], [50.6, 14.1], [50.08, 14.43]],
|
||||
}]
|
||||
const { result } = await renderRoadtrip()
|
||||
|
||||
await act(async () => { await result.current.addRoadtripVia(52.0, 12.6) })
|
||||
expect(rt.vias.add).not.toHaveBeenCalled()
|
||||
expect(toasts.filter(t => t.message === NO_VIA)).toHaveLength(1)
|
||||
expect(result.current.manualStopTargetFor(52.0, 12.6)).toMatchObject({ dayId: 6, position: 0 })
|
||||
|
||||
// Past the first stop the card's own leg takes one as always.
|
||||
await act(async () => { await result.current.addRoadtripVia(50.6, 14.1) })
|
||||
expect(rt.vias.add).toHaveBeenCalledWith(6, 0, 50.6, 14.1)
|
||||
})
|
||||
|
||||
it('FE-TP-ROAD-150: the hotel a day starts or ends at has its pin on the map, also when no day holds its stop', async () => {
|
||||
seedTrip({
|
||||
days: [buildDay({ id: 5, day_number: 1 })],
|
||||
places: [buildPlace({ id: 900, ...HOTEL }), buildPlace({ id: 901, lat: 53, lng: 10 })],
|
||||
})
|
||||
const place = useTripStore.getState().places.find(p => p.id === 901)!
|
||||
useTripStore.setState({ assignments: { '5': [buildAssignment({ id: 11, day_id: 5, place_id: 901, place })] } })
|
||||
rt.corridor.day = { dayId: 5, dayNumber: 1 }
|
||||
rt.routes.days = [{
|
||||
dayId: 5,
|
||||
dayNumber: 1,
|
||||
stops: [
|
||||
{ assignmentId: 11, placeId: 901, ownerIndex: 0, lat: 53, lng: 10, name: 'Lookout' },
|
||||
bookend('evening', { ownerIndex: 1, ...HOTEL }),
|
||||
],
|
||||
geometry: [[53, 10], [HOTEL.lat, HOTEL.lng]],
|
||||
}]
|
||||
const { result } = await renderRoadtrip()
|
||||
|
||||
expect(result.current.roadtripMapPlaces.map(p => p.id).sort((a, b) => a - b)).toEqual([900, 901])
|
||||
|
||||
// Folded, the card takes its hotel off the map with its stops.
|
||||
act(() => { result.current.toggleRoadtripDay(5) })
|
||||
expect(result.current.roadtripMapPlaces.map(p => p.id)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1093,6 +1093,12 @@ export function useTripPlanner() {
|
||||
*/
|
||||
const roadtripMapPlaces = useMemo(() => {
|
||||
const plannedIds = new Set(Object.values(assignments).flat().map(a => a.place_id))
|
||||
// The hotel a day sets out from or ends at is drawn with its drive whether or not a day
|
||||
// still holds its stop: with that stop removed, the line and the walk to the door ended
|
||||
// at a spot with no pin, where the phone's stage map has one (`stagePlaceIds`).
|
||||
for (const day of roadtripRoutes.days) {
|
||||
for (const stop of day.stops) if (stop.bookend) plannedIds.add(stop.placeId)
|
||||
}
|
||||
const plannedPlaces = mapPlaces.filter(p => plannedIds.has(p.id))
|
||||
if (!collapsedRoadtripDays.size) return plannedPlaces
|
||||
const hidden = new Set<number>()
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { CarrierTerminal, RoadtripDay, RoadtripRoutes, RoadtripStop, RouteS
|
||||
import type { LegAlternatives } from '../../../../src/components/Roadtrip/useRouteAlternatives'
|
||||
import { openLeg } from '../../../helpers/legAlternatives'
|
||||
|
||||
// FE-MOB-RTTAB-001 to FE-MOB-RTTAB-064
|
||||
// FE-MOB-RTTAB-001 to FE-MOB-RTTAB-065
|
||||
//
|
||||
// The stage bar pictures the place its day ends at. It reads that place out of the trip
|
||||
// store rather than the planner, the unfiltered list, so the picture tests seed the store.
|
||||
@@ -1007,6 +1007,19 @@ describe('MRoadtripTab', () => {
|
||||
expect(reader.handlePlaceClick).toHaveBeenCalledWith(900)
|
||||
expect(shell.openSheet).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-MOB-RTTAB-065: a stage that only drives from one stay to the next says its drive and no count of stops', () => {
|
||||
const transfer = stage({
|
||||
stops: [bookend('morning', 0), { ...bookend('evening', 0), name: 'Wallinga', lat: 48, lng: 12 }],
|
||||
legs: [seg('172 km', '2 h')],
|
||||
schedule: { entries: [entry('08:00'), entry('10:00')], warnings: [] },
|
||||
legVias: [[]], driveWarnings: [], spills: [], dryPoints: [],
|
||||
})
|
||||
renderTab(planner({ roadtripRoutes: routes({ days: [transfer] }) }))
|
||||
|
||||
expect(screen.getByText('roadtrip.bookend.back:Wallinga')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/roadtrip\.day\.stopCount/)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildPlanner, buildShell } from '../../../helpers/mobileTrip'
|
||||
import { resetAllStores, seedStore } from '../../../helpers/store'
|
||||
import { fireEvent, render, screen } from '../../../helpers/render'
|
||||
|
||||
// FE-MOB-RTINFO-001 to FE-MOB-RTINFO-018
|
||||
// FE-MOB-RTINFO-001 to FE-MOB-RTINFO-019
|
||||
//
|
||||
// The sheet renders inside the real TranslationProvider, so the copy is asserted
|
||||
// in English. Everything it shows comes out of the roadtrip preferences store,
|
||||
@@ -154,7 +154,7 @@ describe('MRtInfoSheet', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('FE-MOB-RTINFO-013: closes with the footnote that names the desktop as the place to change them', () => {
|
||||
it('FE-MOB-RTINFO-013: carries the footnote that names the desktop as the place to change them', () => {
|
||||
renderSheet()
|
||||
expect(screen.getByText(/These figures are set on the desktop/)).toBeInTheDocument()
|
||||
})
|
||||
@@ -179,7 +179,9 @@ describe('MRtInfoSheet', () => {
|
||||
const toggle = screen.getByRole('switch', { name: 'Start and end each day at your stay' })
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false')
|
||||
expect(toggle).toBeEnabled()
|
||||
expect(screen.getByText('After a booked night the day starts where you slept, and before one it ends there.')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('After a booked night the day starts at that stay, and before one it ends at the stay booked for that night.'),
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(toggle)
|
||||
expect(saveRoadtripLimit).toHaveBeenCalledWith('roadtrip_hotel_bookends', true)
|
||||
expect(saveRoadtripLimit).toHaveBeenCalledTimes(1)
|
||||
@@ -194,6 +196,15 @@ describe('MRtInfoSheet', () => {
|
||||
expect(saveRoadtripLimit).toHaveBeenCalledWith('roadtrip_hotel_bookends', false)
|
||||
})
|
||||
|
||||
it('FE-MOB-RTINFO-019: the footnote about figures set on the desktop stands under the figures, above the switch that is set here', () => {
|
||||
renderSheet()
|
||||
const note = screen.getByText(/These figures are set on the desktop/)
|
||||
const limits = screen.getByText('Longest drive at once')
|
||||
const toggle = screen.getByRole('switch', { name: 'Start and end each day at your stay' })
|
||||
expect(limits.compareDocumentPosition(note) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(note.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('FE-MOB-RTINFO-018: a reader who may not edit days sees the switch, disabled', () => {
|
||||
// The planner hands out no save to a reader without day_edit, or before the trip's
|
||||
// settings are in, and the switch says so rather than flipping and springing back.
|
||||
|
||||
@@ -18,6 +18,15 @@ import { roadtripViaUpdateRequestSchema, type RoadtripViaUpdateRequest } from '@
|
||||
*/
|
||||
const roadtripAddonOn = addonGate(ADDON_IDS.ROADTRIP);
|
||||
|
||||
/**
|
||||
* What a via on the drive into a booked night's hotel does, said on both tools that place
|
||||
* one. The planner refuses the click there with a sentence (`roadtrip.bookend.noVia`); a
|
||||
* tool stores what it is given, so the assistant has to hear before it reports a detour
|
||||
* that the calculation will not take.
|
||||
*/
|
||||
const BOOKEND_VIA_NOTE =
|
||||
'With roadtrip_hotel_bookends on, a via after the last stop of a day that ends at a booked night is kept but not used: the drive to the hotel keeps its own road, so calculate_roadtrip returns the same route. Add a stop there instead.';
|
||||
|
||||
/**
|
||||
* Road-trip via points over MCP, the same surface the REST routes expose (#1797).
|
||||
*
|
||||
@@ -58,7 +67,7 @@ export class RoadtripMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'add_route_via',
|
||||
description: 'Make a day\'s drive pass through a point without stopping there — use it to send the route over a particular road or away from one. For somewhere the traveller actually stops, add a place and assign it to the day instead.',
|
||||
description: `Make a day's drive pass through a point without stopping there: use it to send the route over a particular road or away from one. For somewhere the traveller actually stops, add a place and assign it to the day instead. ${BOOKEND_VIA_NOTE}`,
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
@@ -85,7 +94,7 @@ export class RoadtripMcp {
|
||||
|
||||
@Tool({
|
||||
name: 'add_route_vias',
|
||||
description: 'Lay a whole chain of via points on one day at once, so the drive follows a particular road for a stretch rather than being nudged at a single point. Use this when the shape comes from a line — a recorded track, a signed scenic route — and add_route_via when it is one detour. Pass replace_legs to clear the vias on those legs first; leave it out to add to what is already there, which is what leaves hand-placed detours on other legs alone.',
|
||||
description: `Lay a whole chain of via points on one day at once, so the drive follows a particular road for a stretch rather than being nudged at a single point. Use this when the shape comes from a line (a recorded track, a signed scenic route) and add_route_via when it is one detour. Pass replace_legs to clear the vias on those legs first; leave it out to add to what is already there, which is what leaves hand-placed detours on other legs alone. ${BOOKEND_VIA_NOTE}`,
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
|
||||
@@ -356,6 +356,20 @@ describe('road-trip MCP tools', () => {
|
||||
expect(testDb.prepare('SELECT COUNT(*) c FROM roadtrip_preferences WHERE trip_id = ?').get(trip.id)).toEqual({ c: 0 });
|
||||
});
|
||||
|
||||
it('MCP-ROADTRIP-014: both via tools say that a via on the drive into a booked night is kept and not used', async () => {
|
||||
// The planner refuses that click with a sentence; a tool stores the via as asked, so
|
||||
// the note on the tool is the only place the assistant can learn it changes nothing.
|
||||
const { user } = createUser(testDb);
|
||||
await withHarness(user.id, async (h) => {
|
||||
const tools = (await h.client.listTools()).tools;
|
||||
for (const name of ['add_route_via', 'add_route_vias']) {
|
||||
const description = tools.find(t => t.name === name)?.description ?? '';
|
||||
expect(description, name).toContain('roadtrip_hotel_bookends');
|
||||
expect(description, name).toContain('kept but not used');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('MCP-ROADTRIP-009: removing one that is not on the day is refused, not silently ignored', async () => {
|
||||
const { user, trip, day } = scenario();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
function hourlyRouter() {
|
||||
return {
|
||||
profiles: () => ['driving'],
|
||||
route: vi.fn(async (_user: number, _trip: number, _day: number, points: { lat: number; lng: number }[]) => ({
|
||||
route: vi.fn(async (_user: number, _trip: number, _day: number, points: { lat: number; lng: number }[], _profile?: string) => ({
|
||||
parts: points.slice(1).map(() => ({ distance: 60000, duration: 3600 })),
|
||||
avoidMissed: [],
|
||||
leg: {
|
||||
@@ -512,6 +512,23 @@ describe('a booked night at both ends of its days', () => {
|
||||
expect(lats(router)[1]).toEqual([HOTEL.lat, 45.3, 45.4, HOTEL.lat]);
|
||||
});
|
||||
|
||||
it('drives out of the morning hotel the way the first place is reached from it, as the browser and the day plan do', async () => {
|
||||
const { user, trip: created, router, plans } = simeon();
|
||||
db.prepare(
|
||||
"UPDATE day_assignments SET incoming_leg_transport_mode = 'walking' WHERE place_id = (SELECT id FROM places WHERE trip_id = ? AND name = 'P3')",
|
||||
).run(created.id);
|
||||
|
||||
const { calculated } = await plans.calculate(created.id, user.id);
|
||||
|
||||
expect(calculated.days[1].stops[0]).toMatchObject({ legMode: 'walking', bookend: { phase: 'morning' } });
|
||||
const runs = router.route.mock.calls.map((call) => [call[4], call[3].map((p: { lat: number }) => p.lat)]);
|
||||
// The hotel's leg on foot on its own, the rest of the day in the day's own mode.
|
||||
expect(runs).toContainEqual(['walking', [HOTEL.lat, 45.3]]);
|
||||
expect(runs).toContainEqual(['driving', [45.3, 45.4, HOTEL.lat]]);
|
||||
// The other mornings are reached no particular way and stay in one run.
|
||||
expect(runs).toContainEqual(['driving', [HOTEL.lat, 45.5, 45.6, HOTEL.lat]]);
|
||||
});
|
||||
|
||||
it('drives the stored days while the trip has it off, and a preview can switch it either way', async () => {
|
||||
const off = simeon({});
|
||||
const stored = await off.plans.calculate(off.trip.id, off.user.id);
|
||||
|
||||
@@ -115,7 +115,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.dayColors': 'لون لكل يوم',
|
||||
'roadtrip.line.hint': 'يحسب أيضًا المسافة بين يومين ويحتسبها على يوم الوصول.',
|
||||
'roadtrip.line.hotelBookends': 'ابدأ كل يوم وأنهِه في مكان إقامتك',
|
||||
'roadtrip.line.hotelBookendsHint': 'بعد ليلة محجوزة يبدأ اليوم حيث نمت، وقبل ليلة محجوزة ينتهي هناك.',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'بعد ليلة محجوزة يبدأ اليوم من مكان الإقامة ذاك، وقبل ليلة محجوزة ينتهي في مكان الإقامة المحجوز لتلك الليلة.',
|
||||
'roadtrip.avoid.section': 'التجنب قدر الإمكان',
|
||||
'roadtrip.avoid.toll': 'الطرق ذات الرسوم',
|
||||
'roadtrip.avoid.motorway': 'الطرق السريعة',
|
||||
@@ -289,6 +290,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'من {name}',
|
||||
'roadtrip.bookend.back': 'العودة إلى {name}',
|
||||
'roadtrip.bookend.checkIn': 'تسجيل الوصول · {name}',
|
||||
'roadtrip.bookend.noVia': 'يبقى الطريق من مكان إقامتك وإليه كما هو.',
|
||||
'roadtrip.bookend.noVia': 'لا يمكن وضع نقطة عبور على الطريق من مكان إقامتك أو إليه. أضف توقفاً هناك بدلاً من ذلك.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Também calcula o trecho entre dois dias e conta no dia de chegada.',
|
||||
'roadtrip.line.hotelBookends': 'Começar e terminar cada dia na hospedagem',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Depois de uma noite reservada, o dia começa onde você dormiu, e antes de uma ele termina lá.',
|
||||
'Depois de uma noite reservada, o dia começa nessa hospedagem, e antes de uma ele termina na hospedagem reservada para aquela noite.',
|
||||
'roadtrip.avoid.section': 'Evitar quando possível',
|
||||
'roadtrip.avoid.toll': 'Pedágios',
|
||||
'roadtrip.avoid.motorway': 'Rodovias',
|
||||
@@ -296,6 +296,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Saindo de {name}',
|
||||
'roadtrip.bookend.back': 'Volta para {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'O trajeto de ida e volta da sua hospedagem mantém o próprio caminho.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Não dá para colocar um ponto de passagem no trajeto de ida ou volta da sua hospedagem. Adicione uma parada ali.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'També calcula el tram entre dos dies i el compta al dia d’arribada.',
|
||||
'roadtrip.line.hotelBookends': 'Comença i acaba cada dia a l’allotjament',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Després d’una nit reservada, el dia comença on has dormit, i abans d’una acaba allà.',
|
||||
'Després d’una nit reservada, el dia comença en aquest allotjament, i abans d’una acaba a l’allotjament reservat per a aquella nit.',
|
||||
'roadtrip.avoid.section': 'Evita quan sigui possible',
|
||||
'roadtrip.avoid.toll': 'Peatges',
|
||||
'roadtrip.avoid.motorway': 'Autopistes',
|
||||
@@ -299,6 +299,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Des de {name}',
|
||||
'roadtrip.bookend.back': 'Tornada a {name}',
|
||||
'roadtrip.bookend.checkIn': 'Entrada · {name}',
|
||||
'roadtrip.bookend.noVia': 'El trajecte des del teu allotjament o fins a ell manté la seva ruta.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'No es pot posar cap punt de pas al trajecte fins al teu allotjament o des d’ell. Afegeix-hi una parada.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -116,7 +116,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.dayColors': 'Barva na den',
|
||||
'roadtrip.line.hint': 'Spočítá i úsek mezi dvěma dny a připočte ho ke dni příjezdu.',
|
||||
'roadtrip.line.hotelBookends': 'Začínat a končit každý den v ubytování',
|
||||
'roadtrip.line.hotelBookendsHint': 'Po zarezervované noci začíná den tam, kde jste spali, a před ní tam končí.',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Po zarezervované noci začíná den v tomto ubytování a před ní končí v ubytování zarezervovaném na tu noc.',
|
||||
'roadtrip.avoid.section': 'Vyhnout se, pokud to jde',
|
||||
'roadtrip.avoid.toll': 'Mýtné silnice',
|
||||
'roadtrip.avoid.motorway': 'Dálnice',
|
||||
@@ -292,6 +293,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Z ubytování {name}',
|
||||
'roadtrip.bookend.back': 'Zpět do ubytování {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'Trasa z vašeho ubytování a do něj zůstává beze změny.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Na trasu do ubytování ani z něj nelze přidat průjezdní bod. Přidejte tam místo toho zastávku.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -116,7 +116,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Verbindungsfahrten zählen zum Ankunftstag.',
|
||||
'roadtrip.line.hotelBookends': 'Jeden Tag an der Unterkunft beginnen und beenden',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Nach einer gebuchten Nacht beginnt der Tag dort, wo du geschlafen hast, und vor einer endet er dort.',
|
||||
'Nach einer gebuchten Nacht beginnt der Tag an dieser Unterkunft, vor einer endet er an der Unterkunft dieser Nacht.',
|
||||
'roadtrip.avoid.section': 'Möglichst meiden',
|
||||
'roadtrip.avoid.toll': 'Mautstraßen',
|
||||
'roadtrip.avoid.motorway': 'Autobahnen',
|
||||
@@ -293,6 +293,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Ab {name}',
|
||||
'roadtrip.bookend.back': 'Zurück zu {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'Die Fahrt von und zu deiner Unterkunft behält ihre Strecke.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Auf der Fahrt zur oder von der Unterkunft geht kein Zwischenpunkt. Füge dort stattdessen einen Stopp hinzu.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -116,7 +116,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Connecting drives count toward the arrival day.',
|
||||
'roadtrip.line.hotelBookends': 'Start and end each day at your stay',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'After a booked night the day starts where you slept, and before one it ends there.',
|
||||
'After a booked night the day starts at that stay, and before one it ends at the stay booked for that night.',
|
||||
'roadtrip.avoid.section': 'Avoid where possible',
|
||||
'roadtrip.avoid.toll': 'Toll roads',
|
||||
'roadtrip.avoid.motorway': 'Motorways',
|
||||
@@ -290,6 +290,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'From {name}',
|
||||
'roadtrip.bookend.back': 'Back to {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'The drive from or to your stay keeps its road.',
|
||||
'roadtrip.bookend.noVia': 'No via point on the drive to or from your stay. Add a stop there instead.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'También calcula el tramo entre dos días y lo cuenta en el día de llegada.',
|
||||
'roadtrip.line.hotelBookends': 'Empezar y terminar cada día en tu alojamiento',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Tras una noche reservada, el día empieza donde dormiste, y antes de una termina allí.',
|
||||
'Tras una noche reservada, el día empieza en ese alojamiento, y antes de una termina en el alojamiento reservado para esa noche.',
|
||||
'roadtrip.avoid.section': 'Evitar si es posible',
|
||||
'roadtrip.avoid.toll': 'Peajes',
|
||||
'roadtrip.avoid.motorway': 'Autopistas',
|
||||
@@ -297,6 +297,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Desde {name}',
|
||||
'roadtrip.bookend.back': 'Vuelta a {name}',
|
||||
'roadtrip.bookend.checkIn': 'Entrada · {name}',
|
||||
'roadtrip.bookend.noVia': 'El trayecto desde tu alojamiento o hasta él mantiene su ruta.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'No se puede poner un punto de paso en el trayecto hacia tu alojamiento o desde él. Añade una parada allí.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -119,7 +119,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Calcule aussi le trajet entre deux jours et le compte au jour d’arrivée.',
|
||||
'roadtrip.line.hotelBookends': 'Commencer et finir chaque jour à votre hébergement',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Après une nuit réservée, la journée commence là où vous avez dormi, et avant une nuit réservée, elle s’y termine.',
|
||||
'Après une nuit réservée, la journée commence à cet hébergement, et avant une nuit réservée, elle se termine à l’hébergement réservé pour cette nuit.',
|
||||
'roadtrip.avoid.section': 'Éviter si possible',
|
||||
'roadtrip.avoid.toll': 'Routes à péage',
|
||||
'roadtrip.avoid.motorway': 'Autoroutes',
|
||||
@@ -299,6 +299,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Depuis {name}',
|
||||
'roadtrip.bookend.back': 'Retour à {name}',
|
||||
'roadtrip.bookend.checkIn': 'Arrivée · {name}',
|
||||
'roadtrip.bookend.noVia': 'Le trajet depuis ou vers votre hébergement garde son itinéraire.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Pas de point de passage sur le trajet vers ou depuis votre hébergement. Ajoutez plutôt un arrêt à cet endroit.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -120,7 +120,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Υπολογίζει και το τμήμα ανάμεσα σε δύο ημέρες, στην ημέρα άφιξης.',
|
||||
'roadtrip.line.hotelBookends': 'Έναρξη και λήξη κάθε ημέρας στο κατάλυμα',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Μετά από μια κρατημένη νύχτα η ημέρα ξεκινά εκεί όπου κοιμηθήκατε, και πριν από μία τελειώνει εκεί.',
|
||||
'Μετά από μια κρατημένη νύχτα η ημέρα ξεκινά από εκείνο το κατάλυμα, και πριν από μία τελειώνει στο κατάλυμα που έχει κρατηθεί για εκείνη τη νύχτα.',
|
||||
'roadtrip.avoid.section': 'Αποφυγή όπου γίνεται',
|
||||
'roadtrip.avoid.toll': 'Δρόμοι με διόδια',
|
||||
'roadtrip.avoid.motorway': 'Αυτοκινητόδρομοι',
|
||||
@@ -301,6 +301,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Από {name}',
|
||||
'roadtrip.bookend.back': 'Επιστροφή: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'Η διαδρομή προς και από το κατάλυμα διατηρεί τον δρόμο της.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Δεν μπαίνει ενδιάμεσο σημείο στη διαδρομή προς ή από το κατάλυμα. Προσθέστε εκεί μια στάση.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'A két nap közti szakaszt is kiszámolja, és az érkezés napjához adja.',
|
||||
'roadtrip.line.hotelBookends': 'Minden nap a szálláson kezdődjön és érjen véget',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Egy lefoglalt éjszaka után a nap ott kezdődik, ahol aludtál, egy lefoglalt éjszaka előtt pedig ott ér véget.',
|
||||
'Egy lefoglalt éjszaka után a nap annál a szállásnál kezdődik, egy lefoglalt éjszaka előtt pedig az arra az éjszakára foglalt szállásnál ér véget.',
|
||||
'roadtrip.avoid.section': 'Kerülés, ha lehet',
|
||||
'roadtrip.avoid.toll': 'Fizetős utak',
|
||||
'roadtrip.avoid.motorway': 'Autópályák',
|
||||
@@ -295,6 +295,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Indulás: {name}',
|
||||
'roadtrip.bookend.back': 'Vissza: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Bejelentkezés · {name}',
|
||||
'roadtrip.bookend.noVia': 'A szálláshoz vezető és onnan induló út útvonala nem változik.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'A szálláshoz vezető vagy onnan induló útra nem kerülhet köztes pont. Adj hozzá inkább egy megállót ott.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Juga menghitung ruas antara dua hari dan memasukkannya ke hari kedatangan.',
|
||||
'roadtrip.line.hotelBookends': 'Mulai dan akhiri setiap hari di penginapan Anda',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Setelah malam yang dipesan, hari dimulai di tempat Anda menginap, dan sebelum malam yang dipesan, hari berakhir di sana.',
|
||||
'Setelah malam yang dipesan, hari dimulai di penginapan itu, dan sebelum malam yang dipesan, hari berakhir di penginapan yang dipesan untuk malam itu.',
|
||||
'roadtrip.avoid.section': 'Hindari bila bisa',
|
||||
'roadtrip.avoid.toll': 'Jalan tol',
|
||||
'roadtrip.avoid.motorway': 'Jalan bebas hambatan',
|
||||
@@ -298,6 +298,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Dari {name}',
|
||||
'roadtrip.bookend.back': 'Kembali ke {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'Perjalanan dari dan ke penginapan Anda tetap pada jalurnya.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Titik lintas tidak bisa ditaruh di perjalanan ke atau dari penginapan Anda. Tambahkan perhentian di sana.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Calcola anche il tratto tra due giorni e lo conta nel giorno di arrivo.',
|
||||
'roadtrip.line.hotelBookends': 'Inizia e termina ogni giorno al tuo alloggio',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Dopo una notte prenotata la giornata parte da dove hai dormito, e prima di una finisce lì.',
|
||||
'Dopo una notte prenotata la giornata parte da quell’alloggio, e prima di una finisce all’alloggio prenotato per quella notte.',
|
||||
'roadtrip.avoid.section': 'Evita se possibile',
|
||||
'roadtrip.avoid.toll': 'Strade a pedaggio',
|
||||
'roadtrip.avoid.motorway': 'Autostrade',
|
||||
@@ -298,6 +298,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Da {name}',
|
||||
'roadtrip.bookend.back': 'Ritorno a {name}',
|
||||
'roadtrip.bookend.checkIn': 'Check-in · {name}',
|
||||
'roadtrip.bookend.noVia': 'Il tragitto da e verso il tuo alloggio mantiene il suo percorso.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Nessun punto di passaggio sul tragitto verso o dal tuo alloggio. Aggiungi invece una sosta in quel punto.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,8 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.dayColors': '日ごとに色を分ける',
|
||||
'roadtrip.line.hint': '日と日の間の区間も計算し、到着した日に加えます。',
|
||||
'roadtrip.line.hotelBookends': '毎日を宿泊先で始めて宿泊先で終える',
|
||||
'roadtrip.line.hotelBookendsHint': '予約した夜の翌日は泊まった宿から始まり、予約した夜の日はその宿で終わります。',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'予約した夜の翌日はその宿から始まり、予約した夜の日はその夜に予約した宿で終わります。',
|
||||
'roadtrip.avoid.section': 'できるだけ避ける',
|
||||
'roadtrip.avoid.toll': '有料道路',
|
||||
'roadtrip.avoid.motorway': '高速道路',
|
||||
@@ -291,6 +292,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': '{name}から',
|
||||
'roadtrip.bookend.back': '{name}へ戻る',
|
||||
'roadtrip.bookend.checkIn': 'チェックイン · {name}',
|
||||
'roadtrip.bookend.noVia': '宿泊先との間の区間は、今のルートのままになります。',
|
||||
'roadtrip.bookend.noVia': '宿泊先との間の区間には経由地を置けません。代わりにそこへ立ち寄り先を追加してください。',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': '두 날 사이 구간도 계산해 도착한 날에 더합니다.',
|
||||
'roadtrip.line.hotelBookends': '매일 숙소에서 시작하고 숙소에서 끝내기',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'예약한 밤 다음 날은 묵은 곳에서 시작하고, 예약한 밤이 있는 날은 그곳에서 끝납니다.',
|
||||
'예약한 밤 다음 날은 그 숙소에서 시작하고, 예약한 밤이 있는 날은 그날 밤 예약한 숙소에서 끝납니다.',
|
||||
'roadtrip.avoid.section': '가능하면 피하기',
|
||||
'roadtrip.avoid.toll': '유료 도로',
|
||||
'roadtrip.avoid.motorway': '고속도로',
|
||||
@@ -289,6 +289,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': '{name}에서 출발',
|
||||
'roadtrip.bookend.back': '{name}(으)로 돌아가기',
|
||||
'roadtrip.bookend.checkIn': '체크인 · {name}',
|
||||
'roadtrip.bookend.noVia': '숙소를 오가는 구간은 지금 경로를 그대로 유지합니다.',
|
||||
'roadtrip.bookend.noVia': '숙소를 오가는 구간에는 경유지를 둘 수 없습니다. 대신 그곳에 정차 지점을 추가하세요.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -119,7 +119,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Berekent ook het stuk tussen twee dagen en telt het bij de aankomstdag.',
|
||||
'roadtrip.line.hotelBookends': 'Elke dag bij je verblijf beginnen en eindigen',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Na een geboekte nacht begint de dag waar je sliep, en vóór een geboekte nacht eindigt hij daar.',
|
||||
'Na een geboekte nacht begint de dag bij dat verblijf, en vóór een geboekte nacht eindigt hij bij het verblijf dat voor die nacht is geboekt.',
|
||||
'roadtrip.avoid.section': 'Vermijden waar het kan',
|
||||
'roadtrip.avoid.toll': 'Tolwegen',
|
||||
'roadtrip.avoid.motorway': 'Snelwegen',
|
||||
@@ -297,6 +297,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Vanaf {name}',
|
||||
'roadtrip.bookend.back': 'Terug naar {name}',
|
||||
'roadtrip.bookend.checkIn': 'Inchecken · {name}',
|
||||
'roadtrip.bookend.noVia': 'De rit van en naar je verblijf houdt zijn route.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Geen tussenpunt op de rit naar of van je verblijf. Voeg daar in plaats daarvan een stop toe.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Wyznacza też odcinek między dniami i liczy go do dnia przyjazdu.',
|
||||
'roadtrip.line.hotelBookends': 'Zaczynaj i kończ każdy dzień w noclegu',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Po zarezerwowanej nocy dzień zaczyna się w miejscu noclegu, a przed taką nocą tam się kończy.',
|
||||
'Po zarezerwowanej nocy dzień zaczyna się w tym noclegu, a przed taką nocą kończy się w noclegu zarezerwowanym na tę noc.',
|
||||
'roadtrip.avoid.section': 'Omijaj, jeśli się da',
|
||||
'roadtrip.avoid.toll': 'Drogi płatne',
|
||||
'roadtrip.avoid.motorway': 'Autostrady',
|
||||
@@ -294,6 +294,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Wyjazd: {name}',
|
||||
'roadtrip.bookend.back': 'Powrót: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Zameldowanie · {name}',
|
||||
'roadtrip.bookend.noVia': 'Trasa do noclegu i z niego pozostaje bez zmian.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Na trasie do noclegu ani z niego nie można dodać punktu pośredniego. Dodaj tam zamiast tego postój.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Также считает участок между двумя днями и относит его ко дню прибытия.',
|
||||
'roadtrip.line.hotelBookends': 'Начинать и заканчивать каждый день в месте проживания',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'После забронированной ночи день начинается там, где вы ночевали, а перед ней заканчивается там.',
|
||||
'После забронированной ночи день начинается в этом жилье, а перед ней заканчивается в жилье, забронированном на эту ночь.',
|
||||
'roadtrip.avoid.section': 'Избегать по возможности',
|
||||
'roadtrip.avoid.toll': 'Платные дороги',
|
||||
'roadtrip.avoid.motorway': 'Автомагистрали',
|
||||
@@ -298,6 +298,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Отправление: {name}',
|
||||
'roadtrip.bookend.back': 'Возвращение: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Заезд · {name}',
|
||||
'roadtrip.bookend.noVia': 'Маршрут от места проживания и обратно к нему остаётся прежним.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'На маршрут до места проживания или от него нельзя поставить промежуточную точку. Добавьте там остановку.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -116,7 +116,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Beräknar även sträckan mellan två dagar och räknar den på ankomstdagen.',
|
||||
'roadtrip.line.hotelBookends': 'Börja och avsluta varje dag vid ditt boende',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Efter en bokad natt börjar dagen där du sov, och före en bokad natt slutar den där.',
|
||||
'Efter en bokad natt börjar dagen vid det boendet, och före en bokad natt slutar den vid boendet som är bokat för den natten.',
|
||||
'roadtrip.avoid.section': 'Undvik om det går',
|
||||
'roadtrip.avoid.toll': 'Avgiftsvägar',
|
||||
'roadtrip.avoid.motorway': 'Motorvägar',
|
||||
@@ -293,6 +293,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Från {name}',
|
||||
'roadtrip.bookend.back': 'Tillbaka till {name}',
|
||||
'roadtrip.bookend.checkIn': 'Incheckning · {name}',
|
||||
'roadtrip.bookend.noVia': 'Resan till och från ditt boende behåller sin väg.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Ingen mellanpunkt på resan till eller från ditt boende. Lägg till ett stopp där i stället.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -118,7 +118,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'İki gün arasındaki yolu da hesaplar ve varış gününe sayar.',
|
||||
'roadtrip.line.hotelBookends': 'Her güne konaklama yerinde başla ve orada bitir',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Rezerve edilmiş bir geceden sonra gün uyuduğunuz yerde başlar, böyle bir geceden önce de orada biter.',
|
||||
'Rezerve edilmiş bir geceden sonra gün o konaklama yerinde başlar, böyle bir geceden önce de o gece için ayrılan konaklama yerinde biter.',
|
||||
'roadtrip.avoid.section': 'Mümkünse kaçın',
|
||||
'roadtrip.avoid.toll': 'Paralı yollar',
|
||||
'roadtrip.avoid.motorway': 'Otoyollar',
|
||||
@@ -296,6 +296,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Başlangıç: {name}',
|
||||
'roadtrip.bookend.back': 'Dönüş: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Giriş · {name}',
|
||||
'roadtrip.bookend.noVia': 'Konaklama yerinize gidiş ve dönüş yolu değişmez.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Konaklama yerinize giden ya da oradan çıkan yola ara nokta konamaz. Bunun yerine oraya bir mola ekleyin.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Також рахує ділянку між двома днями і зараховує її дню прибуття.',
|
||||
'roadtrip.line.hotelBookends': 'Починати й завершувати кожен день у місці проживання',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Після заброньованої ночі день починається там, де ви ночували, а перед нею там і закінчується.',
|
||||
'Після заброньованої ночі день починається в цьому житлі, а перед нею закінчується в житлі, заброньованому на цю ніч.',
|
||||
'roadtrip.avoid.section': 'Уникати за можливості',
|
||||
'roadtrip.avoid.toll': 'Платні дороги',
|
||||
'roadtrip.avoid.motorway': 'Автомагістралі',
|
||||
@@ -295,6 +295,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Відправлення: {name}',
|
||||
'roadtrip.bookend.back': 'Повернення: {name}',
|
||||
'roadtrip.bookend.checkIn': 'Заїзд · {name}',
|
||||
'roadtrip.bookend.noVia': 'Маршрут від місця проживання й назад до нього лишається незмінним.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'На маршрут до місця проживання чи від нього не можна поставити проміжну точку. Додайте там зупинку.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -117,7 +117,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.hint': 'Cũng tính đoạn giữa hai ngày và cộng vào ngày tới nơi.',
|
||||
'roadtrip.line.hotelBookends': 'Bắt đầu và kết thúc mỗi ngày tại nơi lưu trú',
|
||||
'roadtrip.line.hotelBookendsHint':
|
||||
'Sau một đêm đã đặt, ngày bắt đầu ở nơi bạn ngủ, và trước một đêm đã đặt, ngày kết thúc ở đó.',
|
||||
'Sau một đêm đã đặt, ngày bắt đầu tại nơi lưu trú đó, và trước một đêm đã đặt, ngày kết thúc tại nơi lưu trú đã đặt cho đêm ấy.',
|
||||
'roadtrip.avoid.section': 'Tránh nếu có thể',
|
||||
'roadtrip.avoid.toll': 'Đường thu phí',
|
||||
'roadtrip.avoid.motorway': 'Đường cao tốc',
|
||||
@@ -294,6 +294,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': 'Từ {name}',
|
||||
'roadtrip.bookend.back': 'Về lại {name}',
|
||||
'roadtrip.bookend.checkIn': 'Nhận phòng · {name}',
|
||||
'roadtrip.bookend.noVia': 'Chặng đi và về nơi lưu trú giữ nguyên tuyến đường.',
|
||||
'roadtrip.bookend.noVia':
|
||||
'Không thể đặt điểm trung gian trên chặng đi hoặc về nơi lưu trú. Hãy thêm một điểm dừng ở đó.',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -110,7 +110,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.dayColors': '每天一種顏色',
|
||||
'roadtrip.line.hint': '也會規劃兩天之間的路段,並計入抵達的那一天。',
|
||||
'roadtrip.line.hotelBookends': '每天從住宿地點出發,並在住宿地點結束',
|
||||
'roadtrip.line.hotelBookendsHint': '預訂的一晚之後,當天從你住的地方出發;預訂的一晚之前,當天在那裡結束。',
|
||||
'roadtrip.line.hotelBookendsHint': '預訂的一晚之後,當天從那間住宿出發;預訂的一晚之前,當天在那晚預訂的住宿結束。',
|
||||
'roadtrip.avoid.section': '盡量避開',
|
||||
'roadtrip.avoid.toll': '收費公路',
|
||||
'roadtrip.avoid.motorway': '高速公路',
|
||||
@@ -278,6 +278,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': '從 {name} 出發',
|
||||
'roadtrip.bookend.back': '返回 {name}',
|
||||
'roadtrip.bookend.checkIn': '入住 · {name}',
|
||||
'roadtrip.bookend.noVia': '往返住宿地點的路段維持原本的路線。',
|
||||
'roadtrip.bookend.noVia': '往返住宿地點的路段無法加入途經點。請改在那裡新增一個停靠點。',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -110,7 +110,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.line.dayColors': '每天一种颜色',
|
||||
'roadtrip.line.hint': '也会规划两天之间的路段,并计入抵达的那一天。',
|
||||
'roadtrip.line.hotelBookends': '每天从住处出发,并在住处结束',
|
||||
'roadtrip.line.hotelBookendsHint': '预订的一晚之后,当天从你住的地方出发;预订的一晚之前,当天在那里结束。',
|
||||
'roadtrip.line.hotelBookendsHint': '预订的一晚之后,当天从那处住宿出发;预订的一晚之前,当天在那晚预订的住宿结束。',
|
||||
'roadtrip.avoid.section': '尽量避开',
|
||||
'roadtrip.avoid.toll': '收费公路',
|
||||
'roadtrip.avoid.motorway': '高速公路',
|
||||
@@ -279,6 +279,6 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.bookend.from': '从 {name} 出发',
|
||||
'roadtrip.bookend.back': '返回 {name}',
|
||||
'roadtrip.bookend.checkIn': '入住 · {name}',
|
||||
'roadtrip.bookend.noVia': '往返住处的路段保持原有路线。',
|
||||
'roadtrip.bookend.noVia': '往返住处的路段不能添加途经点。请改为在那里添加一个停靠点。',
|
||||
};
|
||||
export default roadtrip;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* ROADTRIP-BOOKENDS-001..027: a booked night stands at both ends of the days around it.
|
||||
* ROADTRIP-BOOKENDS-001..028: a booked night stands at both ends of the days around it.
|
||||
*
|
||||
* Pinned here: which hotel a day wakes up in and which it sleeps in, where the two are
|
||||
* seated and where they are not (the hotel already there, a landing, a departure, a hire
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type BookendStay,
|
||||
} from './nightBookends';
|
||||
import type { CarrierTerminal, PlanDay, RoadtripStop, RoutedLeg } from './planning-types';
|
||||
import { standsAsDay } from './roadtripModel';
|
||||
import { splitIntoRuns, standsAsDay } from './roadtripModel';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -325,7 +325,7 @@ describe('what a bookend is', () => {
|
||||
for (const id of ids) expect(id).toBeLessThan(terminalAssignmentId(1_000_000, 'arrival'));
|
||||
});
|
||||
|
||||
it('ROADTRIP-BOOKENDS-021: never a clock, a stay, a mode or a night of its own, whatever the stay says', () => {
|
||||
it('ROADTRIP-BOOKENDS-021: never a clock, a stay, a mode of its own or a night, whatever the stay says', () => {
|
||||
const [day] = seatNightBookends([planDay(D4, [visit(D4, 0), visit(D4, 1)])], days, [
|
||||
stay(5, D1, D4, HOTEL, { check_out: '11:00' }),
|
||||
]);
|
||||
@@ -506,3 +506,34 @@ describe('with the switch off', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the drive out of the morning hotel', () => {
|
||||
it('ROADTRIP-BOOKENDS-028: goes the way the first stop is reached from the hotel, the leg the day plan draws; the evening sets no mode', () => {
|
||||
const hotel = stay(5, D1, D4, HOTEL);
|
||||
const [day] = seatNightBookends(
|
||||
[
|
||||
planDay(D2, [
|
||||
visit(D2, 0, { name: 'P3', incomingLegMode: 'walking', legMode: 'cycling' }),
|
||||
visit(D2, 1, { name: 'P4' }),
|
||||
]),
|
||||
],
|
||||
days,
|
||||
[hotel],
|
||||
);
|
||||
expect(shape(day)).toEqual(['morning:5', 'P3', 'P4', 'evening:5']);
|
||||
expect(day!.stops[0]).toMatchObject({ legMode: 'walking', incomingLegMode: null });
|
||||
expect(day!.stops[3]).toMatchObject({ legMode: null, incomingLegMode: null });
|
||||
// Read the way the browser and the server both split a day: the hotel's leg on foot,
|
||||
// the first stop's own mode after it, and the day's default into tonight's hotel.
|
||||
const runs = splitIntoRuns(day!.stops, (from) => from.legMode ?? 'driving');
|
||||
expect(runs.map((run) => [run.mode, shape({ ...day!, stops: run.stops })])).toEqual([
|
||||
['walking', ['morning:5', 'P3']],
|
||||
['cycling', ['P3', 'P4']],
|
||||
['driving', ['P4', 'evening:5']],
|
||||
]);
|
||||
|
||||
// A first stop reached no particular way leaves the drive to the day's own mode.
|
||||
const [plain] = seatNightBookends([planDay(D3, [visit(D3, 0), visit(D3, 1)])], days, [hotel]);
|
||||
expect(plain!.stops[0]!.legMode).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +104,19 @@ export function bookendStaysOf<S extends Omit<BookendStay, 'reservation_id'>>(
|
||||
return out;
|
||||
}
|
||||
|
||||
function bookendStop(day: PlanDay, phase: BookendPhase, stay: BookendStay, ownerIndex: number): RoadtripStop {
|
||||
/**
|
||||
* A bookend as a stop. `legMode` is the mode of the drive that leaves it: the morning's
|
||||
* drive to the first stop goes the way that stop is reached from the hotel
|
||||
* (`incoming_leg_transport_mode`, the mode the day plan draws that leg in), and the
|
||||
* evening's leaves nowhere.
|
||||
*/
|
||||
function bookendStop(
|
||||
day: PlanDay,
|
||||
phase: BookendPhase,
|
||||
stay: BookendStay,
|
||||
ownerIndex: number,
|
||||
legMode: string | null,
|
||||
): RoadtripStop {
|
||||
const checkingOut = phase === 'morning' && stay.end_day_id === day.dayId;
|
||||
return {
|
||||
bookend: {
|
||||
@@ -131,7 +143,7 @@ function bookendStop(day: PlanDay, phase: BookendPhase, stay: BookendStay, owner
|
||||
dwellMinutes: 0,
|
||||
night: false,
|
||||
endDay: false,
|
||||
legMode: null,
|
||||
legMode,
|
||||
incomingLegMode: null,
|
||||
stopType: 'hotel',
|
||||
fillPercent: null,
|
||||
@@ -193,9 +205,9 @@ export function seatNightBookends(plan: PlanDay[], days: readonly DayRef[], stay
|
||||
return {
|
||||
...day,
|
||||
stops: [
|
||||
...(head ? [bookendStop(day, 'morning', hotels.morning!, 0)] : []),
|
||||
...(head ? [bookendStop(day, 'morning', hotels.morning!, 0, first?.incomingLegMode ?? null)] : []),
|
||||
...stops,
|
||||
...(tail ? [bookendStop(day, 'evening', hotels.evening!, stored)] : []),
|
||||
...(tail ? [bookendStop(day, 'evening', hotels.evening!, stored, null)] : []),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -367,16 +367,6 @@ export function computeSchedule(
|
||||
return { entries, warnings, endsAt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rail lists this day among the days, rather than as a quiet placeholder
|
||||
* asking for stops.
|
||||
*
|
||||
* Two stops make a drive. A booked night makes a day on its own: the day you arrive
|
||||
* somewhere and go no further, or the one you check in at ten and set out from. It
|
||||
* used to need company, and a hotel entered under Days for the arrival day was filed
|
||||
* at the bottom of the rail under "only X so far", which reads as the hotel missing
|
||||
* from the road trip altogether.
|
||||
*/
|
||||
/**
|
||||
* Whether a stop is one somebody stored on the day: an assignment, filed at its own
|
||||
* index. Not an automatic night, a terminal or a booked night at a day's edge, which the
|
||||
@@ -391,6 +381,16 @@ export function isStoredStop(stop: Pick<RoadtripStop, 'automaticNight' | 'carrie
|
||||
return !stop.automaticNight && !stop.carrier && !stop.bookend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rail lists this day among the days, rather than as a quiet placeholder
|
||||
* asking for stops.
|
||||
*
|
||||
* Two stops make a drive. A booked night makes a day on its own: the day you arrive
|
||||
* somewhere and go no further, or the one you check in at ten and set out from. It
|
||||
* used to need company, and a hotel entered under Days for the arrival day was filed
|
||||
* at the bottom of the rail under "only X so far", which reads as the hotel missing
|
||||
* from the road trip altogether.
|
||||
*/
|
||||
export function standsAsDay(stops: readonly { night?: boolean }[]): boolean {
|
||||
return stops.length > 1 || stops.some((stop) => stop.night === true);
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ These tools work without an open browser. The external assistant chooses places
|
||||
| `search_roadtrip_corridor` | Fuel, charging, rest areas, campsites, food, sights or hotels along a day | `trips:read` |
|
||||
| `update_route_via` | Move an existing routing handle and optionally change its outgoing leg | `trips:write` |
|
||||
| `list_route_vias` | A day's via points, or the whole trip's together with its followed tracks | `trips:read` |
|
||||
| `add_route_via`, `add_route_vias` | Add one via point, or a whole chain on one day | `trips:write` |
|
||||
| `add_route_via`, `add_route_vias` | Add one via point, or a whole chain on one day. With `roadtrip_hotel_bookends` on, a via after the last stop of a day that ends at a booked night is kept but not used, because the drive to the hotel keeps its own road | `trips:write` |
|
||||
| `reanchor_route_vias` | Re-pin a day's via points after its stops changed | `trips:write` |
|
||||
| `remove_route_via` | Remove a via point so the leg drives direct again | `trips:write` |
|
||||
| `list_day_boundaries`, `set_day_boundary` | Read or set dragged day endings; null restores the automatic ending | `trips:read` / `trips:write` |
|
||||
|
||||
+3
-3
@@ -296,10 +296,10 @@ With it on:
|
||||
- The hotel stands at the edge of the day card as a row of its own, with a bed icon and no number. In the morning it reads *Check-out · Hotel Adler* with *until 10:00* on the day you leave, and *From Hotel Adler* on a day you come back to. In the evening it reads *Back to Hotel Adler*, or *Check-in · Hotel Adler* on the day the stay begins when the hotel's own stop is not on the card before it, for example because you removed it.
|
||||
- The drive to and from the hotel counts towards the day's distance, driving time, driving limits and range. The hotel does not count as a stop and does not refill the tank.
|
||||
- These rows are not stops you planned, and nothing is stored for them: they cannot be dragged, removed or given a stay. Clicking one opens the night's booking, or the hotel's place when there is no booking to open. On the phone, a night without a booking opens the stay for somebody who may edit days.
|
||||
- Only real nights count: a stay that checks in and out on the same day adds no row, and neither does a hotel without coordinates. Where two stays overlap, the one entered first counts.
|
||||
- Only real nights count: a stay that checks in and out on the same day adds no row, and neither does a hotel without coordinates. Where two stays overlap, the choice is the one **Days** makes: the day ends at a stay checked in that day, otherwise at the one it started from, and it starts at a stay checked in on an earlier day, the one entered first where there are two. A drive between two different hotels is then a real drive.
|
||||
- A side of the day that already starts or ends at the hotel gets no row. Neither does a day that starts where a flight, train or ferry lands or at a hire car's pick-up desk, or ends at a departure terminal or where the hire car goes back.
|
||||
- A day with one place becomes a drive from the hotel to it and back. A day between two nights in the same hotel with no place planned has no drive, and a check-out day with no places is the drive from one hotel to the next.
|
||||
- The drive from and to the hotel keeps its road. Clicking it on the map places no via point and says *The drive from or to your stay keeps its road.*, and its drive band offers no **Other ways**. A place added from the search along that drive goes to the start or the end of the day.
|
||||
- The drive from and to the hotel keeps its road. Clicking it on the map places no via point and says *No via point on the drive to or from your stay. Add a stop there instead.*, and its drive band offers no **Other ways**. A place added from the search along that drive goes to the start or the end of the day.
|
||||
- With **Connect the days** on or daily travel times set, a night at one hotel is no drive between the two days. A via point on the drive from one day into the next is kept, but not used while that day ends at a hotel.
|
||||
|
||||
Switch it off and every day drives from its first stop to its last, as before. The **Days** view does not change either way.
|
||||
@@ -409,7 +409,7 @@ What TREK contacts, and from where:
|
||||
| *Use a Google Maps directions link.* | The link is not a Google Maps directions link, for example a single place. |
|
||||
| *The link must contain between 2 and 30 readable stops.* | Too few or too many stops in the link. Split a longer route into days. |
|
||||
| Via handles do not appear or do not move | Zoom in further. Otherwise you are offline, or you may not edit days on this trip. |
|
||||
| *The drive from or to your stay keeps its road.* | You clicked the drive to or from a hotel row, which takes no via point. Add a stop there instead, or switch off **Start and end each day at your stay**. |
|
||||
| *No via point on the drive to or from your stay. Add a stop there instead.* | You clicked the drive to or from a hotel row, which takes no via point. Add a stop there instead, or switch off **Start and end each day at your stay**. |
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
Reference in New Issue
Block a user