mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
69d2f80fe2
ReservationOverlay and reservationsMapbox each carried the booking type list, the icon and arc table, the time zone aware duration and the loop that turns a booking into waypoints, arcs and labels. All of that now lives in transportHops.ts; each renderer keeps only its drawing. The one difference stays a parameter: Leaflet asks for arcs that cross the antimeridian to be copied 360 degrees over, the GL map does not because it repeats features across world copies itself. Leaflet still adds its transit segments on top of the shared hop.
144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { Reservation, ReservationEndpoint } from '../../types';
|
|
import { TRANSPORT_META, transportHop } from './transportHops';
|
|
|
|
function endpoint(over: Partial<ReservationEndpoint>): ReservationEndpoint {
|
|
return {
|
|
role: 'from',
|
|
sequence: 0,
|
|
name: 'X',
|
|
code: null,
|
|
lat: 0,
|
|
lng: 0,
|
|
timezone: null,
|
|
local_time: null,
|
|
local_date: null,
|
|
...over,
|
|
} as ReservationEndpoint;
|
|
}
|
|
|
|
function booking(type: string, endpoints: ReservationEndpoint[], over: Partial<Reservation> = {}): Reservation {
|
|
return { id: 7, type, status: 'confirmed', endpoints, ...over } as unknown as Reservation;
|
|
}
|
|
|
|
describe('transportHop', () => {
|
|
it('skips a booking that is no transport', () => {
|
|
const r = booking('hotel', [endpoint({ role: 'from' }), endpoint({ role: 'to', sequence: 1 })]);
|
|
expect(transportHop(r, true)).toBeNull();
|
|
});
|
|
|
|
it('skips a transport with fewer than two places to connect', () => {
|
|
expect(transportHop(booking('train', [endpoint({ role: 'from' })]), true)).toBeNull();
|
|
expect(
|
|
transportHop(booking('train', [endpoint({ role: 'from' }), endpoint({ role: 'check_in' as never })]), true)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('orders from, stops and to by sequence and draws one straight leg each for ground transport', () => {
|
|
const r = booking('train', [
|
|
endpoint({ role: 'to', sequence: 2, code: 'HND', lat: 3, lng: 3 }),
|
|
endpoint({ role: 'from', sequence: 0, code: 'FRA', lat: 1, lng: 1 }),
|
|
endpoint({ role: 'stop', sequence: 1, code: 'BER', lat: 2, lng: 2 }),
|
|
]);
|
|
const hop = transportHop(r, true)!;
|
|
expect(hop.waypoints.map((w) => w.code)).toEqual(['FRA', 'BER', 'HND']);
|
|
expect(hop.from.code).toBe('FRA');
|
|
expect(hop.to.code).toBe('HND');
|
|
expect(hop.arcs).toEqual([
|
|
[
|
|
[1, 1],
|
|
[2, 2],
|
|
],
|
|
[
|
|
[2, 2],
|
|
[3, 3],
|
|
],
|
|
]);
|
|
expect(hop.mainLabel).toBe('FRA → BER → HND');
|
|
expect(hop.res).toBe(r);
|
|
expect(hop.type).toBe('train');
|
|
});
|
|
|
|
it('falls back to the two end codes when a stop has none, and to no label without them', () => {
|
|
const withEnds = booking('bus', [
|
|
endpoint({ role: 'from', code: 'A' }),
|
|
endpoint({ role: 'stop', sequence: 1 }),
|
|
endpoint({ role: 'to', sequence: 2, code: 'B' }),
|
|
]);
|
|
expect(transportHop(withEnds, true)!.mainLabel).toBe('A → B');
|
|
const bare = booking('bus', [endpoint({ role: 'from' }), endpoint({ role: 'to', sequence: 1 })]);
|
|
expect(transportHop(bare, true)!.mainLabel).toBeNull();
|
|
});
|
|
|
|
it('puts duration and great-circle distance in the sub label', () => {
|
|
const r = booking('car', [
|
|
endpoint({ role: 'from', lat: 0, lng: 0, local_date: '2026-05-01', local_time: '08:00' }),
|
|
endpoint({ role: 'to', sequence: 1, lat: 0, lng: 1, local_date: '2026-05-01', local_time: '09:30' }),
|
|
]);
|
|
expect(transportHop(r, true)!.subLabel).toBe('1h 30m · 111 km');
|
|
});
|
|
|
|
it('copies a flight arc across the antimeridian only when asked to wrap', () => {
|
|
const r = booking('flight', [
|
|
endpoint({ role: 'from', lat: 35, lng: 170 }),
|
|
endpoint({ role: 'to', sequence: 1, lat: 40, lng: -170 }),
|
|
]);
|
|
expect(transportHop(r, true)!.arcs).toHaveLength(2);
|
|
expect(transportHop(r, false)!.arcs).toHaveLength(1);
|
|
expect(transportHop(r, false)!.arcs[0].length).toBeGreaterThan(2);
|
|
});
|
|
|
|
it('draws ferries and cruises as arcs but a cable car as a straight line', () => {
|
|
expect(TRANSPORT_META.ferry.geodesic).toBe(true);
|
|
expect(TRANSPORT_META.cruise.geodesic).toBe(true);
|
|
expect(TRANSPORT_META.cable_car.geodesic).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('transportHop duration', () => {
|
|
// Both ends sit on the same spot, so the sub label is the duration plus "0 km".
|
|
const at = (date: string | null, time: string | null, timezone: string | null = null) =>
|
|
endpoint({ local_date: date, local_time: time, timezone });
|
|
const subLabel = (
|
|
from: ReservationEndpoint,
|
|
to: ReservationEndpoint,
|
|
start: string | null = null,
|
|
end: string | null = null
|
|
) =>
|
|
transportHop(
|
|
booking(
|
|
'train',
|
|
[
|
|
{ ...from, role: 'from' },
|
|
{ ...to, role: 'to', sequence: 1 },
|
|
],
|
|
{
|
|
reservation_time: start,
|
|
reservation_end_time: end,
|
|
}
|
|
),
|
|
true
|
|
)!.subLabel;
|
|
|
|
it('reads each end in its own time zone', () => {
|
|
expect(subLabel(at('2026-05-01', '10:00', 'Europe/Berlin'), at('2026-05-01', '10:00', 'Europe/London'))).toBe(
|
|
'1h 0m · 0 km'
|
|
);
|
|
});
|
|
|
|
it('rolls an arrival before the departure over to the next day', () => {
|
|
expect(subLabel(at('2026-05-01', '23:30'), at('2026-05-01', '00:15'))).toBe('45m · 0 km');
|
|
});
|
|
|
|
it('borrows the date of the other end for a bare time from the booking', () => {
|
|
expect(subLabel(at(null, null), at('2026-05-01', '12:00'), '11:00')).toBe('1h 0m · 0 km');
|
|
});
|
|
|
|
it('leaves the duration out for missing, malformed or implausible times', () => {
|
|
expect(subLabel(at(null, null), at(null, null))).toBe('0 km');
|
|
expect(subLabel(at(null, null), at(null, null), '10:00', '11:00')).toBe('0 km');
|
|
expect(subLabel(at('2026-05-01', '10', 'Europe/Berlin'), at('2026-05-01', '11:00', 'Europe/Berlin'))).toBe('0 km');
|
|
expect(subLabel(at('2026-05-01', '10:00'), at('2026-05-04', '10:00'))).toBe('0 km');
|
|
});
|
|
});
|