mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
fix(tours): a city walk's walking time, stays and total read apart and alike everywhere, and a stay plans no route again (CW-02)
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.
This commit is contained in:
@@ -139,6 +139,19 @@ export class TourWaypointsRepository extends TrekRepository<TourWaypoints> {
|
||||
return rows.map(({ place_id, ...point }) => ({ place_id, ...readWaypoint(point) }));
|
||||
}
|
||||
|
||||
/** TW2b: every waypoint of these tours with its place, ordered by place and sequence; none for no tour. */
|
||||
async listForPlaces(place_ids: readonly number[]): Promise<TourWaypointWithPlaceRow[]> {
|
||||
if (place_ids.length === 0) return [];
|
||||
const rows = await this.db()
|
||||
.selectFrom('tour_waypoints')
|
||||
.select(['place_id', ...WAYPOINT_COLUMNS])
|
||||
.where('place_id', 'in', [...new Set(place_ids)])
|
||||
.orderBy('place_id', 'asc')
|
||||
.orderBy('sequence', 'asc')
|
||||
.execute();
|
||||
return rows.map(({ place_id, ...point }) => ({ place_id, ...readWaypoint(point) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* TW3 — `INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence, fixed_leg, lock, shaping)
|
||||
* VALUES (...)` for each point. The caller owns the transaction: a create
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Tours } from '../../db/entities/Tours.entity';
|
||||
import type { ToursRepository, TourLibraryRow } from '../../db/repositories/Tours.repository';
|
||||
import { TripMembershipService } from '../trip-membership/trip-membership.service';
|
||||
import { parseRouteGeometry, thinnedLine } from './tours.helpers';
|
||||
import { parseRouteGeometry, thinnedLine, walkStationFacts } from './tours.helpers';
|
||||
import { ToursService } from './tours.service';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
/** The most rows one list reads before the name is matched; past it the answer says it was cut. */
|
||||
const LIBRARY_READ_LIMIT = 2000;
|
||||
|
||||
function toItem(row: TourLibraryRow): TourLibraryItem {
|
||||
/** A tour of another trip as the list shows it; a city walk with its stations and their stays (CW-02). */
|
||||
function toItem(row: TourLibraryRow, points: Parameters<typeof walkStationFacts>[1] = []): TourLibraryItem {
|
||||
return {
|
||||
place_id: row.place_id,
|
||||
name: row.name,
|
||||
@@ -36,6 +37,7 @@ function toItem(row: TourLibraryRow): TourLibraryItem {
|
||||
max_hiking_difficulty: row.max_hiking_difficulty,
|
||||
has_waypoints: row.has_waypoints === 1,
|
||||
origin: row.origin as TourOrigin,
|
||||
...walkStationFacts(row.tour_type, points),
|
||||
trip: { id: row.trip_id, title: row.trip_title },
|
||||
};
|
||||
}
|
||||
@@ -74,8 +76,10 @@ export class TourLibraryService {
|
||||
]);
|
||||
const needle = query.q?.toLocaleLowerCase();
|
||||
const matching = needle ? rows.filter((row) => row.name.toLocaleLowerCase().includes(needle)) : rows;
|
||||
const shown = matching.slice(0, TOUR_LIBRARY_LIMIT);
|
||||
const walkPoints = await this.tours.walkPointsByPlace(shown);
|
||||
return {
|
||||
tours: matching.slice(0, TOUR_LIBRARY_LIMIT).map(toItem),
|
||||
tours: shown.map((row) => toItem(row, walkPoints.get(row.place_id))),
|
||||
trips,
|
||||
truncated: matching.length > TOUR_LIBRARY_LIMIT || rows.length === LIBRARY_READ_LIMIT,
|
||||
};
|
||||
@@ -85,8 +89,9 @@ export class TourLibraryService {
|
||||
const row = await this.toursRepo.findForLibrary(await this.reachableTrips(userId), placeId);
|
||||
if (!row) throw new NotFoundException('Tour not found');
|
||||
const line = parseRouteGeometry(row.route_geometry).map((point): [number, number] => [point[0], point[1]]);
|
||||
const walkPoints = await this.tours.walkPointsByPlace([row]);
|
||||
return {
|
||||
tour: toItem(row),
|
||||
tour: toItem(row, walkPoints.get(row.place_id)),
|
||||
line: thinnedLine(line, TOUR_LIBRARY_PREVIEW_POINTS),
|
||||
waypoint_count: row.waypoint_count,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* PlacesService prepares and persists imported places; these helpers derive
|
||||
* metrics from the resulting [[lat,lng,ele?]] geometry.
|
||||
*/
|
||||
import type { GeometryPoint } from '@trek/shared';
|
||||
import { walkStationsOf, type GeometryPoint, type TourListItem, type TourWaypoint } from '@trek/shared';
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
@@ -30,6 +30,27 @@ export function parseRouteGeometry(routeGeometry: string | null | undefined): Ge
|
||||
}
|
||||
}
|
||||
|
||||
/** What a list says of a city walk's stations: how many, their stays as far as entered, and how many have none. */
|
||||
export type WalkStationFacts = Pick<TourListItem, 'station_count' | 'station_stay_minutes' | 'station_stays_unknown'>;
|
||||
|
||||
/**
|
||||
* CW-02, CW-03: a city walk's stations as a list counts them, from its saved
|
||||
* points: a shaping point and the way back of a loop are no station, a stay
|
||||
* nobody entered is counted apart and never as nothing. Any other type has none.
|
||||
*/
|
||||
export function walkStationFacts(
|
||||
tourType: string,
|
||||
points: readonly Pick<TourWaypoint, 'lat' | 'lng' | 'shaping' | 'stay_minutes'>[],
|
||||
): WalkStationFacts {
|
||||
if (tourType !== 'walk') return { station_count: null, station_stay_minutes: null, station_stays_unknown: null };
|
||||
const stations = walkStationsOf(points);
|
||||
return {
|
||||
station_count: stations.length,
|
||||
station_stay_minutes: stations.reduce((sum, station) => sum + (station.stay_minutes ?? 0), 0),
|
||||
station_stays_unknown: stations.filter((station) => station.stay_minutes == null).length,
|
||||
};
|
||||
}
|
||||
|
||||
/** Confidence threshold: below this, the tour imports with a caution flag. */
|
||||
export const LOW_CONFIDENCE_THRESHOLD = 0.5;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
localHashOf,
|
||||
parseRouteGeometry,
|
||||
LOW_CONFIDENCE_THRESHOLD,
|
||||
walkStationFacts,
|
||||
type GeometryPoint,
|
||||
} from './tours.helpers';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
@@ -116,7 +117,7 @@ export class ToursService {
|
||||
private readonly paddleChecks: TourPaddleCheckService,
|
||||
) {}
|
||||
|
||||
/** A tour as it is listed; a city walk with the stations its points make (CW-03). */
|
||||
/** A tour as it is listed; a city walk with the stations its points make and their stays (CW-02, CW-03). */
|
||||
private toItem(r: TourListRow, points: readonly TourWaypointRow[] = []): TourListItem {
|
||||
return {
|
||||
place_id: r.place_id,
|
||||
@@ -149,7 +150,7 @@ export class ToursService {
|
||||
has_waypoints: Boolean(r.has_waypoints),
|
||||
revision: r.revision,
|
||||
hiking_preferences: decodeJson(TOUR_HIKING_PREFERENCES, r.hiking_preferences, 'tour'),
|
||||
station_count: r.tour_type === 'walk' ? walkStationsOf(points).length : null,
|
||||
...walkStationFacts(r.tour_type, points),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,21 +226,24 @@ export class ToursService {
|
||||
const tid = toRowId(tripId);
|
||||
if (tid === null) return [];
|
||||
const rows = await this.toursRepo.listForTrip(tid);
|
||||
const walkPoints = await this.walkPointsOf(tid, rows);
|
||||
const walkPoints = await this.walkPointsByPlace(rows);
|
||||
return await this.paddleChecks.forTrip(
|
||||
tid,
|
||||
rows.map((r) => this.toItem(r, walkPoints.get(r.place_id) ?? [])),
|
||||
);
|
||||
}
|
||||
|
||||
/** CW-03: the points of the trip's city walks by tour, read only where the trip has a walk. */
|
||||
private async walkPointsOf(
|
||||
tripId: number,
|
||||
rows: readonly TourListRow[],
|
||||
/**
|
||||
* CW-02, CW-03: the points of the city walks among these tours, by tour, for
|
||||
* a list that counts their stations and stays; the points of another type
|
||||
* are not read.
|
||||
*/
|
||||
async walkPointsByPlace(
|
||||
rows: readonly { place_id: number; tour_type: string }[],
|
||||
): Promise<Map<number, TourWaypointWithPlaceRow[]>> {
|
||||
const byPlace = new Map<number, TourWaypointWithPlaceRow[]>();
|
||||
if (!rows.some((row) => row.tour_type === 'walk')) return byPlace;
|
||||
for (const point of await this.waypointsRepo.listForTrip(tripId)) {
|
||||
const walks = rows.filter((row) => row.tour_type === 'walk').map((row) => row.place_id);
|
||||
for (const point of await this.waypointsRepo.listForPlaces(walks)) {
|
||||
const points = byPlace.get(point.place_id);
|
||||
if (points) points.push(point);
|
||||
else byPlace.set(point.place_id, [point]);
|
||||
|
||||
Reference in New Issue
Block a user