mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
fix(tours): keep a city walk on foot across the water, mark a crossing by boat apart from the walking time and save it only once taken
This commit is contained in:
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { server } from '../../../tests/helpers/msw/server'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { valhallaBase, valhallaAvailable, valhallaTurn, resetValhallaTurns, valhallaRouteAvoiding, valhallaRun, valhallaAlternates, alternatesFrom, runFrom, legAvoids, valhallaTraceAttributes } from './valhallaRoute'
|
||||
import { valhallaBase, valhallaAvailable, valhallaTurn, resetValhallaTurns, valhallaRouteAvoiding, valhallaRun, valhallaRunOnFoot, valhallaAlternates, alternatesFrom, runFrom, legAvoids, valhallaTraceAttributes } from './valhallaRoute'
|
||||
|
||||
const FOSSGIS_VALHALLA = 'https://valhalla1.openstreetmap.de/route'
|
||||
|
||||
@@ -630,3 +630,61 @@ describe('valhallaTraceAttributes (BIK-01)', () => {
|
||||
expect(asked).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('crossings by boat on a walk (CW-12)', () => {
|
||||
/** A walk's leg as the public instance answers one: the boat boards at point 1 and lands at point 2. */
|
||||
const ferryLeg = (maneuvers?: unknown[]) => ({
|
||||
shape: SHAPE,
|
||||
summary: { length: 1.5, time: 900, has_ferry: true },
|
||||
...(maneuvers ? { maneuvers } : {}),
|
||||
})
|
||||
// The San Tomà to Ca' Rezzonico crossing as measured on Venice, laid onto the test shape.
|
||||
const boarding = [
|
||||
{ type: 1, begin_shape_index: 0, end_shape_index: 1, time: 100, length: 0.2 },
|
||||
{ type: 28, ferry: true, begin_shape_index: 1, end_shape_index: 2, time: 744.959, length: 1.236 },
|
||||
]
|
||||
const walked = { shape: SHAPE, summary: { length: 1, time: 700, has_ferry: false } }
|
||||
|
||||
it('VALHALLA-FERRY-001: reads where a leg goes by boat off its maneuvers, with the crossing\'s own length and time', () => {
|
||||
const run = runFrom({ trip: { legs: [ferryLeg(boarding)] } })
|
||||
expect(run?.legs[0].ferrySpans).toEqual([{ from: 1, to: 2, metres: 1236, seconds: 744.959 }])
|
||||
})
|
||||
|
||||
it('VALHALLA-FERRY-002: the joined line keeps each crossing on its own points', () => {
|
||||
const run = runFrom({ trip: { legs: [walked, ferryLeg(boarding)] } })
|
||||
// The second leg does not start where the first ends, so its three points follow the first three.
|
||||
expect(run?.total.coordinates).toHaveLength(6)
|
||||
expect(run?.total.ferrySpans).toEqual([{ from: 4, to: 5, metres: 1236, seconds: 744.959 }])
|
||||
expect(run?.legs[0]).not.toHaveProperty('ferrySpans')
|
||||
})
|
||||
|
||||
it('VALHALLA-FERRY-003: a ferry the maneuvers do not place takes the whole leg, its time unknown', () => {
|
||||
const run = runFrom({ trip: { legs: [ferryLeg()] } })
|
||||
expect(run?.legs[0].ferrySpans).toEqual([{ from: 0, to: 2, metres: 0, seconds: null }])
|
||||
})
|
||||
|
||||
it('VALHALLA-FERRY-004: crossings that touch are one, and a crossing with no time leaves the sum unknown', () => {
|
||||
const run = runFrom({
|
||||
trip: {
|
||||
legs: [ferryLeg([
|
||||
{ type: 28, ferry: true, begin_shape_index: 0, end_shape_index: 1, time: 60, length: 0.1 },
|
||||
{ type: 28, ferry: true, begin_shape_index: 1, end_shape_index: 2, length: 0.2 },
|
||||
])],
|
||||
},
|
||||
})
|
||||
expect(run?.legs[0].ferrySpans).toEqual([{ from: 0, to: 2, metres: 300, seconds: null }])
|
||||
})
|
||||
|
||||
it('VALHALLA-FERRY-005: binds no point to a boat line when asked, and only then', async () => {
|
||||
const bodies: Array<{ locations: Array<Record<string, unknown>> }> = []
|
||||
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
||||
bodies.push(await request.json() as { locations: Array<Record<string, unknown>> })
|
||||
return HttpResponse.json({ trip: { legs: [walked] } })
|
||||
}))
|
||||
const points = [{ lat: 45.4342798, lng: 12.338111 }, { lat: 45.4367609, lng: 12.3287531 }]
|
||||
await valhallaRunOnFoot(points, undefined, { use_ferry: 0 }, { exclude_ferry: true })
|
||||
await valhallaRun(points, 'walking', [], undefined, { use_ferry: 0 })
|
||||
expect(bodies[0].locations.map(location => location.search_filter)).toEqual([{ exclude_ferry: true }, { exclude_ferry: true }])
|
||||
expect(bodies[1].locations.every(location => !('search_filter' in location))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -100,6 +100,27 @@ const CHUNK_PAUSE_MS = 1100
|
||||
*/
|
||||
const CHUNK_RETRIES = 1
|
||||
|
||||
/**
|
||||
* A stretch of a leg the router takes by boat, by the indices of the leg's
|
||||
* line where the boat leaves and lands, with the crossing's own length in
|
||||
* metres and its time in seconds (null where the router gave none).
|
||||
*/
|
||||
export interface FerrySpan {
|
||||
from: number
|
||||
to: number
|
||||
metres: number
|
||||
seconds: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* What the router may bind a point to. `exclude_ferry` keeps a point off a
|
||||
* ferry's edge: a stop beside a pier, bound onto the boat line, would make the
|
||||
* route ride the boat to its next landing however the costing weighs ferries.
|
||||
*/
|
||||
export interface SearchFilter {
|
||||
exclude_ferry?: boolean
|
||||
}
|
||||
|
||||
/** One way of driving a leg, in the units OSRM answers in so callers need no branch. */
|
||||
export interface ValhallaLeg {
|
||||
coordinates: [number, number][]
|
||||
@@ -111,6 +132,8 @@ export interface ValhallaLeg {
|
||||
hasToll: boolean
|
||||
hasHighway: boolean
|
||||
hasFerry: boolean
|
||||
/** CW-12: where the leg goes by boat, read off its maneuvers; absent where it does not. */
|
||||
ferrySpans?: FerrySpan[]
|
||||
}
|
||||
|
||||
/** A routed chain: one leg per pair of consecutive waypoints, plus the whole of it. */
|
||||
@@ -195,6 +218,37 @@ export async function valhallaRun(
|
||||
avoid: readonly AvoidClass[],
|
||||
signal?: AbortSignal,
|
||||
costingOptions: Readonly<CostingOptions> = {},
|
||||
): Promise<ValhallaRun | null> {
|
||||
return chainRun(waypoints, profile, avoid, signal, { costingOptions, searchFilter: null })
|
||||
}
|
||||
|
||||
/**
|
||||
* CW-12: a chain walked on foot, every point bound off the boat lines. Measured
|
||||
* on Venice: a stop by a vaporetto pier bound onto the boat line ended the
|
||||
* route on the ferry to its next landing, which `use_ferry` cannot weigh away
|
||||
* because the edge is where the route starts, not a choice along it.
|
||||
*/
|
||||
export function valhallaRunOnFoot(
|
||||
waypoints: Waypoint[],
|
||||
signal: AbortSignal | undefined,
|
||||
costingOptions: Readonly<CostingOptions>,
|
||||
searchFilter: Readonly<SearchFilter>,
|
||||
): Promise<ValhallaRun | null> {
|
||||
return chainRun(waypoints, 'walking', [], signal, { costingOptions, searchFilter })
|
||||
}
|
||||
|
||||
/** How a chain is asked for besides its profile and avoidance. */
|
||||
interface RunAsk {
|
||||
costingOptions: Readonly<CostingOptions>
|
||||
searchFilter: Readonly<SearchFilter> | null
|
||||
}
|
||||
|
||||
async function chainRun(
|
||||
waypoints: Waypoint[],
|
||||
profile: 'driving' | 'walking' | 'cycling',
|
||||
avoid: readonly AvoidClass[],
|
||||
signal: AbortSignal | undefined,
|
||||
ask: RunAsk,
|
||||
): Promise<ValhallaRun | null> {
|
||||
const base = valhallaBase()
|
||||
if (!base || notValhalla.has(base) || waypoints.length < 2) return null
|
||||
@@ -208,7 +262,7 @@ export async function valhallaRun(
|
||||
// rate limit on the last piece of a split day would otherwise throw away the pieces
|
||||
// that already answered and ask for all of them again — three times over, against a
|
||||
// host that allows one request a second.
|
||||
const run = await askWithRetry(base, () => requestRun(base, chunk, profile, avoid, signal, costingOptions), signal)
|
||||
const run = await askWithRetry(base, () => requestRun(base, chunk, profile, avoid, signal, ask), signal)
|
||||
// A day is one answer or none. Half a day of legs would be worse than no answer:
|
||||
// the schedule chains leg times blindly and cannot tell that it is missing some, so
|
||||
// it would print a complete, plausible, wrong timetable rather than a gap.
|
||||
@@ -319,6 +373,7 @@ function routeBody(
|
||||
profile: 'driving' | 'walking' | 'cycling',
|
||||
avoid: readonly AvoidClass[],
|
||||
costingOptions: Readonly<CostingOptions> = {},
|
||||
searchFilter: Readonly<SearchFilter> | null = null,
|
||||
): Record<string, unknown> {
|
||||
const costing = COSTING[profile]
|
||||
// All of them in one request: the options are independent weightings, so asking to
|
||||
@@ -328,7 +383,12 @@ function routeBody(
|
||||
const options: CostingOptions = { ...costingOptions }
|
||||
for (const cls of avoid) options[AVOID_OPTION[cls]] = 0
|
||||
return {
|
||||
locations: waypoints.map(w => ({ lat: w.lat, lon: w.lng, radius: SNAP_RADIUS_M })),
|
||||
locations: waypoints.map(w => ({
|
||||
lat: w.lat,
|
||||
lon: w.lng,
|
||||
radius: SNAP_RADIUS_M,
|
||||
...(searchFilter ? { search_filter: searchFilter } : {}),
|
||||
})),
|
||||
costing,
|
||||
costing_options: { [costing]: options },
|
||||
directions_options: { units: 'kilometers' },
|
||||
@@ -445,10 +505,10 @@ async function requestRun(
|
||||
waypoints: Waypoint[],
|
||||
profile: 'driving' | 'walking' | 'cycling',
|
||||
avoid: readonly AvoidClass[],
|
||||
signal?: AbortSignal,
|
||||
costingOptions: Readonly<CostingOptions> = {},
|
||||
signal: AbortSignal | undefined,
|
||||
{ costingOptions, searchFilter }: RunAsk,
|
||||
): Promise<Omit<ValhallaRun, 'snapped'> | null> {
|
||||
const data = await postRoute(base, routeBody(waypoints, profile, avoid, costingOptions), signal)
|
||||
const data = await postRoute(base, routeBody(waypoints, profile, avoid, costingOptions, searchFilter), signal)
|
||||
return data === null ? null : runFrom(data)
|
||||
}
|
||||
|
||||
@@ -508,10 +568,14 @@ export function alternatesFrom(data: unknown): ValhallaLeg[] | null {
|
||||
*/
|
||||
function joinLegs(legs: ValhallaLeg[]): ValhallaLeg {
|
||||
const coordinates: [number, number][] = []
|
||||
const ferrySpans: FerrySpan[] = []
|
||||
for (const leg of legs) {
|
||||
const last = coordinates[coordinates.length - 1]
|
||||
const first = leg.coordinates[0]
|
||||
const continues = last && first && last[0] === first[0] && last[1] === first[1]
|
||||
// Where the leg's first point lands on the joined line, so its boat stretches keep pointing at their own points.
|
||||
const offset = continues ? coordinates.length - 1 : coordinates.length
|
||||
for (const span of leg.ferrySpans ?? []) ferrySpans.push({ ...span, from: span.from + offset, to: span.to + offset })
|
||||
coordinates.push(...(continues ? leg.coordinates.slice(1) : leg.coordinates))
|
||||
}
|
||||
return {
|
||||
@@ -523,6 +587,7 @@ function joinLegs(legs: ValhallaLeg[]): ValhallaLeg {
|
||||
hasToll: legs.some(l => l.hasToll),
|
||||
hasHighway: legs.some(l => l.hasHighway),
|
||||
hasFerry: legs.some(l => l.hasFerry),
|
||||
...(ferrySpans.length ? { ferrySpans } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,19 +625,66 @@ export function runFrom(data: unknown): Omit<ValhallaRun, 'snapped'> | null {
|
||||
// Paris route reads 488.56, which is not a latitude; as 6 it reads 48.856193.
|
||||
const measure = measured(summary, decodePolyline(leg.shape, 6))
|
||||
if (!measure) return null
|
||||
legs.push(measure)
|
||||
const ferrySpans = measure.hasFerry ? ferrySpansOf(leg.maneuvers, measure.coordinates.length) : []
|
||||
legs.push(ferrySpans.length ? { ...measure, ferrySpans } : measure)
|
||||
}
|
||||
return { legs, total: joinLegs(legs) }
|
||||
}
|
||||
|
||||
interface ValhallaSummary { [key: string]: number | boolean | undefined }
|
||||
|
||||
interface ValhallaManeuver {
|
||||
type?: number
|
||||
ferry?: boolean
|
||||
begin_shape_index?: number
|
||||
end_shape_index?: number
|
||||
time?: number
|
||||
length?: number
|
||||
}
|
||||
|
||||
function tripOf(data: unknown) {
|
||||
return (data as {
|
||||
trip?: { legs?: { shape?: string; summary?: ValhallaSummary }[]; summary?: ValhallaSummary }
|
||||
trip?: {
|
||||
legs?: { shape?: string; summary?: ValhallaSummary; maneuvers?: ValhallaManeuver[] }[]
|
||||
summary?: ValhallaSummary
|
||||
}
|
||||
})?.trip
|
||||
}
|
||||
|
||||
/** Valhalla's maneuver type for boarding a ferry; the maneuver also carries `ferry: true` for its crossing. */
|
||||
const FERRY_ENTER = 28
|
||||
|
||||
/**
|
||||
* CW-12: the stretches of a leg the router takes by boat, read off its
|
||||
* maneuvers: a crossing is the maneuver that boards the ferry, from the point
|
||||
* it starts at to the one it ends at, with its own length and time. Measured
|
||||
* on Venice, the San Tomà to Ca' Rezzonico line came back as one maneuver of
|
||||
* type 28, ferry true, 1.236 km and 745 s over shape points 24 to 46, then the
|
||||
* walk off it. Crossings that touch are one. A leg the summary says has a
|
||||
* ferry but whose maneuvers do not say where is taken whole, without a time:
|
||||
* the crossing is somewhere on it, and none of it is counted as walked.
|
||||
*/
|
||||
function ferrySpansOf(maneuvers: ValhallaManeuver[] | undefined, points: number): FerrySpan[] {
|
||||
const spans: FerrySpan[] = []
|
||||
for (const maneuver of maneuvers ?? []) {
|
||||
if (maneuver.ferry !== true && maneuver.type !== FERRY_ENTER) continue
|
||||
const from = Number(maneuver.begin_shape_index)
|
||||
const to = Number(maneuver.end_shape_index)
|
||||
if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to <= from || to >= points) continue
|
||||
const metres = Number.isFinite(maneuver.length) ? Number(maneuver.length) * 1000 : 0
|
||||
const seconds = Number.isFinite(maneuver.time) ? Number(maneuver.time) : null
|
||||
const previous = spans[spans.length - 1]
|
||||
if (previous && previous.to === from) {
|
||||
previous.to = to
|
||||
previous.metres += metres
|
||||
previous.seconds = previous.seconds === null || seconds === null ? null : previous.seconds + seconds
|
||||
} else {
|
||||
spans.push({ from, to, metres, seconds })
|
||||
}
|
||||
}
|
||||
return spans.length || points < 2 ? spans : [{ from: 0, to: points - 1, metres: 0, seconds: null }]
|
||||
}
|
||||
|
||||
/** A summary and a shape as a leg, or null when the summary carries no numbers. */
|
||||
function measured(summary: ValhallaSummary, coordinates: [number, number][]): ValhallaLeg | null {
|
||||
if (!coordinates.length) return null
|
||||
|
||||
Reference in New Issue
Block a user