mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
e64ade010f
On a day a flight, train, ferry, cruise or bus leaves and lands, tonight's stay was seated by its check-in time. A hotel in Munich checked into from 15:00 stood ahead of a 15:15 flight out of Hamburg, so the drive went from the Munich hotel to Hamburg airport and reported the flight as nine hours late. A check-in is the earliest arrival, not an appointment: such a stay now waits behind the landing when it lies nearer the arrival than the departure, and its check-in only holds it when the ride lands before the room is ready. A time typed on the stop keeps its seat. With the stays at both ends of a day switched on: - a day with nothing but the next stay's check-in and a ride booked across it gets no drive from one hotel to the other, the same rule the day plan follows since #2476; - a stay is never joined by road to a terminal more than 2000 km away. The check moved to shared/roadtrip so Days and the road trip read one copy. A departure terminal's pin is dated: reaching it more than half a day late no longer moves the flight to the next day without a word, it is a late warning with the full minutes. A check-in evening also carries the stay's check-in time, as a label only. calculate_roadtrip runs the same code and its description says all of the above.
449 lines
18 KiB
TypeScript
449 lines
18 KiB
TypeScript
import {
|
||
boxAround,
|
||
corridorTiles,
|
||
distanceToSegmentKm,
|
||
drivenPieces,
|
||
haversineKm,
|
||
inRiddenRange,
|
||
MAX_DRIVE_KM,
|
||
pointAtMeters,
|
||
projectOntoRoute,
|
||
rideGaps,
|
||
riddenRanges,
|
||
sliceAtMeters,
|
||
simplifyLine,
|
||
withinDriveRange,
|
||
type LatLng,
|
||
} from './corridor';
|
||
import type { RoadtripStop } from './planning-types';
|
||
|
||
import { describe, it, expect } from 'vitest';
|
||
|
||
const BERLIN: LatLng = { lat: 52.52, lng: 13.405 };
|
||
const DRESDEN: LatLng = { lat: 51.05, lng: 13.74 };
|
||
const PRAGUE: LatLng = { lat: 50.088, lng: 14.42 };
|
||
|
||
describe('haversineKm', () => {
|
||
it('measures a known distance', () => {
|
||
// Berlin → Dresden is about 165 km as the crow flies.
|
||
expect(haversineKm(BERLIN, DRESDEN)).toBeGreaterThan(160);
|
||
expect(haversineKm(BERLIN, DRESDEN)).toBeLessThan(170);
|
||
});
|
||
|
||
it('is zero for the same point', () => {
|
||
expect(haversineKm(BERLIN, BERLIN)).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('withinDriveRange', () => {
|
||
it('joins by road what lies up to MAX_DRIVE_KM apart as the crow flies, and nothing further', () => {
|
||
expect(MAX_DRIVE_KM).toBe(2000);
|
||
expect(withinDriveRange(BERLIN, PRAGUE)).toBe(true);
|
||
// Paris to New York is a flight, not a leg of anybody's day.
|
||
expect(withinDriveRange({ lat: 48.8566, lng: 2.3522 }, { lat: 40.6413, lng: -73.7781 })).toBe(false);
|
||
// A degree of latitude is about 111 km: 17.9 of them fall short of the limit, 18.1 pass it.
|
||
expect(withinDriveRange(BERLIN, { lat: BERLIN.lat + 17.9, lng: BERLIN.lng })).toBe(true);
|
||
expect(withinDriveRange(BERLIN, { lat: BERLIN.lat + 18.1, lng: BERLIN.lng })).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('distanceToSegmentKm', () => {
|
||
const a: LatLng = { lat: 52, lng: 13 };
|
||
const b: LatLng = { lat: 52, lng: 14 };
|
||
|
||
it('measures perpendicular distance to the line', () => {
|
||
const p: LatLng = { lat: 52.09, lng: 13.5 };
|
||
// 0.09° of latitude is about 10 km, whatever the longitude does.
|
||
expect(distanceToSegmentKm(p, a, b)).toBeGreaterThan(9);
|
||
expect(distanceToSegmentKm(p, a, b)).toBeLessThan(11);
|
||
});
|
||
|
||
it('measures to the end, not to the infinite line, for a point beyond it', () => {
|
||
const beyond: LatLng = { lat: 52, lng: 16 };
|
||
const d = distanceToSegmentKm(beyond, a, b);
|
||
// Two degrees of longitude past the end at 52° north — roughly 137 km.
|
||
expect(d).toBeGreaterThan(120);
|
||
expect(d).toBeLessThan(150);
|
||
});
|
||
|
||
it('falls back to point distance for a zero-length segment', () => {
|
||
expect(distanceToSegmentKm(DRESDEN, a, a)).toBeCloseTo(haversineKm(DRESDEN, a), 5);
|
||
});
|
||
});
|
||
|
||
describe('projectOntoRoute', () => {
|
||
const line = [BERLIN, DRESDEN, PRAGUE];
|
||
|
||
it('reports how far off the route a point is', () => {
|
||
const onRoute = projectOntoRoute({ lat: 51.8, lng: 13.5 }, line);
|
||
expect(onRoute!.offRouteKm).toBeLessThan(15);
|
||
const wayOff = projectOntoRoute({ lat: 53.55, lng: 10.0 }, line);
|
||
expect(wayOff!.offRouteKm).toBeGreaterThan(150);
|
||
});
|
||
|
||
it('orders points by how far into the drive they come', () => {
|
||
const early = projectOntoRoute({ lat: 52.3, lng: 13.45 }, line)!;
|
||
const late = projectOntoRoute({ lat: 50.4, lng: 14.2 }, line)!;
|
||
expect(early.alongKm).toBeLessThan(late.alongKm);
|
||
});
|
||
|
||
it('tells two points on the same long straight apart', () => {
|
||
// The petrol stations along one motorway segment must not all report the same
|
||
// position in the drive, which is what taking the segment midpoint produced.
|
||
const straight = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 52, lng: 15 },
|
||
];
|
||
const early = projectOntoRoute({ lat: 52.01, lng: 13.3 }, straight)!;
|
||
const late = projectOntoRoute({ lat: 52.01, lng: 14.7 }, straight)!;
|
||
expect(late.alongKm - early.alongKm).toBeGreaterThan(80);
|
||
});
|
||
|
||
it('returns nothing for a line that is not one', () => {
|
||
expect(projectOntoRoute(BERLIN, [])).toBeNull();
|
||
expect(projectOntoRoute(BERLIN, [BERLIN])).toBeNull();
|
||
});
|
||
|
||
describe('within a stretch of the drive', () => {
|
||
// Out along a straight road and back over it: every point of the road is on the
|
||
// line twice, once on the way out and once on the way back.
|
||
const out = { lat: 52, lng: 13 };
|
||
const tip = { lat: 52, lng: 14 };
|
||
const outAndBack = [out, tip, out];
|
||
const onRoad = { lat: 52, lng: 13.3 };
|
||
|
||
it('answers with the first pass for the whole line', () => {
|
||
const whole = projectOntoRoute(onRoad, outAndBack)!;
|
||
const oneWay = haversineKm(out, tip);
|
||
expect(whole.offRouteKm).toBeLessThan(0.01);
|
||
expect(whole.alongKm).toBeLessThan(oneWay / 2);
|
||
});
|
||
|
||
it('finds the second pass when asked for the way back', () => {
|
||
const oneWay = haversineKm(out, tip);
|
||
const back = projectOntoRoute(onRoad, outAndBack, { fromKm: oneWay, toKm: 2 * oneWay })!;
|
||
expect(back.offRouteKm).toBeLessThan(0.01);
|
||
expect(back.alongKm).toBeGreaterThan(oneWay);
|
||
expect(back.alongKm).toBeCloseTo(2 * oneWay - haversineKm(out, onRoad), 1);
|
||
});
|
||
|
||
it('measures to the edge of the stretch when the point lies beyond it', () => {
|
||
const straight = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 52, lng: 15 },
|
||
];
|
||
const length = haversineKm(straight[0]!, straight[1]!);
|
||
const hit = projectOntoRoute({ lat: 52, lng: 13.1 }, straight, { fromKm: length / 2, toKm: length })!;
|
||
expect(hit.alongKm).toBeCloseTo(length / 2, 3);
|
||
expect(hit.offRouteKm).toBeGreaterThan(55);
|
||
});
|
||
|
||
it('answers nothing for a stretch past the end of the line', () => {
|
||
expect(projectOntoRoute(onRoad, outAndBack, { fromKm: 10_000, toKm: 20_000 })).toBeNull();
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('corridorTiles', () => {
|
||
it('covers a short route with a single box', () => {
|
||
const tiles = corridorTiles([BERLIN, { lat: 52.4, lng: 13.5 }], 5);
|
||
expect(tiles).toHaveLength(1);
|
||
expect(tiles[0]!.south).toBeLessThan(52.4);
|
||
expect(tiles[0]!.north).toBeGreaterThan(52.52);
|
||
});
|
||
|
||
it('splits a long route into boxes the server will not clamp', () => {
|
||
const tiles = corridorTiles([BERLIN, DRESDEN, PRAGUE], 10);
|
||
expect(tiles.length).toBeGreaterThan(1);
|
||
for (const t of tiles) {
|
||
expect(t.north - t.south).toBeLessThanOrEqual(0.45);
|
||
expect(t.east - t.west).toBeLessThanOrEqual(0.45);
|
||
}
|
||
});
|
||
|
||
it('pads longitude more the further north it goes', () => {
|
||
const south = corridorTiles([{ lat: 0, lng: 0 }], 10)[0]!;
|
||
const north = corridorTiles([{ lat: 60, lng: 0 }], 10)[0]!;
|
||
expect(north.east - north.west).toBeGreaterThan(south.east - south.west);
|
||
});
|
||
|
||
it('covers the middle of a long straight leg, not only its two ends', () => {
|
||
// A motorway with no bend in it reduces to two points, 250 km apart. Tiling walks
|
||
// point to point, so without splitting the step everything between them goes
|
||
// unsearched while the search still reports itself complete.
|
||
const straight = [
|
||
{ lat: 53.55, lng: 9.99 },
|
||
{ lat: 52.52, lng: 13.4 },
|
||
];
|
||
const tiles = corridorTiles(straight, 10);
|
||
|
||
expect(tiles.length).toBeGreaterThan(4);
|
||
// A box somewhere over the middle of the drive, which is what was missing.
|
||
const midLat = 53.03;
|
||
const midLng = 11.7;
|
||
expect(tiles.some((t) => t.south <= midLat && t.north >= midLat && t.west <= midLng && t.east >= midLng)).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
it('leaves no gap between consecutive boxes along a route', () => {
|
||
const tiles = corridorTiles(
|
||
[
|
||
{ lat: 53.55, lng: 9.99 },
|
||
{ lat: 52.52, lng: 13.4 },
|
||
],
|
||
10,
|
||
);
|
||
for (let i = 1; i < tiles.length; i++) {
|
||
const a = tiles[i - 1]!;
|
||
const b = tiles[i]!;
|
||
// Overlapping or touching in both axes: a hole here is a stretch nobody looks at.
|
||
expect(Math.min(a.north, b.north) - Math.max(a.south, b.south)).toBeGreaterThanOrEqual(0);
|
||
expect(Math.min(a.east, b.east) - Math.max(a.west, b.west)).toBeGreaterThanOrEqual(0);
|
||
}
|
||
});
|
||
|
||
it('has nothing to cover for an empty route', () => {
|
||
expect(corridorTiles([], 10)).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('simplifyLine', () => {
|
||
it('keeps the ends and drops what sits on the line between them', () => {
|
||
const straight = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 52, lng: 13.5 },
|
||
{ lat: 52, lng: 14 },
|
||
];
|
||
expect(simplifyLine(straight, 1)).toEqual([straight[0]!, straight[2]!]);
|
||
});
|
||
|
||
it('keeps a point that actually bends the route', () => {
|
||
const bent = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 52.5, lng: 13.5 },
|
||
{ lat: 52, lng: 14 },
|
||
];
|
||
expect(simplifyLine(bent, 1)).toHaveLength(3);
|
||
});
|
||
|
||
it('leaves a two-point line alone', () => {
|
||
expect(simplifyLine([BERLIN, DRESDEN], 5)).toEqual([BERLIN, DRESDEN]);
|
||
});
|
||
});
|
||
|
||
describe('pointAtMeters', () => {
|
||
// A straight run due east along one parallel, so the arithmetic is checkable by hand:
|
||
// at 52° north a degree of longitude is about 68.5 km.
|
||
const WEST: LatLng = { lat: 52, lng: 13 };
|
||
const EAST: LatLng = { lat: 52, lng: 14 };
|
||
const LEG = haversineKm(WEST, EAST) * 1000;
|
||
|
||
it('lands halfway along a single segment', () => {
|
||
const mid = pointAtMeters([WEST, EAST], LEG / 2)!;
|
||
expect(mid.lat).toBeCloseTo(52, 5);
|
||
expect(mid.lng).toBeCloseTo(13.5, 3);
|
||
});
|
||
|
||
it('walks past the first segment into the second', () => {
|
||
const line = [WEST, EAST, { lat: 52, lng: 15 }];
|
||
const at = pointAtMeters(line, LEG * 1.5)!;
|
||
// One and a half segments in, so halfway through the second.
|
||
expect(at.lng).toBeGreaterThan(14);
|
||
expect(at.lng).toBeLessThan(15);
|
||
});
|
||
|
||
it('returns the last point past the end rather than nothing', () => {
|
||
// A tank that runs out after the day's final stop still ran out somewhere; the
|
||
// caller decides what to do about that, so this must not answer null.
|
||
expect(pointAtMeters([WEST, EAST], LEG * 10)).toEqual(EAST);
|
||
});
|
||
|
||
it('answers the first point for zero, and nothing for an empty line', () => {
|
||
expect(pointAtMeters([WEST, EAST], 0)).toEqual(WEST);
|
||
expect(pointAtMeters([WEST, EAST], -5)).toEqual(WEST);
|
||
expect(pointAtMeters([], 100)).toBeNull();
|
||
expect(pointAtMeters([WEST], 100)).toEqual(WEST);
|
||
});
|
||
|
||
it('survives a repeated vertex without dividing by zero', () => {
|
||
// A route line concatenated from several runs does contain these.
|
||
const at = pointAtMeters([WEST, WEST, EAST], LEG / 2)!;
|
||
expect(at.lng).toBeCloseTo(13.5, 3);
|
||
});
|
||
});
|
||
|
||
describe('boxAround', () => {
|
||
it('reaches the asked radius on all four sides', () => {
|
||
const box = boxAround(BERLIN, 10);
|
||
// North and south are the easy ones: a degree of latitude is constant.
|
||
expect(haversineKm(BERLIN, { lat: box.north, lng: BERLIN.lng })).toBeCloseTo(10, 0);
|
||
expect(haversineKm(BERLIN, { lat: BERLIN.lat, lng: box.east })).toBeCloseTo(10, 0);
|
||
});
|
||
|
||
it('widens the longitude span the further north it sits', () => {
|
||
// The same radius is more degrees of longitude in Tromsø than in Berlin.
|
||
const berlin = boxAround(BERLIN, 10);
|
||
const tromso = boxAround({ lat: 69.65, lng: 18.96 }, 10);
|
||
expect(tromso.east - tromso.west).toBeGreaterThan(berlin.east - berlin.west);
|
||
});
|
||
});
|
||
|
||
describe('sliceAtMeters', () => {
|
||
// A degree of latitude is about 111 km, so this line is roughly 111 km long and every
|
||
// figure below can be read off it without a calculator.
|
||
const line: LatLng[] = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 53, lng: 13 },
|
||
];
|
||
|
||
it('cuts a stretch out of the middle and interpolates both ends', () => {
|
||
const out = sliceAtMeters(line, 30_000, 60_000);
|
||
expect(out).toHaveLength(2);
|
||
expect(haversineKm(out[0]!, out[1]!) * 1000).toBeGreaterThan(29_000);
|
||
expect(haversineKm(out[0]!, out[1]!) * 1000).toBeLessThan(31_000);
|
||
});
|
||
|
||
it('two adjacent slices meet exactly, so the parts rebuild the whole', () => {
|
||
// The reason this exists: a day's line is its legs end to end, and a night drive
|
||
// takes its own leg to the next card. A gap at the join would draw as a break in
|
||
// the road.
|
||
const first = sliceAtMeters(line, 0, 40_000);
|
||
const second = sliceAtMeters(line, 40_000, 80_000);
|
||
const end = first[first.length - 1]!;
|
||
expect(haversineKm(end, second[0]!)).toBeLessThan(0.001);
|
||
});
|
||
|
||
it('clamps to the line rather than running past its end', () => {
|
||
const out = sliceAtMeters(line, 0, 999_000_000);
|
||
expect(out[out.length - 1]!).toEqual(line[line.length - 1]!);
|
||
});
|
||
|
||
it('is empty for a zero-length or backwards range', () => {
|
||
expect(sliceAtMeters(line, 50_000, 50_000)).toEqual([]);
|
||
expect(sliceAtMeters(line, 60_000, 30_000)).toEqual([]);
|
||
});
|
||
|
||
it('is empty for a line that is not one', () => {
|
||
expect(sliceAtMeters([], 0, 100)).toEqual([]);
|
||
expect(sliceAtMeters([{ lat: 52, lng: 13 }], 0, 100)).toEqual([]);
|
||
});
|
||
|
||
it('survives a repeated vertex without producing NaN', () => {
|
||
// A concatenated route line really does contain these, and dividing by a zero-length
|
||
// segment is how a route full of NaN reaches the map.
|
||
const doubled: LatLng[] = [
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 52, lng: 13 },
|
||
{ lat: 53, lng: 13 },
|
||
];
|
||
const out = sliceAtMeters(doubled, 10_000, 20_000);
|
||
expect(out.every((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('a ride in the corridor (#2428)', () => {
|
||
// A drive north along one meridian, a flight on to the next terminal, and a short drive
|
||
// after it: a degree of latitude is about 111 km, so every figure reads off the line.
|
||
const ORIGIN: LatLng = { lat: 52, lng: 13 };
|
||
const DEPARTURE: LatLng = { lat: 53, lng: 13 };
|
||
const ARRIVAL: LatLng = { lat: 55, lng: 13 };
|
||
const HOTEL: LatLng = { lat: 55.5, lng: 13 };
|
||
const spine = [ORIGIN, DEPARTURE, ARRIVAL, HOTEL];
|
||
const underTheFlight: LatLng = { lat: 54, lng: 13 };
|
||
|
||
const terminal = (
|
||
role: 'departure' | 'arrival' | 'pickup' | 'return',
|
||
at: LatLng,
|
||
reservationId = 7,
|
||
): Pick<RoadtripStop, 'carrier' | 'lat' | 'lng'> => ({
|
||
carrier: {
|
||
reservationId,
|
||
type: role === 'pickup' || role === 'return' ? 'car' : 'flight',
|
||
role,
|
||
title: '',
|
||
code: null,
|
||
at: null,
|
||
},
|
||
...at,
|
||
});
|
||
|
||
it('a departure followed by the arrival of its own booking is a gap, at the departure index', () => {
|
||
const stops = [ORIGIN, terminal('departure', DEPARTURE), terminal('arrival', ARRIVAL), HOTEL];
|
||
expect(rideGaps(stops)).toEqual([{ index: 1, from: DEPARTURE, to: ARRIVAL }]);
|
||
});
|
||
|
||
it('a lone departure, another booking’s arrival and a hire car’s desks make no gap', () => {
|
||
// Landing tomorrow, the arrival is on another day; the drive between a return desk
|
||
// and a pick-up desk IS driven, whatever booking the two belong to.
|
||
expect(rideGaps([ORIGIN, terminal('departure', DEPARTURE)])).toEqual([]);
|
||
expect(rideGaps([terminal('departure', DEPARTURE, 7), terminal('arrival', ARRIVAL, 8)])).toEqual([]);
|
||
expect(rideGaps([terminal('pickup', DEPARTURE), terminal('return', ARRIVAL)])).toEqual([]);
|
||
expect(rideGaps([])).toEqual([]);
|
||
});
|
||
|
||
it('projects both terminals onto the spine and keeps the stretches in order', () => {
|
||
const [ride] = riddenRanges(spine, [{ from: DEPARTURE, to: ARRIVAL }]);
|
||
expect(ride!.fromKm).toBeCloseTo(111, -1);
|
||
expect(ride!.toKm).toBeCloseTo(333, -1);
|
||
// Handed in backwards, or twice over, the stretches still read from the start.
|
||
const two = riddenRanges(spine, [
|
||
{ from: ARRIVAL, to: HOTEL },
|
||
{ from: ARRIVAL, to: DEPARTURE },
|
||
]);
|
||
expect(two.map((r) => Math.round(r.fromKm))).toEqual([111, 334]);
|
||
// A ride the spine cannot place, or whose ends land on the same point, is no stretch.
|
||
expect(riddenRanges([ORIGIN], [{ from: DEPARTURE, to: ARRIVAL }])).toEqual([]);
|
||
expect(riddenRanges(spine, [{ from: DEPARTURE, to: DEPARTURE }])).toEqual([]);
|
||
});
|
||
|
||
it('a hit under the flight is inside the ride, the terminals themselves are not', () => {
|
||
const ridden = riddenRanges(spine, [{ from: DEPARTURE, to: ARRIVAL }]);
|
||
const along = (p: LatLng): number => projectOntoRoute(p, spine)!.alongKm;
|
||
expect(inRiddenRange(ridden, along(underTheFlight))).toBe(true);
|
||
expect(inRiddenRange(ridden, along(DEPARTURE))).toBe(false);
|
||
expect(inRiddenRange(ridden, along(ARRIVAL))).toBe(false);
|
||
expect(inRiddenRange(ridden, along(ORIGIN))).toBe(false);
|
||
expect(inRiddenRange([], along(underTheFlight))).toBe(false);
|
||
});
|
||
|
||
it('cuts the line into the driven pieces, and no box is asked for under the flight', () => {
|
||
const ridden = riddenRanges(spine, [{ from: DEPARTURE, to: ARRIVAL }]);
|
||
const pieces = drivenPieces(spine, ridden);
|
||
expect(pieces).toHaveLength(2);
|
||
expect(pieces[0]![0]).toEqual(ORIGIN);
|
||
expect(pieces[0]![pieces[0]!.length - 1]!.lat).toBeCloseTo(DEPARTURE.lat, 3);
|
||
expect(pieces[1]![0]!.lat).toBeCloseTo(ARRIVAL.lat, 3);
|
||
expect(pieces[1]![pieces[1]!.length - 1]).toEqual(HOTEL);
|
||
|
||
const covers = (tiles: ReturnType<typeof corridorTiles>, p: LatLng): boolean =>
|
||
tiles.some((t) => t.south <= p.lat && t.north >= p.lat && t.west <= p.lng && t.east >= p.lng);
|
||
const tiles = pieces.flatMap((piece) => corridorTiles(piece, 5));
|
||
expect(covers(tiles, underTheFlight)).toBe(false);
|
||
expect(covers(tiles, { lat: 52.5, lng: 13 })).toBe(true);
|
||
// The same line straight through is what the search asked for before.
|
||
expect(covers(corridorTiles(spine, 5), underTheFlight)).toBe(true);
|
||
});
|
||
|
||
it('without a ride the whole line is the one piece, and a window is a slice of it', () => {
|
||
expect(drivenPieces(spine, [])).toEqual([spine]);
|
||
expect(drivenPieces(spine, [])[0]).toBe(spine);
|
||
const [windowed] = drivenPieces(spine, [], { fromKm: 0, toKm: 50 });
|
||
expect(windowed![windowed!.length - 1]!.lat).toBeCloseTo(52.45, 1);
|
||
expect(drivenPieces([ORIGIN], [])).toEqual([]);
|
||
});
|
||
|
||
it('a window and a ride together leave the driven part of the window only', () => {
|
||
const ridden = riddenRanges(spine, [{ from: DEPARTURE, to: ARRIVAL }]);
|
||
const pieces = drivenPieces(spine, ridden, { fromKm: 100, toKm: 400 });
|
||
expect(pieces).toHaveLength(2);
|
||
expect(pieces[0]![0]!.lat).toBeCloseTo(52.9, 1);
|
||
expect(pieces[0]![pieces[0]!.length - 1]!.lat).toBeCloseTo(53, 2);
|
||
expect(pieces[1]![0]!.lat).toBeCloseTo(55, 2);
|
||
expect(pieces[1]![pieces[1]!.length - 1]).toEqual(HOTEL);
|
||
// A window that ends before the ride never reaches it; one lying inside it is empty.
|
||
expect(drivenPieces(spine, ridden, { fromKm: 0, toKm: 50 })).toHaveLength(1);
|
||
expect(drivenPieces(spine, ridden, { fromKm: 150, toKm: 300 })).toEqual([]);
|
||
});
|
||
});
|