refactor(map): the road trip via helpers move to utils, so the route hook imports no component

This commit is contained in:
Maurice
2026-10-12 21:14:05 +00:00
parent 64e4c34472
commit 7b023c6b54
9 changed files with 139 additions and 94 deletions
@@ -0,0 +1,99 @@
import type { RoadtripVia } from '@trek/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { buildAssignment, buildPlace } from '../../../tests/helpers/factories';
import type { AssignmentsMap, RouteSegment } from '../../types';
import { dayViaLookup } from '../../utils/dayRouteVias';
import { routeLeg } from './dayRouteLeg';
vi.mock('./RouteCalculator', async (importActual) => {
const actual = await importActual<typeof import('./RouteCalculator')>();
return { ...actual, calculateRouteWithLegs: vi.fn() };
});
const { calculateRouteWithLegs } = await import('./RouteCalculator');
const leg = (distance: number): RouteSegment => ({
mid: [0, 0],
from: [distance, 0],
to: [distance + 1, 0],
distance,
duration: distance / 10,
distanceText: '',
durationText: '',
walkingText: '',
drivingText: '',
});
const via = (dayId: number, afterOrderIndex: number, lat: number, sequence = 0): RoadtripVia =>
({
id: lat,
day_id: dayId,
after_order_index: afterOrderIndex,
sequence,
lat,
lng: 0,
created_at: '',
}) as RoadtripVia;
const stop = (id: number, order: number, extra: Record<string, unknown> = {}) =>
buildAssignment({ id, day_id: 1, order_index: order, place: buildPlace({ lat: 48 + order, lng: 2 }), ...extra });
// Stored order: hotel row, Paris, a place without coordinates, Lille, Brussels.
const STORED: AssignmentsMap = {
'1': [
stop(14, 3),
stop(11, 1),
stop(10, 0, { accommodation_id: 5 }),
buildAssignment({
id: 12,
day_id: 1,
order_index: 2,
place: buildPlace({ lat: null as unknown as number, lng: null as unknown as number }),
}),
stop(13, 2.5),
],
};
beforeEach(() => {
vi.mocked(calculateRouteWithLegs).mockReset();
});
describe('dayRouteLeg (#2580)', () => {
it('FE-MAP-DRV-009: one leg is routed through its vias and read back as one leg in its mode', async () => {
vi.mocked(calculateRouteWithLegs).mockResolvedValue({
coordinates: [],
distance: 0,
duration: 0,
legs: [leg(1000), leg(2000)],
});
const lookup = dayViaLookup(STORED, { 1: [via(1, 1, 49.3)] });
const options = { profile: 'driving', tripId: 7, dayId: 1 };
const seg = await routeLeg(
{ lat: 49, lng: 2, assignmentId: 11 },
{ lat: 50.5, lng: 2, assignmentId: 13 },
lookup,
options,
'metric'
);
expect(vi.mocked(calculateRouteWithLegs).mock.calls[0][0]).toEqual([
{ lat: 49, lng: 2 },
{ lat: 49.3, lng: 0 },
{ lat: 50.5, lng: 2 },
]);
expect(seg?.distance).toBe(3000);
expect(seg?.mode).toBe('driving');
});
it('FE-MAP-DRV-010: a leg the router cannot answer is undefined', async () => {
vi.mocked(calculateRouteWithLegs).mockRejectedValueOnce(new Error('down'));
expect(
await routeLeg({ lat: 1, lng: 1 }, { lat: 2, lng: 2 }, null, { profile: 'driving', dayId: 1 }, 'metric')
).toBeUndefined();
vi.mocked(calculateRouteWithLegs).mockResolvedValueOnce({ coordinates: [], distance: 0, duration: 0, legs: [] });
expect(
await routeLeg({ lat: 1, lng: 1 }, { lat: 2, lng: 2 }, null, { profile: 'driving', dayId: 1 }, 'metric')
).toBeUndefined();
});
});
+29
View File
@@ -0,0 +1,29 @@
import type { DistanceUnit, RouteSegment } from '../../types';
import { foldViaLegs, viasOnDayLeg, type DayViaLookup } from '../../utils/dayRouteVias';
import { calculateRouteWithLegs } from './RouteCalculator';
/** One end of a leg: a planned stop carries its assignment, which the vias are filed against. */
export type LegEnd = { lat: number; lng: number; assignmentId?: number };
/**
* The drive between two points of a day as one leg, bent through the road trip's vias
* between them and tagged with the mode it was routed in. Undefined when the router has
* no road for it.
*/
export async function routeLeg(
from: LegEnd,
to: LegEnd,
lookup: DayViaLookup | null,
options: Parameters<typeof calculateRouteWithLegs>[1] & { profile: string; dayId: number },
unit: DistanceUnit | undefined
): Promise<RouteSegment | undefined> {
const vias = viasOnDayLeg(lookup, options.dayId, options.profile, from, to);
try {
const waypoints = [from, ...vias, to].map((p) => ({ lat: p.lat, lng: p.lng }));
const r = await calculateRouteWithLegs(waypoints, options);
const leg = foldViaLegs(r.legs, [0, vias.length + 1], unit)[0];
return leg ? { ...leg, mode: options.profile } : undefined;
} catch {
return undefined;
}
}
@@ -1,6 +1,6 @@
import { calculateRouteWithLegs, RoutingRefusedError, type RouteProfileKey } from './RouteCalculator'
import { buildDayRouteRuns, type DayRouteInputs, type DayRoutePoint } from './dayRoutePlan'
import { dayViaLookup, foldViaLegs, threadVias, viasOnDayLeg, type DayViaLookup } from './dayRouteVias'
import { dayViaLookup, foldViaLegs, threadVias, viasOnDayLeg, type DayViaLookup } from '../../utils/dayRouteVias'
import { projectDayItinerary } from './dayTourProjection'
import { resolveLegMode } from '../Planner/legMode'
import { dayColor } from '../Roadtrip/dayColors'
@@ -11,7 +11,8 @@ import { ChevronDown, ChevronRight, ChevronUp, Compass, RotateCcw, ExternalLink,
import { type PickedPlace } from './TransitSearchPanel'
import { buildTransitLeg, buildTransitNameIndex } from './transitLeg'
import { optimizeRoute, generateGoogleMapsUrl, generateCoMapsUrl } from '../Map/RouteCalculator'
import { dayViaLookup, routeLeg, type LegEnd, type ViasByDay } from '../Map/dayRouteVias'
import { dayViaLookup, type ViasByDay } from '../../utils/dayRouteVias'
import { routeLeg, type LegEnd } from '../Map/dayRouteLeg'
import GoogleMapsIcon from '../shared/GoogleMapsIcon'
import PlaceAvatar from '../shared/PlaceAvatar'
import ConfirmDialog from '../shared/ConfirmDialog'
@@ -14,7 +14,7 @@ import { dayWindow } from './dayWindow'
import { useTranslation } from '../../i18n/TranslationContext'
import { stayStartingOn } from '../../utils/dayMerge'
import { staysAtTheirPlaces } from './nightBookend'
import { viaKeyOf } from '../Map/dayRouteVias'
import { viaKeyOf } from '../../utils/dayRouteVias'
export type { RoadtripStop, RoadtripDay, RoadtripRoutes, QuietDay, AccessSpur, RoutedLeg, SnappedPoint } from '@trek/shared/roadtrip'
+1 -1
View File
@@ -6,7 +6,7 @@ import { useSettingsStore } from '../store/settingsStore'
import { useAddonStore } from '../store/addonStore'
import { calculateRouteWithLegs, type RouteProfileKey } from '../components/Map/RouteCalculator'
import { buildDayRouteRuns, hotelBookendOf, TRANSPORT_TYPES } from '../components/Map/dayRoutePlan'
import { dayViaLookup, foldViaLegs, threadVias, viaKeyOf, viasOnDayLeg } from '../components/Map/dayRouteVias'
import { dayViaLookup, foldViaLegs, threadVias, viaKeyOf, viasOnDayLeg } from '../utils/dayRouteVias'
import { resolveLegMode } from '../components/Planner/legMode'
import type { RoadtripVia } from '@trek/shared'
import type { TripStoreState } from '../store/tripStore'
@@ -14,7 +14,7 @@ import { useRefuelSearch } from '../../components/Roadtrip/useRefuelSearch'
import { useRoadtripCorridor } from '../../components/Roadtrip/useRoadtripCorridor'
import { useRoadtripRoutes } from '../../components/Roadtrip/useRoadtripRoutes'
import type { RoadtripVias } from '../../components/Roadtrip/useRoadtripVias'
import { roadtripStopList } from '../../components/Map/dayRouteVias'
import { roadtripStopList } from '../../utils/dayRouteVias'
import { useLoadRoadtripSettings } from '../../hooks/useRoadtripSettings'
import { roadtripPreferencesRepo } from '../../repo/roadtripPreferencesRepo'
import { useAuthStore } from '../../store/authStore'
@@ -1,23 +1,8 @@
import type { RoadtripVia } from '@trek/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { buildAssignment, buildPlace } from '../../../tests/helpers/factories';
import type { AssignmentsMap, RouteSegment } from '../../types';
import {
dayViaLookup,
foldViaLegs,
roadtripStopList,
routeLeg,
threadVias,
viaKeyOf,
viasOnDayLeg,
} from './dayRouteVias';
vi.mock('./RouteCalculator', async (importActual) => {
const actual = await importActual<typeof import('./RouteCalculator')>();
return { ...actual, calculateRouteWithLegs: vi.fn() };
});
const { calculateRouteWithLegs } = await import('./RouteCalculator');
import { describe, expect, it } from 'vitest';
import { buildAssignment, buildPlace } from '../../tests/helpers/factories';
import type { AssignmentsMap, RouteSegment } from '../types';
import { dayViaLookup, foldViaLegs, roadtripStopList, threadVias, viaKeyOf, viasOnDayLeg } from './dayRouteVias';
const leg = (distance: number): RouteSegment => ({
mid: [0, 0],
@@ -68,10 +53,6 @@ function lookupFor(viasByDay: Record<number, RoadtripVia[]>) {
return lookup;
}
beforeEach(() => {
vi.mocked(calculateRouteWithLegs).mockReset();
});
describe('dayRouteVias (#2580)', () => {
it('FE-MAP-DRV-001: the stop list is the stored order with coordinates, hidden rows included', () => {
expect(roadtripStopList(STORED['1']).map((a) => a.id)).toEqual([10, 11, 13, 14]);
@@ -144,42 +125,4 @@ describe('dayRouteVias (#2580)', () => {
expect(viaKeyOf({ ...a, 1: [via(1, 0, 49.4)] })).not.toBe(viaKeyOf(a));
expect(viaKeyOf(undefined)).toBe('');
});
it('FE-MAP-DRV-009: one leg is routed through its vias and read back as one leg in its mode', async () => {
vi.mocked(calculateRouteWithLegs).mockResolvedValue({
coordinates: [],
distance: 0,
duration: 0,
legs: [leg(1000), leg(2000)],
});
const lookup = dayViaLookup(STORED, { 1: [via(1, 1, 49.3)] });
const options = { profile: 'driving', tripId: 7, dayId: 1 };
const seg = await routeLeg(
{ lat: 49, lng: 2, assignmentId: 11 },
{ lat: 50.5, lng: 2, assignmentId: 13 },
lookup,
options,
'metric'
);
expect(vi.mocked(calculateRouteWithLegs).mock.calls[0][0]).toEqual([
{ lat: 49, lng: 2 },
{ lat: 49.3, lng: 0 },
{ lat: 50.5, lng: 2 },
]);
expect(seg?.distance).toBe(3000);
expect(seg?.mode).toBe('driving');
});
it('FE-MAP-DRV-010: a leg the router cannot answer is undefined', async () => {
vi.mocked(calculateRouteWithLegs).mockRejectedValueOnce(new Error('down'));
expect(
await routeLeg({ lat: 1, lng: 1 }, { lat: 2, lng: 2 }, null, { profile: 'driving', dayId: 1 }, 'metric')
).toBeUndefined();
vi.mocked(calculateRouteWithLegs).mockResolvedValueOnce({ coordinates: [], distance: 0, duration: 0, legs: [] });
expect(
await routeLeg({ lat: 1, lng: 1 }, { lat: 2, lng: 2 }, null, { profile: 'driving', dayId: 1 }, 'metric')
).toBeUndefined();
});
});
@@ -1,7 +1,6 @@
import type { RoadtripVia } from '@trek/shared';
import { isCarrierMode, mergeRouteSegments, viasOnLeg } from '@trek/shared/roadtrip';
import type { Assignment, AssignmentsMap, DistanceUnit, RouteSegment } from '../../types';
import { calculateRouteWithLegs } from './RouteCalculator';
import type { Assignment, AssignmentsMap, DistanceUnit, RouteSegment } from '../types';
/**
* The stops of a day as the road trip counts them, in the order it drives them.
@@ -116,29 +115,3 @@ export function foldViaLegs(legs: RouteSegment[], stopAt: number[], unit: Distan
}
return folded;
}
/** One end of a leg: a planned stop carries its assignment, which the vias are filed against. */
export type LegEnd = { lat: number; lng: number; assignmentId?: number };
/**
* The drive between two points of a day as one leg, bent through the road trip's vias
* between them and tagged with the mode it was routed in. Undefined when the router has
* no road for it.
*/
export async function routeLeg(
from: LegEnd,
to: LegEnd,
lookup: DayViaLookup | null,
options: Parameters<typeof calculateRouteWithLegs>[1] & { profile: string; dayId: number },
unit: DistanceUnit | undefined
): Promise<RouteSegment | undefined> {
const vias = viasOnDayLeg(lookup, options.dayId, options.profile, from, to);
try {
const waypoints = [from, ...vias, to].map((p) => ({ lat: p.lat, lng: p.lng }));
const r = await calculateRouteWithLegs(waypoints, options);
const leg = foldViaLegs(r.legs, [0, vias.length + 1], unit)[0];
return leg ? { ...leg, mode: options.profile } : undefined;
} catch {
return undefined;
}
}