mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
feat: optimize routes around accommodation, confirm note deletions (#1123)
Optimize day routes around the accommodation
When a day has an accommodation set, the route optimizer now treats it as
the day's home base: it optimizes a loop that leaves the hotel and returns
to it, so the stop nearest the hotel comes first. On a transfer day -
checking out of one hotel and into another - the route runs from the first
hotel to the second instead.
The optimizer also gained a 2-opt pass on top of the nearest-neighbor
ordering, which removes the crossings the greedy pass used to leave behind.
A new display setting ("optimize route from accommodation", on by default)
lets you turn the anchoring off.
Confirm before deleting notes
Deleting a plan note or a collab note now asks for confirmation first. On
phones and tablets the edit and delete icons sit close together and were
easy to mis-tap, which deleted notes with no way back.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { Day, Accommodation } from '../types'
|
||||
import { getDayOrder, isDayInAccommodationRange, getAccommodationAnchors } from './dayOrder'
|
||||
|
||||
const days = [
|
||||
{ id: 10, day_number: 1 },
|
||||
{ id: 20, day_number: 2 },
|
||||
{ id: 30, day_number: 3 },
|
||||
] as unknown as Day[]
|
||||
|
||||
const hotel = (over: Partial<Accommodation>): Accommodation =>
|
||||
({ place_lat: 48.1, place_lng: 11.5, start_day_id: 10, end_day_id: 30, ...over }) as Accommodation
|
||||
|
||||
describe('getDayOrder', () => {
|
||||
it('prefers day_number when present', () => {
|
||||
expect(getDayOrder(days[1], days)).toBe(2)
|
||||
})
|
||||
it('falls back to array index when day_number is missing', () => {
|
||||
const noNumber = [{ id: 5 }, { id: 6 }] as unknown as Day[]
|
||||
expect(getDayOrder(noNumber[1], noNumber)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isDayInAccommodationRange', () => {
|
||||
it('is inclusive of both the check-in and check-out day', () => {
|
||||
expect(isDayInAccommodationRange(days[0], 10, 30, days)).toBe(true) // check-in morning
|
||||
expect(isDayInAccommodationRange(days[1], 10, 30, days)).toBe(true) // mid-stay
|
||||
expect(isDayInAccommodationRange(days[2], 10, 30, days)).toBe(true) // check-out day
|
||||
})
|
||||
it('excludes days outside the stay', () => {
|
||||
expect(isDayInAccommodationRange(days[0], 20, 30, days)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAccommodationAnchors', () => {
|
||||
it('returns no anchors when the day has no accommodation', () => {
|
||||
expect(getAccommodationAnchors(days[1], days, [])).toEqual({})
|
||||
})
|
||||
|
||||
it('anchors both ends to the same hotel on a mid-stay day (round trip)', () => {
|
||||
const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: 48.1, place_lng: 11.5 })]
|
||||
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({
|
||||
start: { lat: 48.1, lng: 11.5 },
|
||||
end: { lat: 48.1, lng: 11.5 },
|
||||
})
|
||||
})
|
||||
|
||||
it('loops a single hotel on its check-out day (home base for the day)', () => {
|
||||
const accs = [hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 2 })]
|
||||
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 1, lng: 2 }, end: { lat: 1, lng: 2 } })
|
||||
})
|
||||
|
||||
it('loops a single hotel on its check-in day (home base for the day)', () => {
|
||||
const accs = [hotel({ start_day_id: 20, end_day_id: 30, place_lat: 3, place_lng: 4 })]
|
||||
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({ start: { lat: 3, lng: 4 }, end: { lat: 3, lng: 4 } })
|
||||
})
|
||||
|
||||
it('uses the checked-out hotel as start and the checked-in hotel as end on a transfer day', () => {
|
||||
const accs = [
|
||||
hotel({ start_day_id: 10, end_day_id: 20, place_lat: 1, place_lng: 1 }), // checkout today
|
||||
hotel({ start_day_id: 20, end_day_id: 30, place_lat: 9, place_lng: 9 }), // check-in today
|
||||
]
|
||||
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({
|
||||
start: { lat: 1, lng: 1 },
|
||||
end: { lat: 9, lng: 9 },
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores accommodations that have no coordinates', () => {
|
||||
const accs = [hotel({ start_day_id: 10, end_day_id: 30, place_lat: null, place_lng: null })]
|
||||
expect(getAccommodationAnchors(days[1], days, accs)).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,34 @@
|
||||
import type { Day } from '../types'
|
||||
import type { Day, Accommodation, RouteAnchors } from '../types'
|
||||
|
||||
export const getDayOrder = (day: Day, days: Day[]): number =>
|
||||
day.day_number ?? days.indexOf(day)
|
||||
|
||||
// Derives route anchors from the accommodation(s) active on a day. A single hotel is the day's home
|
||||
// base, so the route is a loop that starts and ends there. A transfer day — checking out of one hotel
|
||||
// and into another — instead runs from the morning hotel to the evening one.
|
||||
export const getAccommodationAnchors = (
|
||||
day: Day,
|
||||
days: Day[],
|
||||
accommodations: Accommodation[],
|
||||
): RouteAnchors => {
|
||||
const located = accommodations.filter(a =>
|
||||
a.place_lat != null && a.place_lng != null &&
|
||||
isDayInAccommodationRange(day, a.start_day_id, a.end_day_id, days),
|
||||
)
|
||||
if (located.length === 0) return {}
|
||||
|
||||
const toAnchor = (a: Accommodation) => ({ lat: a.place_lat as number, lng: a.place_lng as number })
|
||||
|
||||
const checkOut = located.find(a => a.end_day_id === day.id) // the hotel you leave this morning
|
||||
const checkIn = located.find(a => a.start_day_id === day.id) // the hotel you arrive at tonight
|
||||
if (checkOut && checkIn && checkOut !== checkIn) {
|
||||
return { start: toAnchor(checkOut), end: toAnchor(checkIn) }
|
||||
}
|
||||
|
||||
const hotel = toAnchor(located[0])
|
||||
return { start: hotel, end: hotel }
|
||||
}
|
||||
|
||||
export const isDayInAccommodationRange = (
|
||||
day: Day,
|
||||
startDayId: number,
|
||||
|
||||
Reference in New Issue
Block a user