mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
fe78831bd9
A stay, a name, an importance or a protection set in a stop's dialog no longer plans a city walk's route again: the walk stays ready to save, its time overview follows at once, and one undo takes the change back without routing either. A stop moved or added plans the route again as before. A walk saves no breaks, so a cafe never counts as a stay and as a break at once, and a break left from another type neither counts nor holds its save up. The planner and the tour detail show a walk's walking time, its stays and their total apart and in the same words: 45 minutes of walking, a museum of 90 minutes and a cafe of 30 minutes make 2 h 45 min. A stay not entered yet makes the stays and the total at least what is known, stays nobody entered read not known yet, and stays entered as 0 read 0 min. A total entered by hand keeps its own label, and the computed total shows beside it. The head of the tour detail, the day row on the desktop and the phone, the tour summary and the library of other trips show that same total, with a ≥ where a stay is missing. Their time on the move is called walking, and the summary says a walk is planned with its stays rather than breaks. The tour list and the library carry the minutes a walk's stops are entered with and how many have no stay yet, counted from the walk's points, so the way back of a loop is no stop and its start's stay counts once. A loop run the other way keeps its start as the stop there, with its key, name and stay. The planner's waypoint list keeps the rows of a leg in a file of their own, and the tour detail its name heading in a part of its own, so both stay within their size limits.
105 lines
4.3 KiB
TypeScript
105 lines
4.3 KiB
TypeScript
import type { TourListItem, TourTypeKey } from '@trek/shared';
|
|
import { heightsUnknown } from './tourFacts';
|
|
import { tourPlannedTimes, tourSource } from './tourPresentation';
|
|
|
|
/**
|
|
* A sum over the tours that have the value, with how many have it of how many
|
|
* could: `value` is null when none has it, never a zero standing in for
|
|
* "unknown", and `known < of` says the sum is short of the whole.
|
|
*/
|
|
export interface ScopeSum {
|
|
value: number | null;
|
|
known: number;
|
|
of: number;
|
|
/** CW-02: a value counted is itself only what is known of it (a city walk's stay not entered yet): the sum is at least this. */
|
|
atLeast?: boolean;
|
|
}
|
|
|
|
/** One tour type's share of the trip: how many tours, how many on a day, and their measures. */
|
|
export interface TourTypeScope {
|
|
type: TourTypeKey;
|
|
count: number;
|
|
onDays: number;
|
|
distanceKm: ScopeSum;
|
|
ascentM: ScopeSum;
|
|
descentM: ScopeSum;
|
|
/** The time the router worked out, for the tours planned in TREK. */
|
|
routedMinutes: ScopeSum;
|
|
/** The time an import brought with it (a GPX file, a wanderer trail). */
|
|
importedMinutes: ScopeSum;
|
|
/** The time planned for the day, breaks or the planner's own figure included. */
|
|
plannedMinutes: ScopeSum;
|
|
}
|
|
|
|
export interface TourScope {
|
|
count: number;
|
|
onDays: number;
|
|
/** By type, the most tours first; the time kinds are never added up across types. */
|
|
types: TourTypeScope[];
|
|
}
|
|
|
|
/** A measure a type has no use for: no value, and nothing it is short of. */
|
|
const NO_SUM: ScopeSum = { value: null, known: 0, of: 0 };
|
|
|
|
function sumOf(values: Array<number | null | undefined>): ScopeSum {
|
|
const known = values.filter((value): value is number => value != null && Number.isFinite(value));
|
|
return {
|
|
value: known.length ? known.reduce((total, value) => total + value, 0) : null,
|
|
known: known.length,
|
|
of: values.length,
|
|
};
|
|
}
|
|
|
|
function scopeOfType(type: TourTypeKey, tours: readonly TourListItem[], onDay: OnDay): TourTypeScope {
|
|
const planned = tours.filter((tour) => tourSource(tour) === 'trek');
|
|
const imported = tours.filter((tour) => tourSource(tour) !== 'trek');
|
|
// A paddle tour drawn in TREK has no true heights (they would be the banks'), so it does not count as having them.
|
|
const withHeights = tours.map((tour) => (heightsUnknown(tour) ? null : tour));
|
|
// On the water a climb says nothing: paddling sums no heights, whatever a recorded track brought.
|
|
const heights = type !== 'kayak';
|
|
return {
|
|
type,
|
|
count: tours.length,
|
|
onDays: tours.filter(onDay).length,
|
|
distanceKm: sumOf(tours.map((tour) => tour.distance)),
|
|
ascentM: heights ? sumOf(withHeights.map((tour) => tour?.elevation_gain)) : NO_SUM,
|
|
descentM: heights ? sumOf(withHeights.map((tour) => tour?.elevation_loss)) : NO_SUM,
|
|
routedMinutes: sumOf(planned.map((tour) => tour.duration)),
|
|
importedMinutes: sumOf(imported.map((tour) => tour.duration)),
|
|
plannedMinutes: plannedSum(tours),
|
|
};
|
|
}
|
|
|
|
/** The planned totals summed; at least the sum where a city walk's total leaves out a stay nobody entered (CW-02). */
|
|
function plannedSum(tours: readonly TourListItem[]): ScopeSum {
|
|
const times = tours.map((tour) => tourPlannedTimes(tour));
|
|
return {
|
|
...sumOf(times.map((time) => time.plannedTotalMinutes)),
|
|
atLeast: times.some((time) => time.atLeast),
|
|
};
|
|
}
|
|
|
|
/** Whether a tour is on a day of the trip. */
|
|
type OnDay = (tour: TourListItem) => boolean;
|
|
|
|
/**
|
|
* STA-01: what the tours of a trip add up to, by type. Each tour counts once,
|
|
* however many days it is on; the list is the trip's tours, one row each, so
|
|
* a tour on three days is still one tour and one distance. `onDay` reads the
|
|
* day lists where the caller has them; the list's own flag is the fallback.
|
|
*/
|
|
export function tourScopeSummary(tours: readonly TourListItem[], onDay: OnDay = (tour) => tour.planned): TourScope {
|
|
const byType = new Map<TourTypeKey, TourListItem[]>();
|
|
for (const tour of new Map(tours.map((tour) => [tour.place_id, tour])).values()) {
|
|
byType.set(tour.tour_type, [...(byType.get(tour.tour_type) ?? []), tour]);
|
|
}
|
|
const types = [...byType.entries()]
|
|
.map(([type, ofType]) => scopeOfType(type, ofType, onDay))
|
|
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
return {
|
|
count: types.reduce((total, scope) => total + scope.count, 0),
|
|
onDays: types.reduce((total, scope) => total + scope.onDays, 0),
|
|
types,
|
|
};
|
|
}
|