mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
ae31b55441
dayRoute moves from the phone's lib into components/Planner. The desktop day plan builds its Google Maps and CoMaps stops with dayExportStops, and both optimizers put the reordered stops around the fixed ones with fillAroundLocked.
181 lines
8.4 KiB
TypeScript
181 lines
8.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { Accommodation, Assignment, Day } from '../../types';
|
|
import { dayCoMapsUrl, dayExportStops, dayGoogleMapsUrl, fillAroundLocked, optimizeDayOrder } from './dayRoute';
|
|
|
|
// FE-MOBILE-DAYROUTE-001 to FE-MOBILE-DAYROUTE-015
|
|
|
|
const days = [
|
|
{ id: 10, day_number: 1 },
|
|
{ id: 20, day_number: 2 },
|
|
{ id: 30, day_number: 3 },
|
|
] as unknown as Day[];
|
|
|
|
const hotel = {
|
|
place_lat: 48.85,
|
|
place_lng: 2.35,
|
|
place_name: 'Hotel Lutetia',
|
|
start_day_id: 10,
|
|
end_day_id: 30,
|
|
} as unknown as Accommodation;
|
|
|
|
const stop = (id: number, name: string, lat: number, lng: number): Assignment =>
|
|
({ id, day_id: 20, order_index: id, place: { id, name, lat, lng } }) as unknown as Assignment;
|
|
|
|
const louvre = stop(1, 'Louvre', 48.86, 2.34);
|
|
const orsay = stop(2, 'Orsay', 48.87, 2.33);
|
|
|
|
describe('dayExportStops', () => {
|
|
it('FE-MOBILE-DAYROUTE-001: carries the stops in planned order, hotel-bookended and named', () => {
|
|
expect(dayExportStops(days[1], days, [louvre, orsay], [hotel], true)).toEqual([
|
|
{ lat: 48.85, lng: 2.35, name: 'Hotel Lutetia' },
|
|
{ lat: 48.86, lng: 2.34, name: 'Louvre' },
|
|
{ lat: 48.87, lng: 2.33, name: 'Orsay' },
|
|
{ lat: 48.85, lng: 2.35, name: 'Hotel Lutetia' },
|
|
]);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-002: drops the bookends when accommodation optimization is off', () => {
|
|
const stops = dayExportStops(days[1], days, [louvre, orsay], [hotel], false);
|
|
expect(stops.map((s) => s.name)).toEqual(['Louvre', 'Orsay']);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-003: a stop without coordinates cannot be exported', () => {
|
|
const nowhere = { id: 3, day_id: 20, order_index: 3, place: { id: 3, name: 'TBD' } } as unknown as Assignment;
|
|
const stops = dayExportStops(days[1], days, [louvre, nowhere], [], false);
|
|
expect(stops.map((s) => s.name)).toEqual(['Louvre']);
|
|
});
|
|
|
|
// The stay above records neither check-in nor check-out time: the #2157 shape.
|
|
const homeNear = stop(4, 'Home', 48.9, 2.4);
|
|
|
|
it('FE-MOBILE-DAYROUTE-006: with a carrier on the day, the not-yet-reached check-in hotel is not prepended (#2157)', () => {
|
|
// Arrival day: you fly in, so the hotel may end the list but must not start it.
|
|
const stops = dayExportStops(days[0], days, [homeNear], [hotel], true, { located: true });
|
|
expect(stops.map((s) => s.name)).toEqual(['Home', 'Hotel Lutetia']);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-007: with a carrier on the day, the already-left check-out hotel is not appended (#2157)', () => {
|
|
// Check-out day: the hotel starts the day, nothing routes back to it after the flight home.
|
|
const stops = dayExportStops(days[2], days, [homeNear], [hotel], true, { located: true });
|
|
expect(stops.map((s) => s.name)).toEqual(['Hotel Lutetia', 'Home']);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-008: without a carrier the no-time loop still closes (#2009 preserved)', () => {
|
|
const stops = dayExportStops(days[0], days, [louvre], [hotel], true, { located: false });
|
|
expect(stops.map((s) => s.name)).toEqual(['Hotel Lutetia', 'Louvre', 'Hotel Lutetia']);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-009: an edge stop out of drive range keeps the no-time hotel out even without a carrier (#2157)', () => {
|
|
// No flight recorded, but "Home" is an ocean away, so the loop default is a guess
|
|
// and does not get to route back across it.
|
|
const farHome = stop(5, 'Home', 21.28, -157.83);
|
|
const stops = dayExportStops(days[2], days, [farHome], [hotel], true);
|
|
expect(stops.map((s) => s.name)).toEqual(['Hotel Lutetia', 'Home']);
|
|
});
|
|
|
|
// Day 2 checks out of one stay and into the next, with no stop of its own.
|
|
const hotelA = { ...hotel, place_name: 'Hotel A', start_day_id: 10, end_day_id: 20 } as Accommodation;
|
|
const hotelB = {
|
|
...hotel,
|
|
place_lat: 53.55,
|
|
place_lng: 9.99,
|
|
place_name: 'Hotel B',
|
|
start_day_id: 20,
|
|
end_day_id: 30,
|
|
} as Accommodation;
|
|
|
|
it('FE-MOBILE-DAYROUTE-010: a flight between the two stays leaves nothing to export (#2476)', () => {
|
|
// Located or not, the flight is how the day gets from one hotel to the other; a
|
|
// driving route between them is the stretch nobody drives.
|
|
expect(dayExportStops(days[1], days, [], [hotelA, hotelB], true, { located: true, booked: true })).toEqual([]);
|
|
expect(dayExportStops(days[1], days, [], [hotelA, hotelB], true, { booked: true })).toEqual([]);
|
|
expect(dayGoogleMapsUrl(days[1], days, [], [hotelA, hotelB], true, { booked: true })).toBeNull();
|
|
expect(dayCoMapsUrl(days[1], days, [], [hotelA, hotelB], true, 'driving', { booked: true })).toBeNull();
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-011: without a booking the move is still the drive from one stay to the next (#1297)', () => {
|
|
expect(dayExportStops(days[1], days, [], [hotelA, hotelB], true).map((s) => s.name)).toEqual([
|
|
'Hotel A',
|
|
'Hotel B',
|
|
]);
|
|
// A stop of its own keeps the day exportable even with the flight booked.
|
|
expect(
|
|
dayExportStops(days[1], days, [louvre], [hotelA, hotelB], true, { located: true, booked: true }).map(
|
|
(s) => s.name
|
|
)
|
|
).toContain('Louvre');
|
|
});
|
|
});
|
|
|
|
describe('day map links', () => {
|
|
it('FE-MOBILE-DAYROUTE-004: both exports read the same stop list', () => {
|
|
// One source for the bookend rules, so the two links can never disagree
|
|
// about which hotel legs are real.
|
|
const google = dayGoogleMapsUrl(days[1], days, [louvre, orsay], [hotel], true)!;
|
|
const coMaps = dayCoMapsUrl(days[1], days, [louvre, orsay], [hotel], true, 'walking')!;
|
|
for (const ll of ['48.85,2.35', '48.86,2.34', '48.87,2.33']) {
|
|
expect(google).toContain(ll);
|
|
expect(coMaps).toContain(ll);
|
|
}
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-005: the CoMaps link travels in the mode it was given', () => {
|
|
// Four bookended stops is more than a CoMaps route link can hold, so it
|
|
// becomes pins, which is also why no mode appears here.
|
|
expect(dayCoMapsUrl(days[1], days, [louvre, orsay], [hotel], true, 'walking')).toMatch(
|
|
/^https:\/\/comaps\.at\/map\?v=1&/
|
|
);
|
|
// Two stops fit a route, and then the mode does apply.
|
|
expect(dayCoMapsUrl(days[1], days, [louvre, orsay], [], false, 'walking')).toContain('type=pedestrian');
|
|
});
|
|
});
|
|
|
|
describe('optimizeDayOrder', () => {
|
|
const timed = (id: number, name: string, lat: number, lng: number, time: string): Assignment =>
|
|
({ id, day_id: 20, order_index: id, place: { id, name, lat, lng, place_time: time } }) as unknown as Assignment;
|
|
const unlocated = {
|
|
id: 8,
|
|
day_id: 20,
|
|
order_index: 8,
|
|
place: { id: 8, name: 'Somewhere', lat: null, lng: null },
|
|
} as unknown as Assignment;
|
|
const a = stop(3, 'A', 10, 10);
|
|
const b = stop(4, 'B', 10, 30);
|
|
const c = stop(5, 'C', 10, 20);
|
|
|
|
it('FE-MOBILE-DAYROUTE-012: a timed stop keeps its slot and the rest are rerouted around it', () => {
|
|
const breakfast = timed(9, 'Breakfast', 40, 40, '08:00');
|
|
const result = optimizeDayOrder(days[1], days, [breakfast, a, b, c], [], false);
|
|
expect(result?.order.map((x) => x.id)).toEqual([9, 3, 5, 4]);
|
|
expect(result?.usedHotel).toBe(false);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-013: stops without coordinates fill the slots left over at the end', () => {
|
|
const result = optimizeDayOrder(days[1], days, [unlocated, a, b, c], [], false);
|
|
expect(result?.order.map((x) => x.id)).toEqual([3, 5, 4, 8]);
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-014: fewer than two movable located stops leave nothing to optimize', () => {
|
|
expect(optimizeDayOrder(days[1], days, [a, unlocated], [], false)).toBeNull();
|
|
expect(optimizeDayOrder(days[1], days, [timed(9, 'T', 1, 1, '08:00'), a], [], false)).toBeNull();
|
|
});
|
|
|
|
it('FE-MOBILE-DAYROUTE-015: the hotel the day starts and ends at anchors the route when asked to', () => {
|
|
const result = optimizeDayOrder(days[1], days, [louvre, orsay, stop(6, 'Opera', 48.871, 2.332)], [hotel], true);
|
|
expect(result?.usedHotel).toBe(true);
|
|
expect(result?.order.map((x) => x.id).sort()).toEqual([1, 2, 6]);
|
|
expect(optimizeDayOrder(days[1], days, [louvre, orsay], [hotel], false)?.usedHotel).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('fillAroundLocked', () => {
|
|
it('FE-PLANNER-DAYROUTE-001: locked entries stay at their index and the queue fills the gaps in order', () => {
|
|
const locked = new Map([
|
|
[1, 'locked-1'],
|
|
[3, 'locked-3'],
|
|
]);
|
|
expect(fillAroundLocked(5, locked, ['q1', 'q2', 'q3'])).toEqual(['q1', 'locked-1', 'q2', 'locked-3', 'q3']);
|
|
expect(fillAroundLocked(2, new Map<number, string>(), ['x', 'y'])).toEqual(['x', 'y']);
|
|
});
|
|
});
|