mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
d3da6ca08d
"Public transit" on the connector between two city walks in the day plan, on the desktop and on the phone, now opens the existing search from where the first walk really ends to where the next one starts, at the coordinates the day's route draws, named after the first walk's last stop and the next walk's first stop. A loop ends at its first stop again. A stop without a name takes the name of the trip place it stands for, and a walk whose stops cannot be read on this device names that end "End of" or "Start of" the walk. The search opens on the walks' day and departs when the first walk ends: its start that day, the time on the day's entry before the place's own, plus its planned total, a total entered by hand before walking and stays. The assignment's own end time is not used. Above the search a note says when the first walk ends, "at the earliest" where a stay or the walking time is not known yet, and when the next walk starts. Without a start or a total no time is made up: the time field stays empty and Search waits until one is chosen. A walk that only ends after midnight says so, and the time field stays empty as well. On any other day, or for any other leg, the search opens as before, with 09:00 as its default. A connection that arrives after the next walk starts says so in its card, before its legs are opened and before it is taken. Nothing is saved until "Add to day", which keeps the connection as its own journey in the plan as before; both walks stay two walks on foot and nothing is moved. A journey saved before a walk that now arrives after the walk starts says so on its row in the plan, on the desktop and the phone, while the plan is looked at. Nothing is rewritten. The desktop no longer draws a connector of nothing between a stop and a booking at the same spot, as the phone already did, so a journey saved at the walks' ends adds no walk on either side. A leg leaving a loop tour no longer departs at the tour's start time. The leg is told by the walk it leaves, so two walks of a day that end at one spot, such as two loops from the hotel, each search from their own end and time, and a stop between two walks keeps the leg an ordinary one. Only the leg asked for last opens the search, and none opens once the plan is left while the walks' stops are still being read. Both views run on one shared hook, the walk's stops are read through one helper the stop list uses as well, and the walk's start on a day is worked out in one place for the times shown in the detail and here.
103 lines
4.3 KiB
TypeScript
103 lines
4.3 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
import { useTranslation } from '../../i18n';
|
|
import type { Accommodation, AssignmentsMap, Day, Place, Reservation, RouteSegment } from '../../types';
|
|
import type { MergedItem } from '../../utils/dayMerge';
|
|
import { projectDayItinerary } from '../Map/dayTourProjection';
|
|
import { readWalkStations } from '../Tours/walk/walkStationsRead';
|
|
import { journeyLate, walkPairAt, walkTransitLeg, type TourOf, type WalkLate } from '../Tours/walk/walkTransit';
|
|
import { buildTransitLeg, buildTransitNameIndex, type PlannedTransitLeg, type TransitLeg } from './transitLeg';
|
|
|
|
export interface TransitLegPlannerDeps {
|
|
tripId: number | string;
|
|
assignments: AssignmentsMap;
|
|
accommodations: Accommodation[];
|
|
reservations: Reservation[];
|
|
places: Place[];
|
|
toursEnabled: boolean;
|
|
tourOf: TourOf;
|
|
/** Opens the search for a leg; without it no leg offers one, as on a trip without dates. */
|
|
onPlan: ((leg: PlannedTransitLeg) => void) | undefined;
|
|
}
|
|
|
|
/** A walk's stops, or null where this device cannot read them. */
|
|
async function stopsOrNull(tripId: number | string, placeId: number, signal: AbortSignal) {
|
|
try {
|
|
return await readWalkStations(tripId, placeId, signal);
|
|
} catch {
|
|
// They only name the leg's two ends: without them it opens named after the walks.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Public transit for one leg of a day (#2398), behind the desktop day plan and
|
|
* the phone timeline alike: the leg's two ends and the time its origin is
|
|
* planned for. Between two city walks (CW-08) it runs from the first walk's
|
|
* last stop to the next one's first, departing when the first one ends, and a
|
|
* saved journey that now arrives after the next walk starts says so. Nothing
|
|
* is saved here: the search opens, and only the connection picked there is
|
|
* added to the day. Only the last leg asked for opens it, and none after the
|
|
* view is gone.
|
|
*/
|
|
export function useTransitLegPlanner({
|
|
tripId,
|
|
assignments,
|
|
accommodations,
|
|
reservations,
|
|
places,
|
|
toursEnabled,
|
|
tourOf,
|
|
onPlan,
|
|
}: TransitLegPlannerDeps) {
|
|
const { t } = useTranslation();
|
|
// Coordinates back to names, over every located place, hotel and booking endpoint of the trip.
|
|
const names = useMemo(
|
|
() => buildTransitNameIndex(assignments, accommodations, reservations),
|
|
[assignments, accommodations, reservations]
|
|
);
|
|
|
|
const legFor = useCallback(
|
|
(dayId: number, seg?: RouteSegment): TransitLeg | null =>
|
|
onPlan ? buildTransitLeg(seg, dayId, names, assignments, reservations) : null,
|
|
[onPlan, names, assignments, reservations]
|
|
);
|
|
|
|
// The walks' stops being read for the leg asked for last; a later leg, or leaving the view, drops them.
|
|
const pending = useRef<AbortController | null>(null);
|
|
useEffect(() => () => pending.current?.abort(), []);
|
|
|
|
/** Opens the search for a leg, told by the entry it leaves where the connector carries one. */
|
|
const plan = useCallback(
|
|
(dayId: number, leg: TransitLeg, originId?: number) => {
|
|
if (!onPlan) return;
|
|
pending.current?.abort();
|
|
const day = [...(assignments[String(dayId)] ?? [])].sort((a, b) => a.order_index - b.order_index);
|
|
const pair = toursEnabled ? walkPairAt(projectDayItinerary(day, true, places), leg, tourOf, originId) : null;
|
|
if (!pair) {
|
|
onPlan({ dayId, ...leg });
|
|
return;
|
|
}
|
|
const controller = new AbortController();
|
|
pending.current = controller;
|
|
// The stops name the ends; read from this device's copy when offline, so the search waits only for them.
|
|
void Promise.all([
|
|
stopsOrNull(tripId, pair.first.item.placeId, controller.signal),
|
|
stopsOrNull(tripId, pair.next.item.placeId, controller.signal),
|
|
]).then(([first, next]) => {
|
|
if (controller.signal.aborted) return;
|
|
onPlan({ dayId, ...walkTransitLeg({ leg, dayId, pair, stops: { first, next }, places, t }) });
|
|
});
|
|
},
|
|
[onPlan, assignments, toursEnabled, places, tourOf, tripId, t]
|
|
);
|
|
|
|
/** A saved journey on a day that arrives after the walk it leads to starts; null otherwise. */
|
|
const lateBefore = useCallback(
|
|
(day: Pick<Day, 'date'>, reservation: Reservation, following: readonly MergedItem[]): WalkLate | null =>
|
|
toursEnabled ? journeyLate({ reservation, following, date: day.date ?? null, tourOf }) : null,
|
|
[toursEnabled, tourOf]
|
|
);
|
|
|
|
return { legFor, plan, lateBefore };
|
|
}
|