mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(planner): only route to multi-day transport endpoints on their pickup/drop-off days (#1210)
This commit is contained in:
@@ -20,7 +20,7 @@ import { useSettingsStore } from '../../store/settingsStore'
|
|||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
import { isDayInAccommodationRange, getAccommodationAnchors } from '../../utils/dayOrder'
|
import { isDayInAccommodationRange, getAccommodationAnchors } from '../../utils/dayOrder'
|
||||||
import {
|
import {
|
||||||
TRANSPORT_TYPES, parseTimeToMinutes, getSpanPhase, getDisplayTimeForDay,
|
TRANSPORT_TYPES, parseTimeToMinutes, getSpanPhase, getDisplayTimeForDay, getTransportRouteEndpoints,
|
||||||
getTransportForDay as _getTransportForDay, getMergedItems as _getMergedItems,
|
getTransportForDay as _getTransportForDay, getMergedItems as _getMergedItems,
|
||||||
type MergedItem,
|
type MergedItem,
|
||||||
} from '../../utils/dayMerge'
|
} from '../../utils/dayMerge'
|
||||||
@@ -383,10 +383,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
if (legsAbortRef.current) legsAbortRef.current.abort()
|
if (legsAbortRef.current) legsAbortRef.current.abort()
|
||||||
if (!selectedDayId || !routeShown) { setRouteLegs({}); setHotelLegs({}); return }
|
if (!selectedDayId || !routeShown) { setRouteLegs({}); setHotelLegs({}); return }
|
||||||
const merged = mergedItemsMap[selectedDayId] || []
|
const merged = mergedItemsMap[selectedDayId] || []
|
||||||
const epLoc = (r: any, role: 'from' | 'to'): { lat: number; lng: number } | null => {
|
|
||||||
const e = (r.endpoints || []).find((x: any) => x.role === role)
|
|
||||||
return e && e.lat != null && e.lng != null ? { lat: e.lat, lng: e.lng } : null
|
|
||||||
}
|
|
||||||
const runs: { id: number; lat: number; lng: number }[][] = []
|
const runs: { id: number; lat: number; lng: number }[][] = []
|
||||||
let cur: { id: number; lat: number; lng: number }[] = []
|
let cur: { id: number; lat: number; lng: number }[] = []
|
||||||
for (const it of merged) {
|
for (const it of merged) {
|
||||||
@@ -394,7 +390,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
cur.push({ id: it.data.id, lat: it.data.place.lat, lng: it.data.place.lng })
|
cur.push({ id: it.data.id, lat: it.data.place.lat, lng: it.data.place.lng })
|
||||||
} else if (it.type === 'transport') {
|
} else if (it.type === 'transport') {
|
||||||
const r = it.data
|
const r = it.data
|
||||||
const from = epLoc(r, 'from'), to = epLoc(r, 'to')
|
const { from, to } = getTransportRouteEndpoints(r, selectedDayId)
|
||||||
if (from || to) {
|
if (from || to) {
|
||||||
// Located transport: route to its departure point, break the run (the
|
// Located transport: route to its departure point, break the run (the
|
||||||
// flight/train itself isn't driven), and let its arrival start the next.
|
// flight/train itself isn't driven), and let its arrival start the next.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
||||||
import { useTripStore } from '../store/tripStore'
|
import { useTripStore } from '../store/tripStore'
|
||||||
import { calculateRouteWithLegs } from '../components/Map/RouteCalculator'
|
import { calculateRouteWithLegs } from '../components/Map/RouteCalculator'
|
||||||
|
import { getTransportRouteEndpoints } from '../utils/dayMerge'
|
||||||
import type { TripStoreState } from '../store/tripStore'
|
import type { TripStoreState } from '../store/tripStore'
|
||||||
import type { RouteSegment, RouteResult } from '../types'
|
import type { RouteSegment, RouteResult } from '../types'
|
||||||
|
|
||||||
@@ -53,12 +54,6 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
return pos != null
|
return pos != null
|
||||||
})
|
})
|
||||||
|
|
||||||
// The departure/arrival coordinate of a transport, if its endpoints carry one.
|
|
||||||
const epLoc = (r: any, role: 'from' | 'to'): { lat: number; lng: number } | null => {
|
|
||||||
const e = (r.endpoints || []).find((x: any) => x.role === role)
|
|
||||||
return e && e.lat != null && e.lng != null ? { lat: e.lat, lng: e.lng } : null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a unified list of places + transports sorted by effective position.
|
// Build a unified list of places + transports sorted by effective position.
|
||||||
type Entry =
|
type Entry =
|
||||||
| { kind: 'place'; lat: number; lng: number; pos: number }
|
| { kind: 'place'; lat: number; lng: number; pos: number }
|
||||||
@@ -67,12 +62,15 @@ export function useRouteCalculation(tripStore: TripStoreState, selectedDayId: nu
|
|||||||
...da.filter(a => a.place?.lat && a.place?.lng).map(a => ({
|
...da.filter(a => a.place?.lat && a.place?.lng).map(a => ({
|
||||||
kind: 'place' as const, lat: a.place.lat!, lng: a.place.lng!, pos: a.order_index,
|
kind: 'place' as const, lat: a.place.lat!, lng: a.place.lng!, pos: a.order_index,
|
||||||
})),
|
})),
|
||||||
...dayTransports.map(r => ({
|
...dayTransports.map(r => {
|
||||||
kind: 'transport' as const,
|
const { from, to } = getTransportRouteEndpoints(r, dayId)
|
||||||
from: epLoc(r, 'from'),
|
return {
|
||||||
to: epLoc(r, 'to'),
|
kind: 'transport' as const,
|
||||||
pos: (r.day_positions?.[dayId] ?? r.day_positions?.[String(dayId)] ?? r.day_plan_position) as number,
|
from,
|
||||||
})),
|
to,
|
||||||
|
pos: (r.day_positions?.[dayId] ?? r.day_positions?.[String(dayId)] ?? r.day_plan_position) as number,
|
||||||
|
}
|
||||||
|
}),
|
||||||
].sort((a, b) => a.pos - b.pos)
|
].sort((a, b) => a.pos - b.pos)
|
||||||
|
|
||||||
// Group located places into driving runs.
|
// Group located places into driving runs.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { parseTimeToMinutes, getSpanPhase, getDisplayTimeForDay, getTransportForDay, getMergedItems } from './dayMerge'
|
import { parseTimeToMinutes, getSpanPhase, getTransportRouteEndpoints, getDisplayTimeForDay, getTransportForDay, getMergedItems } from './dayMerge'
|
||||||
|
|
||||||
describe('parseTimeToMinutes', () => {
|
describe('parseTimeToMinutes', () => {
|
||||||
it('parses HH:MM string', () => {
|
it('parses HH:MM string', () => {
|
||||||
@@ -34,6 +34,38 @@ describe('getSpanPhase', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getTransportRouteEndpoints', () => {
|
||||||
|
const pickup = { role: 'from', lat: 48.1, lng: 11.5 }
|
||||||
|
const dropoff = { role: 'to', lat: 52.5, lng: 13.4 }
|
||||||
|
// A car rental spanning day 1 (pickup) through day 3 (drop-off).
|
||||||
|
const rental = { day_id: 1, end_day_id: 3, endpoints: [pickup, dropoff] }
|
||||||
|
|
||||||
|
it('routes to the pickup only on the start day of a multi-day rental', () => {
|
||||||
|
expect(getTransportRouteEndpoints(rental, 1)).toEqual({ from: { lat: 48.1, lng: 11.5 }, to: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('routes from the drop-off only on the end day', () => {
|
||||||
|
expect(getTransportRouteEndpoints(rental, 3)).toEqual({ from: null, to: { lat: 52.5, lng: 13.4 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds no waypoints on the days in between (regression for #1210)', () => {
|
||||||
|
expect(getTransportRouteEndpoints(rental, 2)).toEqual({ from: null, to: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses both endpoints for a single-day transport', () => {
|
||||||
|
const sameDay = { day_id: 1, end_day_id: 1, endpoints: [pickup, dropoff] }
|
||||||
|
expect(getTransportRouteEndpoints(sameDay, 1)).toEqual({
|
||||||
|
from: { lat: 48.1, lng: 11.5 },
|
||||||
|
to: { lat: 52.5, lng: 13.4 },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns nulls when the endpoints carry no coordinates', () => {
|
||||||
|
const noCoords = { day_id: 1, end_day_id: 1, endpoints: [{ role: 'from' }, { role: 'to' }] }
|
||||||
|
expect(getTransportRouteEndpoints(noCoords, 1)).toEqual({ from: null, to: null })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('getDisplayTimeForDay', () => {
|
describe('getDisplayTimeForDay', () => {
|
||||||
const r = { day_id: 1, end_day_id: 3, reservation_time: '2025-01-01T09:00:00', reservation_end_time: '2025-01-03T14:00:00' }
|
const r = { day_id: 1, end_day_id: 3, reservation_time: '2025-01-01T09:00:00', reservation_end_time: '2025-01-03T14:00:00' }
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,33 @@ export function getSpanPhase(
|
|||||||
return 'middle'
|
return 'middle'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The route waypoints a transport contributes on a given day, respecting multi-day spans.
|
||||||
|
* A car rental (or any reservation whose span covers several days) is only routed to on its
|
||||||
|
* pickup day (the departure endpoint) and from on its drop-off day (the arrival endpoint) — on
|
||||||
|
* the days in between you simply hold the vehicle, so it adds no waypoints and must not pull the
|
||||||
|
* route to those points. Single-day transports contribute both endpoints.
|
||||||
|
*/
|
||||||
|
export function getTransportRouteEndpoints(
|
||||||
|
r: any,
|
||||||
|
dayId: number
|
||||||
|
): { from: { lat: number; lng: number } | null; to: { lat: number; lng: number } | null } {
|
||||||
|
const ep = (role: 'from' | 'to'): { lat: number; lng: number } | null => {
|
||||||
|
const e = (r.endpoints || []).find((x: any) => x.role === role)
|
||||||
|
return e && e.lat != null && e.lng != null ? { lat: e.lat, lng: e.lng } : null
|
||||||
|
}
|
||||||
|
switch (getSpanPhase(r, dayId)) {
|
||||||
|
case 'start':
|
||||||
|
return { from: ep('from'), to: null }
|
||||||
|
case 'end':
|
||||||
|
return { from: null, to: ep('to') }
|
||||||
|
case 'middle':
|
||||||
|
return { from: null, to: null }
|
||||||
|
default:
|
||||||
|
return { from: ep('from'), to: ep('to') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getDisplayTimeForDay(
|
export function getDisplayTimeForDay(
|
||||||
r: { day_id?: number | null; end_day_id?: number | null; reservation_time?: string | null; reservation_end_time?: string | null },
|
r: { day_id?: number | null; end_day_id?: number | null; reservation_time?: string | null; reservation_end_time?: string | null },
|
||||||
dayId: number
|
dayId: number
|
||||||
|
|||||||
Reference in New Issue
Block a user