From 49dd2db04037b986e517acbb8109e9a1642b10dc Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 20 Oct 2026 18:26:00 +0200 Subject: [PATCH] refactor(planner): project a day's tours by assignment in one place The day route plan and the day plan sidebar both built the map of a day's stops projected onto their tours, each stage on its own stretch, by hand. projectedById in dayTourProjection builds it for both, and the sidebar's drive runs reuse the route plan's point type instead of spelling it out twice. --- client/src/components/Map/dayRoutePlan.ts | 4 ++-- client/src/components/Map/dayTourProjection.ts | 10 ++++++++++ client/src/components/Planner/DayPlanSidebar.tsx | 9 +++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/src/components/Map/dayRoutePlan.ts b/client/src/components/Map/dayRoutePlan.ts index a7227a58b..07f0079a7 100644 --- a/client/src/components/Map/dayRoutePlan.ts +++ b/client/src/components/Map/dayRoutePlan.ts @@ -3,7 +3,7 @@ import { getTransportRouteEndpoints, getTransportForDay, getMergedItems, isCarri import { getDayBookendHotels, shouldDrawMorningLeg, shouldDrawEveningLeg, type CarrierEdge } from '../../utils/dayOrder' import { withinDriveRange } from '@trek/shared/roadtrip' import type { Accommodation, AssignmentsMap, Day, Place, Reservation } from '../../types' -import { projectDayItinerary, type TourStageStops } from './dayTourProjection' +import { projectedById, type TourStageStops } from './dayTourProjection' export const TRANSPORT_TYPES = ['flight', 'train', 'bus', 'car', 'taxi', 'bicycle', 'cruise', 'ferry', 'cable_car', 'transit', 'transport_other'] @@ -85,7 +85,7 @@ export function buildDayRouteRuns(dayId: number, input: DayRouteInputs): DayRout days: allDays, }) const merged = getMergedItems({ dayAssignments: da, dayNotes: [], dayTransports, dayId }) - const projected = new Map(projectDayItinerary(da, input.toursEnabled ?? false, input.places, input.tourStageStops).map(item => [item.assignment.id, item])) + const projected = projectedById(da, input.toursEnabled ?? false, input.places, input.tourStageStops) type Entry = | { kind: 'place'; id: number; lat: number; lng: number; pos: number; time: string | null; mode: string | null; incoming: string | null } diff --git a/client/src/components/Map/dayTourProjection.ts b/client/src/components/Map/dayTourProjection.ts index cf137dea3..1d5f4a3cd 100644 --- a/client/src/components/Map/dayTourProjection.ts +++ b/client/src/components/Map/dayTourProjection.ts @@ -85,4 +85,14 @@ export function projectDayItinerary( loop: !!start && !!end && start.lat === end.lat && start.lng === end.lng, valid: !!end } as const }) +} + +/** The day's stops projected as `projectDayItinerary` does, by assignment id, for the route that runs through them. */ +export function projectedById( + assignments: Assignment[], + toursEnabled: boolean, + places?: readonly Place[], + stageStops?: TourStageStops, +): Map { + return new Map(projectDayItinerary(assignments, toursEnabled, places, stageStops).map(item => [item.assignment.id, item])) } \ No newline at end of file diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index f40e19bd2..f02c09473 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -69,7 +69,8 @@ import { RouteConnector, HotelRouteConnector } from './DayPlanSidebarRouteConnec import { resolveLegMode } from './legMode' import { useLegModeActions } from './useLegModeActions' import { dayExportStops, fillAroundLocked } from './dayRoute' -import { projectDayItinerary, type TourStageStops } from '../Map/dayTourProjection' +import { projectDayItinerary, projectedById, type TourStageStops } from '../Map/dayTourProjection' +import type { DayRoutePoint } from '../Map/dayRoutePlan' import { usePluginDaySchedule, usePluginDayTints, dayTintBackground, dayTinted, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule' import { MobileAddPlaceButton } from './DayPlanSidebarMobileAddPlaceButton' import { DayPlanSidebarToolbar } from './DayPlanSidebarToolbar' @@ -623,12 +624,12 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) { // legs to draw. Side-effect free, so the async loop below only does OSRM I/O. const planDay = (dayId: number) => { const merged = mergedItemsMap[dayId] || [] - const projected = new Map(projectDayItinerary(assignments[String(dayId)] || [], toursEnabled, places, props.tourStageStops).map(item => [item.assignment.id, item])) + const projected = projectedById(assignments[String(dayId)] || [], toursEnabled, places, props.tourStageStops) // Each run point carries the ORIGIN assignment's per-segment travel mode // (#1281): the leg from this point to the next is routed with `mode` (null = // inherit the day default). Transport endpoints carry no mode → day default. - const runs: { id: number; lat: number; lng: number; isPlace: boolean; leg_transport_mode?: string | null; incoming_leg_transport_mode?: string | null; assignmentId?: number }[][] = [] - let cur: { id: number; lat: number; lng: number; isPlace: boolean; leg_transport_mode?: string | null; incoming_leg_transport_mode?: string | null; assignmentId?: number }[] = [] + const runs: (DayRoutePoint & { id: number })[][] = [] + let cur: (DayRoutePoint & { id: number })[] = [] // A run is only a real drive when it holds an actual place. Two back-to-back // transports (e.g. two flights on one day) would otherwise pair the first's // arrival with the second's departure into a phantom airport→airport leg — the