mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
73237ffedb
The tour type is a dropdown; a bike tour picks its profile, routes for it, says when a point lies off every way, and shows what it runs on: surfaces, kinds of way and, for a mountain bike, the trail grades, each pointed at on the map. A type's own map (OpenHikingMap for a hike, CyclOSM for a bike tour) is offered while a tour of that type is open, with an admin switch each. From the planned cards: a hike avoids steep climbs, steps or ferries, nothing else; a saved tour can be shared where the platform takes GPX files; and the tours rail adds a tour from one menu: plan new, import GPX, or take one over from another trip.
586 lines
26 KiB
TypeScript
586 lines
26 KiB
TypeScript
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'
|
|
|
|
const FOSSGIS_VALHALLA = 'https://valhalla1.openstreetmap.de/route'
|
|
|
|
/**
|
|
* A real polyline6, encoded from three known points and verified by decoding it back.
|
|
* Precision matters here more than anywhere else in the file: read as polyline5 the
|
|
* first vertex would be 535.511, which is not a latitude, and a wrong guess draws a
|
|
* line into the sea rather than failing.
|
|
*/
|
|
const SHAPE = 'w~nceBg}}`RgkPwjr@~fn~@oirmE'
|
|
const SHAPE_POINTS: [number, number][] = [[53.5511, 9.9937], [53.56, 10.02], [52.52, 13.405]]
|
|
|
|
const answer = (summary: Record<string, unknown> = {}) => ({
|
|
trip: {
|
|
legs: [{ shape: SHAPE }],
|
|
summary: {
|
|
has_time_restrictions: false,
|
|
has_toll: false,
|
|
has_highway: true,
|
|
has_ferry: false,
|
|
// Asked in kilometres, so this is 295.7 km and 191 minutes — the real
|
|
// Hamburg-to-Berlin answer, which the adapter has to hand on in metres and seconds.
|
|
length: 295.7,
|
|
time: 11460,
|
|
...summary,
|
|
},
|
|
},
|
|
})
|
|
|
|
const setSettings = (patch: Record<string, string>) =>
|
|
useSettingsStore.setState(st => ({ settings: { ...st.settings, ...patch } }))
|
|
|
|
afterEach(() => {
|
|
setSettings({ routing_base_url: '', valhalla_base_url: '' })
|
|
resetValhallaTurns()
|
|
})
|
|
|
|
describe('valhallaTurn', () => {
|
|
const PUBLIC = 'https://valhalla1.openstreetmap.de'
|
|
|
|
afterEach(() => vi.useRealTimers())
|
|
|
|
it('spaces questions to the public instance a little over a second apart, across callers', async () => {
|
|
vi.useFakeTimers()
|
|
const done: number[] = []
|
|
const start = Date.now()
|
|
await valhallaTurn(PUBLIC)
|
|
done.push(Date.now() - start)
|
|
const second = valhallaTurn(PUBLIC).then(() => done.push(Date.now() - start))
|
|
const third = valhallaTurn(PUBLIC).then(() => done.push(Date.now() - start))
|
|
|
|
await vi.advanceTimersByTimeAsync(5000)
|
|
await Promise.all([second, third])
|
|
|
|
expect(done[0]).toBe(0)
|
|
expect(done[1]).toBeGreaterThanOrEqual(1100)
|
|
expect(done[2] - done[1]).toBeGreaterThanOrEqual(1100)
|
|
})
|
|
|
|
it("does not pace an operator's own host", async () => {
|
|
vi.useFakeTimers()
|
|
await valhallaTurn('https://valhalla.example.org')
|
|
let through = false
|
|
void valhallaTurn('https://valhalla.example.org').then(() => { through = true })
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
expect(through).toBe(true)
|
|
})
|
|
|
|
it('lets a caller that gives up go without taking the turn', async () => {
|
|
vi.useFakeTimers()
|
|
await valhallaTurn(PUBLIC)
|
|
const controller = new AbortController()
|
|
const waiting = valhallaTurn(PUBLIC, controller.signal)
|
|
controller.abort()
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
await waiting
|
|
// The abandoned wait claimed nothing, so the next caller only waits out the first turn.
|
|
const start = Date.now()
|
|
const next = valhallaTurn(PUBLIC)
|
|
await vi.advanceTimersByTimeAsync(1100)
|
|
await next
|
|
expect(Date.now() - start).toBeLessThanOrEqual(1100)
|
|
})
|
|
})
|
|
|
|
const HAMBURG = { lat: 53.5511, lng: 9.9937 }
|
|
const BERLIN = { lat: 52.52, lng: 13.405 }
|
|
|
|
describe('valhallaBase', () => {
|
|
it('VALHALLA-BASE-001: falls back to the public instance when nothing is configured', () => {
|
|
expect(valhallaBase()).toBe('https://valhalla1.openstreetmap.de')
|
|
expect(valhallaAvailable()).toBe(true)
|
|
})
|
|
|
|
it('VALHALLA-BASE-002: uses the configured instance and drops its trailing slashes', () => {
|
|
setSettings({ valhalla_base_url: 'https://valhalla.example.org//' })
|
|
expect(valhallaBase()).toBe('https://valhalla.example.org')
|
|
})
|
|
|
|
it('VALHALLA-BASE-003: asks nothing when the instance runs its own OSRM', () => {
|
|
// An operator who filled in their own router chose against the public hosts, usually
|
|
// because the instance is not supposed to reach third parties at all. Adding one they
|
|
// never asked for would undo that, and their MLD-built OSRM answers exclude itself.
|
|
setSettings({ routing_base_url: 'https://osrm.example.org' })
|
|
expect(valhallaBase()).toBeNull()
|
|
expect(valhallaAvailable()).toBe(false)
|
|
})
|
|
|
|
it('VALHALLA-BASE-004: an own Valhalla wins over an own OSRM', () => {
|
|
setSettings({ routing_base_url: 'https://osrm.example.org', valhalla_base_url: 'https://v.example.org' })
|
|
expect(valhallaBase()).toBe('https://v.example.org')
|
|
})
|
|
})
|
|
|
|
describe('runFrom', () => {
|
|
const total = (data: unknown) => runFrom(data)?.total ?? null
|
|
|
|
it('VALHALLA-PARSE-001: decodes the shape as polyline6 and converts km to metres', () => {
|
|
const leg = total(answer())
|
|
expect(leg).not.toBeNull()
|
|
expect(leg!.coordinates).toEqual(SHAPE_POINTS)
|
|
expect(leg!.distance).toBeCloseTo(295700, 0)
|
|
expect(leg!.duration).toBe(11460)
|
|
})
|
|
|
|
it('VALHALLA-PARSE-002: carries the flags through as booleans', () => {
|
|
const leg = total(answer({ has_toll: true, has_ferry: true, has_highway: false }))
|
|
expect(leg).toMatchObject({ hasToll: true, hasFerry: true, hasHighway: false })
|
|
})
|
|
|
|
it('VALHALLA-PARSE-003: a missing flag reads as absent rather than unknown', () => {
|
|
// The public instance omits a flag it has nothing to say about, and the caller turns
|
|
// this into a label. Undefined has to become false, not a truthy object.
|
|
const leg = total({ trip: { legs: [{ shape: SHAPE }], summary: { length: 1, time: 60 } } })
|
|
expect(leg).toMatchObject({ hasToll: false, hasHighway: false, hasFerry: false })
|
|
})
|
|
|
|
it('VALHALLA-PARSE-004: keeps a chain divided leg by leg, and adds it up as well', () => {
|
|
// What a request with a via really answers: one summary per pair of stops. The
|
|
// schedule is built from those, so flattening them into a total would lose the
|
|
// arrival time at every stop but the last.
|
|
const chain = {
|
|
trip: {
|
|
legs: [
|
|
{ shape: SHAPE, summary: { length: 100, time: 3600, has_toll: false, has_highway: true, has_ferry: false } },
|
|
{ shape: SHAPE, summary: { length: 50, time: 1800, has_toll: true, has_highway: false, has_ferry: false } },
|
|
],
|
|
summary: { length: 150, time: 5400, has_toll: true, has_highway: true, has_ferry: false },
|
|
},
|
|
}
|
|
const run = runFrom(chain)!
|
|
expect(run.legs.map(l => l.distance)).toEqual([100000, 50000])
|
|
expect(run.legs.map(l => l.hasToll)).toEqual([false, true])
|
|
expect(run.total.distance).toBe(150000)
|
|
expect(run.total.duration).toBe(5400)
|
|
// One tolled leg makes the drive tolled. `every` here would call a day with a
|
|
// single toll booth in it toll-free.
|
|
expect(run.total.hasToll).toBe(true)
|
|
expect(run.total.coordinates).toHaveLength(SHAPE_POINTS.length * 2)
|
|
})
|
|
|
|
it('VALHALLA-PARSE-007: several legs with no summaries of their own are refused', () => {
|
|
// There is no honest way to divide one total between them, and a guess would reach
|
|
// the user as a wrong arrival time rather than as a missing one.
|
|
const two = { trip: { legs: [{ shape: SHAPE }, { shape: SHAPE }], summary: { length: 2, time: 120 } } }
|
|
expect(runFrom(two)).toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-PARSE-005: an error body is not a leg', () => {
|
|
// What a 400 carries: no trip at all. Parsing it as one would produce a route of
|
|
// NaN kilometres rather than the null every caller reads as "no second way".
|
|
expect(total({ error_code: 442, error: 'No path could be found for input' })).toBeNull()
|
|
expect(total({ trip: { legs: [], summary: { length: 1, time: 1 } } })).toBeNull()
|
|
expect(total({ trip: { legs: [{ shape: SHAPE }] } })).toBeNull()
|
|
expect(total(null)).toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-PARSE-006: a summary without numbers is not a leg', () => {
|
|
expect(total({ trip: { legs: [{ shape: SHAPE }], summary: { length: 'far', time: 60 } } })).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('legAvoids', () => {
|
|
const leg = (flags: Partial<{ hasToll: boolean; hasHighway: boolean; hasFerry: boolean }>) => ({
|
|
coordinates: SHAPE_POINTS, distance: 1, duration: 1,
|
|
hasToll: false, hasHighway: false, hasFerry: false, ...flags,
|
|
})
|
|
|
|
it('VALHALLA-AVOID-001: reads the answer, not the request', () => {
|
|
// use_tolls: 0 is a weighting. Between two points with no untolled connection the
|
|
// answer is still a tolled road, honestly flagged, and labelling that "No tolls"
|
|
// would put a lie on the map.
|
|
expect(legAvoids(leg({ hasToll: false }), 'toll')).toBe(true)
|
|
expect(legAvoids(leg({ hasToll: true }), 'toll')).toBe(false)
|
|
})
|
|
|
|
it('VALHALLA-AVOID-002: each class reads its own flag', () => {
|
|
expect(legAvoids(leg({ hasHighway: true }), 'motorway')).toBe(false)
|
|
expect(legAvoids(leg({ hasHighway: true }), 'toll')).toBe(true)
|
|
expect(legAvoids(leg({ hasFerry: true }), 'ferry')).toBe(false)
|
|
expect(legAvoids(leg({ hasFerry: false }), 'ferry')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('valhallaRouteAvoiding', () => {
|
|
it('VALHALLA-REQ-001: posts the avoidance as a costing option under the right profile', async () => {
|
|
let body: Record<string, unknown> | null = null
|
|
server.use(
|
|
http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
body = await request.json() as Record<string, unknown>
|
|
return HttpResponse.json(answer())
|
|
})
|
|
)
|
|
const leg = await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')
|
|
expect(leg!.distance).toBeCloseTo(295700, 0)
|
|
expect(body).toMatchObject({
|
|
costing: 'auto',
|
|
costing_options: { auto: { use_tolls: 0 } },
|
|
directions_options: { units: 'kilometers' },
|
|
})
|
|
// The radius is what makes a place in a pedestrian zone routable at all: without it
|
|
// Vienna's Stephansdom answers error 442 and the whole leg is lost.
|
|
expect(body!.locations).toEqual([
|
|
{ lat: 53.5511, lon: 9.9937, radius: 50 },
|
|
{ lat: 52.52, lon: 13.405, radius: 50 },
|
|
])
|
|
})
|
|
|
|
it('VALHALLA-REQ-002: each class maps to its own costing option', async () => {
|
|
const seen: Record<string, unknown>[] = []
|
|
server.use(
|
|
http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
seen.push(await request.json() as Record<string, unknown>)
|
|
return HttpResponse.json(answer())
|
|
})
|
|
)
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'motorway')
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'ferry')
|
|
expect(seen.map(b => b.costing_options)).toEqual([
|
|
{ auto: { use_highways: 0 } },
|
|
{ auto: { use_ferry: 0 } },
|
|
])
|
|
})
|
|
|
|
it('VALHALLA-REQ-003: walking and cycling get their own costing models', async () => {
|
|
const seen: string[] = []
|
|
server.use(
|
|
http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
const body = await request.json() as { costing: string }
|
|
seen.push(body.costing)
|
|
return HttpResponse.json(answer())
|
|
})
|
|
)
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'walking', 'ferry')
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'cycling', 'ferry')
|
|
expect(seen).toEqual(['pedestrian', 'bicycle'])
|
|
})
|
|
|
|
it('VALHALLA-REQ-004: a refusal is a null, not a throw', async () => {
|
|
// Every failure means the same thing to the caller — there is no second way to
|
|
// show — so a 400 for an unroutable point and a 429 for the rate limit both end
|
|
// here rather than as an error over a map that is drawing fine.
|
|
server.use(
|
|
http.post(FOSSGIS_VALHALLA, () =>
|
|
HttpResponse.json({ error_code: 442, error: 'No path could be found for input' }, { status: 400 })
|
|
)
|
|
)
|
|
await expect(valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')).resolves.toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-REQ-005: a host that is not a Valhalla is only asked once', async () => {
|
|
let calls = 0
|
|
setSettings({ valhalla_base_url: 'https://not-valhalla.example.org' })
|
|
server.use(
|
|
http.post('https://not-valhalla.example.org/route', () => {
|
|
calls++
|
|
return new HttpResponse(null, { status: 404 })
|
|
})
|
|
)
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')
|
|
await valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'motorway')
|
|
// A 404 is about the URL rather than about this leg, so remembering it saves every
|
|
// later leg a request against a host that will never answer.
|
|
expect(calls).toBe(1)
|
|
})
|
|
|
|
it('VALHALLA-REQ-006: asks nothing at all when the instance has its own OSRM', async () => {
|
|
let calls = 0
|
|
setSettings({ routing_base_url: 'https://osrm.example.org' })
|
|
server.use(http.post(FOSSGIS_VALHALLA, () => { calls++; return HttpResponse.json(answer()) }))
|
|
await expect(valhallaRouteAvoiding(HAMBURG, BERLIN, 'driving', 'toll')).resolves.toBeNull()
|
|
expect(calls).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('valhallaRun over a whole day', () => {
|
|
/** A chain answer with one summary per leg, which is what a real request returns. */
|
|
const chain = (legCount: number) => ({
|
|
trip: {
|
|
legs: Array.from({ length: legCount }, () => ({
|
|
shape: SHAPE,
|
|
summary: { length: 10, time: 600, has_toll: false, has_highway: true, has_ferry: false },
|
|
})),
|
|
summary: { length: 10 * legCount, time: 600 * legCount, has_toll: false, has_highway: true, has_ferry: false },
|
|
},
|
|
})
|
|
|
|
const stops = (n: number) =>
|
|
Array.from({ length: n }, (_, i) => ({ lat: 53 - i * 0.1, lng: 10 + i * 0.1 }))
|
|
|
|
it('retains Tours costing options through the upstream spaced retry', async () => {
|
|
const bodies: unknown[] = []
|
|
const times: number[] = []
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
bodies.push(await request.json())
|
|
times.push(Date.now())
|
|
return bodies.length === 1 ? new HttpResponse(null, { status: 429 }) : HttpResponse.json(chain(1))
|
|
}))
|
|
const result = await valhallaRun(stops(2), 'walking', [], undefined, { max_hiking_difficulty: 6 })
|
|
expect(result).not.toBeNull()
|
|
expect(bodies).toHaveLength(2)
|
|
expect(bodies[1]).toEqual(bodies[0])
|
|
expect(bodies[1]).toMatchObject({ costing_options: { pedestrian: { max_hiking_difficulty: 6 } } })
|
|
expect(times[1] - times[0]).toBeGreaterThanOrEqual(1000)
|
|
})
|
|
|
|
it('does not send another Tours request when aborted during the retry pause', async () => {
|
|
const controller = new AbortController()
|
|
let calls = 0
|
|
server.use(http.post(FOSSGIS_VALHALLA, () => {
|
|
calls++
|
|
setTimeout(() => controller.abort(), 25)
|
|
return new HttpResponse(null, { status: 429 })
|
|
}))
|
|
await expect(valhallaRun(stops(2), 'walking', [], controller.signal, { max_hiking_difficulty: 4 })).resolves.toBeNull()
|
|
expect(calls).toBe(1)
|
|
})
|
|
|
|
it.each([1, 2, 3, 4, 5, 6])('keeps Tours T%i options in the exact pedestrian request with avoidance', async difficulty => {
|
|
let body: unknown
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
body = await request.json()
|
|
return HttpResponse.json(chain(1))
|
|
}))
|
|
await valhallaRun(stops(2), 'walking', ['ferry'], undefined, { max_hiking_difficulty: difficulty })
|
|
expect(body).toEqual({
|
|
locations: stops(2).map(point => ({ lat: point.lat, lon: point.lng, radius: 50 })),
|
|
costing: 'pedestrian',
|
|
costing_options: { pedestrian: { max_hiking_difficulty: difficulty, use_ferry: 0 } },
|
|
directions_options: { units: 'kilometers' },
|
|
})
|
|
})
|
|
|
|
it('VALHALLA-RUN-001: a day inside the cap goes out as one request, one leg per pair', async () => {
|
|
let calls = 0
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
calls++
|
|
const body = await request.json() as { locations: unknown[] }
|
|
return HttpResponse.json(chain(body.locations.length - 1))
|
|
}))
|
|
|
|
const run = await valhallaRun(stops(6), 'driving', [])
|
|
|
|
expect(calls).toBe(1)
|
|
expect(run!.legs).toHaveLength(5)
|
|
expect(run!.total.distance).toBe(50000)
|
|
})
|
|
|
|
it('VALHALLA-RUN-008: forwards caller-specific costing options only when supplied', async () => {
|
|
let body: Record<string, unknown> | null = null
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
body = await request.json() as Record<string, unknown>
|
|
return HttpResponse.json(chain(1))
|
|
}))
|
|
|
|
await valhallaRun(stops(2), 'walking', [], undefined, { max_hiking_difficulty: 2 })
|
|
|
|
expect(body).toMatchObject({
|
|
costing: 'pedestrian',
|
|
costing_options: { pedestrian: { max_hiking_difficulty: 2 } },
|
|
})
|
|
})
|
|
|
|
it('VALHALLA-RUN-002: a day over the cap is split on a shared point, and the legs still line up', async () => {
|
|
// Ten locations is hard: the eleventh answers error 150. Split runs overlap by one
|
|
// point, which is exact rather than approximate — measured over a six-stop day,
|
|
// [0..3] plus [3..5] came back within two metres of the same day asked whole.
|
|
const sizes: number[] = []
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
const body = await request.json() as { locations: unknown[] }
|
|
sizes.push(body.locations.length)
|
|
return HttpResponse.json(chain(body.locations.length - 1))
|
|
}))
|
|
|
|
const run = await valhallaRun(stops(14), 'driving', ['toll'])
|
|
|
|
// Fourteen stops: the first run takes ten, the second starts on the tenth again and
|
|
// takes the remaining five. Nine pairs plus four.
|
|
expect(sizes).toEqual([10, 5])
|
|
// Thirteen pairs across two requests, none lost and none counted twice.
|
|
expect(run!.legs).toHaveLength(13)
|
|
expect(run!.total.distance).toBe(130000)
|
|
})
|
|
|
|
it('VALHALLA-RUN-003: never sends more than ten locations, however long the day', async () => {
|
|
const sizes: number[] = []
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
const body = await request.json() as { locations: unknown[] }
|
|
sizes.push(body.locations.length)
|
|
return HttpResponse.json(chain(body.locations.length - 1))
|
|
}))
|
|
|
|
await valhallaRun(stops(28), 'driving', [])
|
|
|
|
expect(Math.max(...sizes)).toBeLessThanOrEqual(10)
|
|
expect(sizes.reduce((sum, n) => sum + n - 1, 0)).toBe(27)
|
|
})
|
|
|
|
it('VALHALLA-RUN-004: a piece that will not answer loses the whole day rather than half of it', async () => {
|
|
// Half a day of legs is worse than none. The schedule chains leg times blindly and
|
|
// cannot tell that some are missing, so it would print a complete, plausible,
|
|
// silently wrong timetable instead of leaving a visible gap.
|
|
//
|
|
// The SECOND CHUNK fails, not the second request: one attempt is retried, so failing
|
|
// by call number would just be answered on the retry.
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
const body = await request.json() as { locations: unknown[] }
|
|
if (body.locations.length < 10) {
|
|
return HttpResponse.json({ error_code: 442, error: 'No path' }, { status: 400 })
|
|
}
|
|
return HttpResponse.json(chain(body.locations.length - 1))
|
|
}))
|
|
|
|
await expect(valhallaRun(stops(14), 'driving', ['toll'])).resolves.toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-RUN-007: a piece that fails once is asked again rather than losing the day', async () => {
|
|
// The caller retries the whole task, so without this a rate limit on the last piece
|
|
// of a split day throws away the pieces that already answered and asks for all of
|
|
// them again — three times over, against a host that allows one request a second.
|
|
let calls = 0
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
const body = await request.json() as { locations: unknown[] }
|
|
calls++
|
|
if (calls === 2) return new HttpResponse(null, { status: 429 })
|
|
return HttpResponse.json(chain(body.locations.length - 1))
|
|
}))
|
|
|
|
const run = await valhallaRun(stops(14), 'driving', ['toll'])
|
|
|
|
expect(run!.legs).toHaveLength(13)
|
|
// First piece, second piece refused, second piece again.
|
|
expect(calls).toBe(3)
|
|
})
|
|
|
|
it('VALHALLA-RUN-005: fewer than two points is nothing to route', async () => {
|
|
let calls = 0
|
|
server.use(http.post(FOSSGIS_VALHALLA, () => { calls++; return HttpResponse.json(chain(1)) }))
|
|
await expect(valhallaRun(stops(1), 'driving', [])).resolves.toBeNull()
|
|
expect(calls).toBe(0)
|
|
})
|
|
|
|
it('VALHALLA-RUN-006: an answer with the wrong number of legs is refused', async () => {
|
|
// A day whose legs do not match its stops cannot be assigned to the rail, and
|
|
// guessing which stop is missing would put the wrong time against the right place.
|
|
server.use(http.post(FOSSGIS_VALHALLA, () => HttpResponse.json(chain(2))))
|
|
await expect(valhallaRun(stops(6), 'driving', [])).resolves.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('valhallaAlternates', () => {
|
|
/** The same leg read three ways, the second of them crossing by ferry. */
|
|
const withAlternates = (alternates: unknown[]) => ({
|
|
...answer({ time: 11_460 }),
|
|
alternates,
|
|
})
|
|
|
|
it('VALHALLA-ALT-001: reads the preferred way, then every alternate, each with its own flags', () => {
|
|
const ways = alternatesFrom(withAlternates([
|
|
answer({ time: 12_000, has_ferry: true }),
|
|
answer({ time: 13_000, has_toll: true }),
|
|
]))
|
|
|
|
expect(ways?.map(w => w.duration)).toEqual([11_460, 12_000, 13_000])
|
|
expect(ways?.map(w => w.hasFerry)).toEqual([false, true, false])
|
|
expect(ways?.map(w => w.hasToll)).toEqual([false, false, true])
|
|
expect(ways?.[1].coordinates).toEqual(SHAPE_POINTS)
|
|
})
|
|
|
|
it('VALHALLA-ALT-002: an alternate that cannot be read is dropped, a preferred way that cannot is no answer', () => {
|
|
// One bad entry is no reason to lose the leg's other ways, but the others are offered
|
|
// against the preferred one and mean nothing without it.
|
|
expect(alternatesFrom(withAlternates([{ trip: { legs: [] } }, answer({ time: 13_000 })]))?.map(w => w.duration))
|
|
.toEqual([11_460, 13_000])
|
|
expect(alternatesFrom(answer())).toHaveLength(1)
|
|
expect(alternatesFrom({ alternates: [answer()] })).toBeNull()
|
|
expect(alternatesFrom({ error_code: 442, error: 'No path could be found for input' })).toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-ALT-003: asks for the leg between exactly its two ends, weighed the way the trip is, with the number of alternates', async () => {
|
|
const bodies: Record<string, unknown>[] = []
|
|
server.use(http.post(FOSSGIS_VALHALLA, async ({ request }) => {
|
|
bodies.push(await request.json() as Record<string, unknown>)
|
|
return HttpResponse.json(withAlternates([answer({ time: 12_000 })]))
|
|
}))
|
|
|
|
const ways = await valhallaAlternates(HAMBURG, BERLIN, 'driving', ['toll', 'ferry'], 2)
|
|
await valhallaAlternates(HAMBURG, BERLIN, 'driving', ['toll'], 0)
|
|
|
|
expect(ways).toHaveLength(2)
|
|
expect(bodies[0]).toMatchObject({ alternates: 2, costing: 'auto', costing_options: { auto: { use_tolls: 0, use_ferry: 0 } } })
|
|
expect(bodies[0].locations).toHaveLength(2)
|
|
// No alternates asked for, none named: the request stays the plain one.
|
|
expect(bodies[1]).not.toHaveProperty('alternates')
|
|
})
|
|
|
|
it('VALHALLA-ALT-004: a refusal is asked once more, then is no answer; an instance with its own OSRM asks nothing', async () => {
|
|
let calls = 0
|
|
server.use(http.post(FOSSGIS_VALHALLA, () => { calls++; return new HttpResponse(null, { status: 429 }) }))
|
|
|
|
await expect(valhallaAlternates(HAMBURG, BERLIN, 'driving', ['toll'], 2)).resolves.toBeNull()
|
|
expect(calls).toBe(2)
|
|
|
|
setSettings({ routing_base_url: 'https://osrm.example.org' })
|
|
await expect(valhallaAlternates(HAMBURG, BERLIN, 'driving', ['toll'], 2)).resolves.toBeNull()
|
|
expect(calls).toBe(2)
|
|
})
|
|
})
|
|
|
|
describe('valhallaTraceAttributes (BIK-01)', () => {
|
|
const TRACE = 'https://valhalla1.openstreetmap.de/trace_attributes'
|
|
|
|
it('VALHALLA-TRACE-001: asks for the ways under the line, matched as a mountain bike rides, and hands the answer on', async () => {
|
|
const bodies: Record<string, unknown>[] = []
|
|
server.use(http.post(TRACE, async ({ request }) => {
|
|
bodies.push(await request.json() as Record<string, unknown>)
|
|
return HttpResponse.json({ shape: SHAPE, edges: [] })
|
|
}))
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS)).resolves.toEqual({ shape: SHAPE, edges: [] })
|
|
expect(bodies[0]).toMatchObject({
|
|
costing: 'bicycle',
|
|
costing_options: { bicycle: { bicycle_type: 'Mountain' } },
|
|
shape_match: 'map_snap',
|
|
filters: { action: 'include' },
|
|
})
|
|
expect(bodies[0].shape).toEqual(SHAPE_POINTS.map(([lat, lon]) => ({ lat, lon })))
|
|
})
|
|
|
|
it('VALHALLA-TRACE-002: a long line is thinned evenly to what one request carries, its ends kept', async () => {
|
|
let shape: unknown[] = []
|
|
server.use(http.post(TRACE, async ({ request }) => {
|
|
shape = ((await request.json()) as { shape: unknown[] }).shape
|
|
return HttpResponse.json({})
|
|
}))
|
|
const line = Array.from({ length: 9001 }, (_, i): [number, number] => [47 + i / 1e5, 11])
|
|
await valhallaTraceAttributes(line)
|
|
expect(shape.length).toBeLessThanOrEqual(4001)
|
|
expect(shape[0]).toEqual({ lat: 47, lon: 11 })
|
|
expect(shape[shape.length - 1]).toEqual({ lat: line[9000][0], lon: 11 })
|
|
})
|
|
|
|
it('VALHALLA-TRACE-003: null for a refusal, a network failure, a line of one point or a router that is not Valhalla', async () => {
|
|
server.use(http.post(TRACE, () => new HttpResponse(null, { status: 400 })))
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS)).resolves.toBeNull()
|
|
server.use(http.post(TRACE, () => HttpResponse.error()))
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS)).resolves.toBeNull()
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS.slice(0, 1))).resolves.toBeNull()
|
|
setSettings({ routing_base_url: 'https://osrm.example' })
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS)).resolves.toBeNull()
|
|
})
|
|
|
|
it('VALHALLA-TRACE-004: a caller gone before its turn asks nothing', async () => {
|
|
let asked = false
|
|
server.use(http.post(TRACE, () => { asked = true; return HttpResponse.json({}) }))
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
await expect(valhallaTraceAttributes(SHAPE_POINTS, controller.signal)).resolves.toBeNull()
|
|
expect(asked).toBe(false)
|
|
})
|
|
})
|