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(); 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(length: number, locked: Map, 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 ); }