Files
TREK/client/src/components/Planner/dayRoute.ts
T
Maurice ae31b55441 refactor(planner): share the day route export and optimize slots between desktop and phone
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.
2026-10-09 07:16:40 +00:00

181 lines
6.1 KiB
TypeScript

import type { Accommodation, Assignment, Day } from '../../types';
import {
getAccommodationAnchors,
getDayBookendHotels,
shouldDrawEveningLeg,
shouldDrawMorningLeg,
} from '../../utils/dayOrder';
import {
generateCoMapsUrl,
generateGoogleMapsUrl,
optimizeRoute,
type NamedWaypoint,
type RouteProfileKey,
} from '../Map/RouteCalculator';
/**
* Day-route helpers shared by the desktop day plan, the phone's plan timeline and
* its day sheet: the exact desktop rules (timed places stay anchored, hotel
* bookends only when the leg is real, #1372/#1465), in one place so the surfaces
* cannot drift apart.
*/
export interface OptimizedDay {
order: Assignment[];
/** True when an accommodation anchored the start or end of the route. */
usedHotel: boolean;
}
/**
* Optimized order for a day's assignments: timed places keep their slot, the
* remaining located stops are rerouted (optionally anchored to the day's
* accommodation) and stops without coordinates fill the leftover slots.
* Null when fewer than two movable located stops exist: nothing to optimize.
*/
export function optimizeDayOrder(
day: Day,
days: Day[],
dayAssignments: Assignment[],
accommodations: Accommodation[],
fromAccommodation: boolean,
dayHasCarrier?: boolean
): OptimizedDay | null {
const locked = new Map<number, Assignment>();
const movable: Assignment[] = [];
dayAssignments.forEach((a, idx) => {
if (a.place?.place_time) locked.set(idx, a);
else movable.push(a);
});
const withCoords = movable.filter((a) => a.place?.lat != null && a.place?.lng != null);
if (withCoords.length < 2) return null;
const noCoords = movable.filter((a) => a.place?.lat == null || a.place?.lng == null);
const anchors = fromAccommodation
? getAccommodationAnchors(
day,
days,
accommodations,
withCoords.map((a) => ({ lat: a.place!.lat!, lng: a.place!.lng! })),
dayHasCarrier
)
: {};
const optimized = optimizeRoute(
withCoords.map((a) => ({ lat: a.place!.lat!, lng: a.place!.lng!, _assignmentId: a.id })),
anchors
)
.map((p) => withCoords.find((a) => a.id === p._assignmentId)!)
.filter(Boolean);
const order = fillAroundLocked(dayAssignments.length, locked, [...optimized, ...noCoords]);
return { order, usedHotel: Boolean(anchors.start || anchors.end) };
}
/**
* A day's new order: the locked entries stay at their index and the queue fills
* the free slots, in its own order.
*/
export function fillAroundLocked<T>(length: number, locked: Map<number, T>, queue: T[]): T[] {
const order: T[] = new Array(length);
locked.forEach((a, idx) => {
order[idx] = a;
});
let qi = 0;
for (let i = 0; i < order.length; i++) {
if (!order[i]) order[i] = queue[qi++];
}
return order;
}
/**
* What the day's carrier bookings (flight, train, ferry, coach) tell the
* exports. `located`: one has an endpoint with coordinates on the day, which
* keeps the no-time bookend default from opening a leg out of a hotel only
* reached tonight or back into one already left (#2157), same as the drawn
* route. `booked`: one is booked on the day at all, located or not, which
* leaves a day without stops with nothing to export: the hotels at either end
* are joined by that booking, not by a road (#2476).
*/
export interface DayCarrier {
located?: boolean;
booked?: boolean;
}
/**
* The day's located stops in planned order, bookended by the morning/evening
* accommodation exactly like the drawn route. Names ride along for the deep
* links that can label a pin with one. See DayCarrier for what the day's
* carrier bookings change.
*/
export function dayExportStops(
day: Day,
days: Day[],
dayAssignments: Assignment[],
accommodations: Accommodation[],
bookendFromAccommodation: boolean,
carrier: DayCarrier = {}
): NamedWaypoint[] {
const located = dayAssignments.filter((a) => a.place?.lat != null && a.place?.lng != null);
if (located.length === 0 && carrier.booked) return [];
const stops = located.map((a) => ({ lat: a.place!.lat!, lng: a.place!.lng!, name: a.place!.name }));
const bookends = bookendFromAccommodation ? getDayBookendHotels(day, days, accommodations) : null;
const firstStop = located[0]
? {
isPlace: true,
time: located[0].place?.place_time ?? null,
lat: located[0].place!.lat!,
lng: located[0].place!.lng!,
}
: undefined;
const last = located[located.length - 1];
const lastStop = last
? { isPlace: true, time: last.place?.place_time ?? null, lat: last.place!.lat!, lng: last.place!.lng! }
: undefined;
const morning =
bookends &&
shouldDrawMorningLeg(bookends, day, firstStop, carrier.located) &&
bookends.morning?.place_lat != null &&
bookends.morning?.place_lng != null
? { lat: bookends.morning.place_lat, lng: bookends.morning.place_lng, name: bookends.morning.place_name }
: null;
const evening =
bookends &&
shouldDrawEveningLeg(bookends, day, lastStop, carrier.located) &&
bookends.evening?.place_lat != null &&
bookends.evening?.place_lng != null
? { lat: bookends.evening.place_lat, lng: bookends.evening.place_lng, name: bookends.evening.place_name }
: null;
return [...(morning ? [morning] : []), ...stops, ...(evening ? [evening] : [])];
}
/** Google-Maps directions URL over the day's bookended stops. */
export function dayGoogleMapsUrl(
day: Day,
days: Day[],
dayAssignments: Assignment[],
accommodations: Accommodation[],
bookendFromAccommodation: boolean,
carrier: DayCarrier = {}
): string | null {
return (
generateGoogleMapsUrl(
dayExportStops(day, days, dayAssignments, accommodations, bookendFromAccommodation, carrier)
) || null
);
}
/** The same stops handed to CoMaps for offline navigation, in the day's travel mode (#1904). */
export function dayCoMapsUrl(
day: Day,
days: Day[],
dayAssignments: Assignment[],
accommodations: Accommodation[],
bookendFromAccommodation: boolean,
profile: RouteProfileKey,
carrier: DayCarrier = {}
): string | null {
return (
generateCoMapsUrl(
dayExportStops(day, days, dayAssignments, accommodations, bookendFromAccommodation, carrier),
profile
) || null
);
}