mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +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
6.1 KiB
TypeScript
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
|
|
);
|
|
}
|