mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
fix(roadtrip): the drive between two days can take another way too
The drive from where one day ended to where the next begins was the one stretch of a connected road trip that offered no other ways. The band above a card's first stop had no button on the desk, and the phone did not show that drive at all. A via behind a day's last stop, where the map already files a point dropped on that drive, was deleted when the day was reordered under Days, and was left on no stop when that last stop was removed or dragged up the day. Switching the avoided road classes or the day's profile kept the old road and minutes on it until a reload. The drive in now opens the same picker as any leg, on the desk band and on a new row at the head of the phone's chain that names where it leaves. A choice is proven with the seam's own router and written behind the last stop of the day before, also when a day with a single stop lies between. The rule for that via lives once in shared (carriedSeam): it stays with the last stop while that stop stays last, and is read like any other once it is not, for the planner's drags and removals as for the server's sorts and seated nights. The cached answer for a seam now records its mode and avoided classes, so changing either asks for it again.
This commit is contained in:
@@ -235,6 +235,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'استخدام الإعداد الافتراضي الخاص بي',
|
||||
'roadtrip.stop.fillSet': 'حدد مقدار التعبئة في هذه المحطة',
|
||||
'roadtrip.leg.pending': 'لا يوجد مسار',
|
||||
'roadtrip.leg.arrivingFrom': 'من {name}',
|
||||
'roadtrip.stop.addTitle': 'إضافة كمحطة توقف',
|
||||
'roadtrip.stop.kind': 'نوع المحطة',
|
||||
'roadtrip.stop.landsOn': 'اليوم {day}، المحطة {position}',
|
||||
|
||||
@@ -241,6 +241,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Usar meu padrão',
|
||||
'roadtrip.stop.fillSet': 'Definir quanto abastecer nesta parada',
|
||||
'roadtrip.leg.pending': 'Sem rota',
|
||||
'roadtrip.leg.arrivingFrom': 'Saindo de {name}',
|
||||
'roadtrip.stop.addTitle': 'Adicionar como parada',
|
||||
'roadtrip.stop.kind': 'Tipo de parada',
|
||||
'roadtrip.stop.landsOn': 'Dia {day}, como parada {position}',
|
||||
|
||||
@@ -243,6 +243,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Usa el meu valor per defecte',
|
||||
'roadtrip.stop.fillSet': 'Defineix quant s’omple en aquesta parada',
|
||||
'roadtrip.leg.pending': 'Sense ruta',
|
||||
'roadtrip.leg.arrivingFrom': 'Des de {name}',
|
||||
'roadtrip.stop.addTitle': 'Afegeix com a parada',
|
||||
'roadtrip.stop.kind': 'Tipus de parada',
|
||||
'roadtrip.stop.landsOn': 'Dia {day}, com a parada {position}',
|
||||
|
||||
@@ -239,6 +239,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Použít moji výchozí hodnotu',
|
||||
'roadtrip.stop.fillSet': 'Nastavit, na kolik se tady tankuje',
|
||||
'roadtrip.leg.pending': 'Bez trasy',
|
||||
'roadtrip.leg.arrivingFrom': 'Z místa {name}',
|
||||
'roadtrip.stop.addTitle': 'Přidat jako zastávku',
|
||||
'roadtrip.stop.kind': 'Druh zastávky',
|
||||
'roadtrip.stop.landsOn': '{day}. den, jako {position}. zastávka',
|
||||
|
||||
@@ -238,6 +238,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Meinen Standard nehmen',
|
||||
'roadtrip.stop.fillSet': 'Festlegen, wie voll hier getankt wird',
|
||||
'roadtrip.leg.pending': 'Keine Route',
|
||||
'roadtrip.leg.arrivingFrom': 'Ab {name}',
|
||||
'roadtrip.stop.addTitle': 'Als Stopp hinzufügen',
|
||||
'roadtrip.stop.kind': 'Art des Stopps',
|
||||
'roadtrip.stop.landsOn': 'Tag {day}, als Stopp {position}',
|
||||
|
||||
@@ -236,6 +236,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Use my default',
|
||||
'roadtrip.stop.fillSet': 'Set how full this stop fills',
|
||||
'roadtrip.leg.pending': 'No route',
|
||||
'roadtrip.leg.arrivingFrom': 'From {name}',
|
||||
'roadtrip.stop.addTitle': 'Add as a stop',
|
||||
'roadtrip.stop.kind': 'Kind of stop',
|
||||
'roadtrip.stop.landsOn': 'Day {day}, as stop {position}',
|
||||
|
||||
@@ -242,6 +242,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Usar mi valor por defecto',
|
||||
'roadtrip.stop.fillSet': 'Definir cuánto se reposta en esta parada',
|
||||
'roadtrip.leg.pending': 'Sin ruta',
|
||||
'roadtrip.leg.arrivingFrom': 'Desde {name}',
|
||||
'roadtrip.stop.addTitle': 'Añadir como parada',
|
||||
'roadtrip.stop.kind': 'Tipo de parada',
|
||||
'roadtrip.stop.landsOn': 'Día {day}, como parada {position}',
|
||||
|
||||
@@ -243,6 +243,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Utiliser ma valeur par défaut',
|
||||
'roadtrip.stop.fillSet': 'Définir le niveau de remplissage de cet arrêt',
|
||||
'roadtrip.leg.pending': 'Pas d’itinéraire',
|
||||
'roadtrip.leg.arrivingFrom': 'Depuis {name}',
|
||||
'roadtrip.stop.addTitle': 'Ajouter comme arrêt',
|
||||
'roadtrip.stop.kind': 'Type d’arrêt',
|
||||
'roadtrip.stop.landsOn': 'Jour {day}, en tant qu’arrêt {position}',
|
||||
|
||||
@@ -244,6 +244,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Χρήση της προεπιλογής μου',
|
||||
'roadtrip.stop.fillSet': 'Όρισε πόσο γεμίζει αυτή η στάση',
|
||||
'roadtrip.leg.pending': 'Χωρίς διαδρομή',
|
||||
'roadtrip.leg.arrivingFrom': 'Από {name}',
|
||||
'roadtrip.stop.addTitle': 'Προσθήκη ως στάση',
|
||||
'roadtrip.stop.kind': 'Είδος στάσης',
|
||||
'roadtrip.stop.landsOn': 'Ημέρα {day}, ως στάση {position}',
|
||||
|
||||
@@ -240,6 +240,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Az alapértelmezettet használom',
|
||||
'roadtrip.stop.fillSet': 'Beállítás: mennyire tölt fel ez a megálló',
|
||||
'roadtrip.leg.pending': 'Nincs útvonal',
|
||||
'roadtrip.leg.arrivingFrom': 'Indulás: {name}',
|
||||
'roadtrip.stop.addTitle': 'Hozzáadás megállóként',
|
||||
'roadtrip.stop.kind': 'Megálló típusa',
|
||||
'roadtrip.stop.landsOn': '{day}. nap, {position}. megállóként',
|
||||
|
||||
@@ -242,6 +242,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Pakai bawaan saya',
|
||||
'roadtrip.stop.fillSet': 'Atur seberapa penuh perhentian ini mengisi',
|
||||
'roadtrip.leg.pending': 'Tidak ada rute',
|
||||
'roadtrip.leg.arrivingFrom': 'Dari {name}',
|
||||
'roadtrip.stop.addTitle': 'Tambahkan sebagai perhentian',
|
||||
'roadtrip.stop.kind': 'Jenis perhentian',
|
||||
'roadtrip.stop.landsOn': 'Hari {day}, sebagai perhentian {position}',
|
||||
|
||||
@@ -242,6 +242,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Usa il mio valore predefinito',
|
||||
'roadtrip.stop.fillSet': 'Imposta quanto si riempie in questa sosta',
|
||||
'roadtrip.leg.pending': 'Nessun percorso',
|
||||
'roadtrip.leg.arrivingFrom': 'Da {name}',
|
||||
'roadtrip.stop.addTitle': 'Aggiungi come sosta',
|
||||
'roadtrip.stop.kind': 'Tipo di sosta',
|
||||
'roadtrip.stop.landsOn': 'Giorno {day}, come sosta {position}',
|
||||
|
||||
@@ -238,6 +238,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': '自分の既定値を使う',
|
||||
'roadtrip.stop.fillSet': 'この停車地でどこまで補給するか決める',
|
||||
'roadtrip.leg.pending': 'ルートなし',
|
||||
'roadtrip.leg.arrivingFrom': '{name}から',
|
||||
'roadtrip.stop.addTitle': '立ち寄り先として追加',
|
||||
'roadtrip.stop.kind': '立ち寄りの種類',
|
||||
'roadtrip.stop.landsOn': '{day} 日目、{position} 番目の立ち寄り先',
|
||||
|
||||
@@ -235,6 +235,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': '내 기본값 사용',
|
||||
'roadtrip.stop.fillSet': '이 정차지에서 얼마나 충전할지 설정',
|
||||
'roadtrip.leg.pending': '경로 없음',
|
||||
'roadtrip.leg.arrivingFrom': '{name}에서 출발',
|
||||
'roadtrip.stop.addTitle': '경유지로 추가',
|
||||
'roadtrip.stop.kind': '경유지 종류',
|
||||
'roadtrip.stop.landsOn': '{day}일차, {position}번째 경유지',
|
||||
|
||||
@@ -242,6 +242,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Mijn standaard gebruiken',
|
||||
'roadtrip.stop.fillSet': 'Instellen hoe vol er hier wordt getankt',
|
||||
'roadtrip.leg.pending': 'Geen route',
|
||||
'roadtrip.leg.arrivingFrom': 'Vanaf {name}',
|
||||
'roadtrip.stop.addTitle': 'Als stop toevoegen',
|
||||
'roadtrip.stop.kind': 'Soort stop',
|
||||
'roadtrip.stop.landsOn': 'Dag {day}, als stop {position}',
|
||||
|
||||
@@ -240,6 +240,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Użyj mojej wartości domyślnej',
|
||||
'roadtrip.stop.fillSet': 'Ustaw, do ile tankować na tym postoju',
|
||||
'roadtrip.leg.pending': 'Brak trasy',
|
||||
'roadtrip.leg.arrivingFrom': 'Z miejsca {name}',
|
||||
'roadtrip.stop.addTitle': 'Dodaj jako przystanek',
|
||||
'roadtrip.stop.kind': 'Rodzaj przystanku',
|
||||
'roadtrip.stop.landsOn': 'Dzień {day}, jako przystanek {position}',
|
||||
|
||||
@@ -242,6 +242,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Взять моё значение по умолчанию',
|
||||
'roadtrip.stop.fillSet': 'Задать, насколько заправляться на этой остановке',
|
||||
'roadtrip.leg.pending': 'Нет маршрута',
|
||||
'roadtrip.leg.arrivingFrom': 'Отправление: {name}',
|
||||
'roadtrip.stop.addTitle': 'Добавить как остановку',
|
||||
'roadtrip.stop.kind': 'Тип остановки',
|
||||
'roadtrip.stop.landsOn': 'День {day}, остановка {position}',
|
||||
|
||||
@@ -239,6 +239,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Använd mitt standardvärde',
|
||||
'roadtrip.stop.fillSet': 'Ange hur mycket det här stoppet fyller',
|
||||
'roadtrip.leg.pending': 'Ingen rutt',
|
||||
'roadtrip.leg.arrivingFrom': 'Från {name}',
|
||||
'roadtrip.stop.addTitle': 'Lägg till som stopp',
|
||||
'roadtrip.stop.kind': 'Typ av stopp',
|
||||
'roadtrip.stop.landsOn': 'Dag {day}, som stopp {position}',
|
||||
|
||||
@@ -240,6 +240,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Kendi varsayılanımı kullan',
|
||||
'roadtrip.stop.fillSet': 'Bu molada ne kadar doldurulacağını ayarla',
|
||||
'roadtrip.leg.pending': 'Rota yok',
|
||||
'roadtrip.leg.arrivingFrom': 'Çıkış: {name}',
|
||||
'roadtrip.stop.addTitle': 'Mola olarak ekle',
|
||||
'roadtrip.stop.kind': 'Mola türü',
|
||||
'roadtrip.stop.landsOn': '{day}. gün, {position}. mola',
|
||||
|
||||
@@ -241,6 +241,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Узяти моє значення за замовчуванням',
|
||||
'roadtrip.stop.fillSet': 'Задати, наскільки заправлятися на цій зупинці',
|
||||
'roadtrip.leg.pending': 'Немає маршруту',
|
||||
'roadtrip.leg.arrivingFrom': 'Відправлення: {name}',
|
||||
'roadtrip.stop.addTitle': 'Додати як зупинку',
|
||||
'roadtrip.stop.kind': 'Тип зупинки',
|
||||
'roadtrip.stop.landsOn': 'День {day}, зупинка {position}',
|
||||
|
||||
@@ -239,6 +239,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': 'Dùng mặc định của tôi',
|
||||
'roadtrip.stop.fillSet': 'Đặt mức đổ tại điểm dừng này',
|
||||
'roadtrip.leg.pending': 'Không có lộ trình',
|
||||
'roadtrip.leg.arrivingFrom': 'Từ {name}',
|
||||
'roadtrip.stop.addTitle': 'Thêm làm điểm dừng',
|
||||
'roadtrip.stop.kind': 'Loại điểm dừng',
|
||||
'roadtrip.stop.landsOn': 'Ngày {day}, là điểm dừng {position}',
|
||||
|
||||
@@ -226,6 +226,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': '使用我的預設值',
|
||||
'roadtrip.stop.fillSet': '設定這一站加到多少',
|
||||
'roadtrip.leg.pending': '無路線',
|
||||
'roadtrip.leg.arrivingFrom': '從 {name} 出發',
|
||||
'roadtrip.stop.addTitle': '新增為停靠點',
|
||||
'roadtrip.stop.kind': '停靠類型',
|
||||
'roadtrip.stop.landsOn': '第 {day} 天,第 {position} 個停靠點',
|
||||
|
||||
@@ -227,6 +227,7 @@ const roadtrip: TranslationStrings = {
|
||||
'roadtrip.stop.fillDefault': '使用我的默认值',
|
||||
'roadtrip.stop.fillSet': '设置这一站加到多少',
|
||||
'roadtrip.leg.pending': '无路线',
|
||||
'roadtrip.leg.arrivingFrom': '从 {name} 出发',
|
||||
'roadtrip.stop.addTitle': '添加为停靠点',
|
||||
'roadtrip.stop.kind': '停靠类型',
|
||||
'roadtrip.stop.landsOn': '第 {day} 天,第 {position} 个停靠点',
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
refuelStopTypeFor,
|
||||
leaveAfter,
|
||||
scheduleStopOf,
|
||||
carriedSeam,
|
||||
reanchorAfterRemove,
|
||||
reanchorByStopOrder,
|
||||
} from './roadtripModel';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
@@ -1169,3 +1172,29 @@ describe('a check-in holds a booked night the way a pinned time does', () => {
|
||||
expect(schedule.warnings).toContainEqual({ index: 1, code: 'late', minutes: 314 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('carriedSeam', () => {
|
||||
it('FE-ROADTRIP-MODEL-107: the via behind a last stop stays with it while it is last, whatever its number', () => {
|
||||
expect(carriedSeam([10, 20, 30], [20, 10, 30])).toEqual({ from: 2, to: 2 });
|
||||
// A stop taken out ahead of it, and a night seated ahead of it.
|
||||
expect(carriedSeam([10, 20, 30], [10, 30])).toEqual({ from: 2, to: 1 });
|
||||
expect(carriedSeam([10, 20, 30], [10, 20, 25, 30])).toEqual({ from: 2, to: 3 });
|
||||
// Another stop is last now, or the day has none.
|
||||
expect(carriedSeam([10, 20, 30], [30, 10, 20])).toBeNull();
|
||||
expect(carriedSeam([10, 20, 30], [10, 20, 30, 40])).toBeNull();
|
||||
expect(carriedSeam([10, 20, 30], [10, 20])).toBeNull();
|
||||
expect(carriedSeam([], [10])).toBeNull();
|
||||
expect(carriedSeam([10], [])).toBeNull();
|
||||
});
|
||||
|
||||
it('FE-ROADTRIP-MODEL-108: the reorder and the removal the planner and the server run both keep to it', () => {
|
||||
const via = (id: number, after: number) => ({ id, after_order_index: after, lat: 53, lng: 10 });
|
||||
expect(reanchorByStopOrder([via(9, 2)], [10, 20, 30], [10, 30])).toEqual({
|
||||
vias: [{ id: 9, after_order_index: 1 }],
|
||||
remove: [],
|
||||
});
|
||||
expect(reanchorByStopOrder([via(9, 2)], [10, 20, 30], [20, 10, 30])).toEqual({ vias: [], remove: [] });
|
||||
expect(reanchorAfterRemove([via(9, 2)], 2, 3)).toEqual({ vias: [], remove: [9] });
|
||||
expect(reanchorAfterRemove([via(9, 2)], 0, 3)).toEqual({ vias: [{ id: 9, after_order_index: 1 }], remove: [] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -542,6 +542,36 @@ export interface Reanchoring {
|
||||
|
||||
const EMPTY_REANCHORING: Reanchoring = { vias: [], remove: [] };
|
||||
|
||||
/**
|
||||
* Where a via filed behind a day's last stop goes once the day's stops change: from its
|
||||
* old index `from` to `to`, the index that stop has now. Null when the stop is no longer
|
||||
* the day's last, and such a via is then read like any other.
|
||||
*
|
||||
* Such a via bends no leg of its own day. It shapes the drive from the day's last stop
|
||||
* into the next day, on a trip with connected days or a night drive: the planner files a
|
||||
* point dropped on that drive there (`anchorFor`), and a way chosen for it is written
|
||||
* there. Read as a leg it leads nowhere and is dropped, which is right for a stop that has
|
||||
* just become last and wrong for one that was last already: sorting the day, or taking a
|
||||
* stop out of its middle, deleted a drive into tomorrow somebody had picked. And a last
|
||||
* stop taken out, or dragged up the day, left its via on a number no stop has, bending
|
||||
* nothing at all.
|
||||
*
|
||||
* The one rule for every writer that renumbers a day, the planner's drags and removals as
|
||||
* much as the server's sorts and seated nights. `previousIds` and `nextIds` are the day's
|
||||
* located stops in order, before and after.
|
||||
*/
|
||||
export function carriedSeam(
|
||||
previousIds: readonly number[],
|
||||
nextIds: readonly number[],
|
||||
): { from: number; to: number } | null {
|
||||
const from = previousIds.length - 1;
|
||||
const to = nextIds.length - 1;
|
||||
return from >= 0 && to >= 0 && previousIds[from] === nextIds[to] ? { from, to } : null;
|
||||
}
|
||||
|
||||
/** The stops of a day as their own positions, for asking `carriedSeam` about a positional edit. */
|
||||
const positions = (count: number): number[] => Array.from({ length: count }, (_, i) => i);
|
||||
|
||||
function collect(vias: AnchoredVia[], at: (index: number) => number | null): Reanchoring {
|
||||
const moved: ReanchoredVia[] = [];
|
||||
const remove: number[] = [];
|
||||
@@ -574,8 +604,15 @@ export function reanchorAfterInsert(
|
||||
export function reanchorAfterRemove(vias: AnchoredVia[], position: number, stopCount: number): Reanchoring {
|
||||
if (!vias.length) return EMPTY_REANCHORING;
|
||||
|
||||
if (stopCount <= 2) return { vias: [], remove: vias.map((v) => v.id) };
|
||||
const before = positions(stopCount);
|
||||
const after = before.filter((i) => i !== position);
|
||||
const seam = carriedSeam(before, after);
|
||||
const last = stopCount - 1;
|
||||
// A day left with one stop has no leg, and only the drive out of it into the next day
|
||||
// can keep its points.
|
||||
if (stopCount <= 2) return collect(vias, (i) => (i === last ? (seam?.to ?? null) : null));
|
||||
return collect(vias, (i) => {
|
||||
if (i === last) return seam?.to ?? null;
|
||||
if (position === 0) return i === 0 ? null : i - 1;
|
||||
if (position === stopCount - 1) return i === position - 1 ? null : i;
|
||||
if (i === position) return position - 1;
|
||||
@@ -585,9 +622,14 @@ export function reanchorAfterRemove(vias: AnchoredVia[], position: number, stopC
|
||||
|
||||
export function reanchorByStopOrder(vias: AnchoredVia[], previousIds: number[], nextIds: number[]): Reanchoring {
|
||||
if (!vias.length) return EMPTY_REANCHORING;
|
||||
const seam = carriedSeam(previousIds, nextIds);
|
||||
const moved: ReanchoredVia[] = [];
|
||||
const remove: number[] = [];
|
||||
for (const via of vias) {
|
||||
if (seam && via.after_order_index === seam.from) {
|
||||
if (seam.to !== seam.from) moved.push({ id: via.id, after_order_index: seam.to });
|
||||
continue;
|
||||
}
|
||||
const stopId = previousIds[via.after_order_index]!;
|
||||
const next = stopId === undefined ? -1 : nextIds.indexOf(stopId);
|
||||
|
||||
@@ -603,7 +645,12 @@ export function reanchorByStopOrder(vias: AnchoredVia[], previousIds: number[],
|
||||
export function reanchorAfterReorder(vias: AnchoredVia[], from: number, to: number, stopCount: number): Reanchoring {
|
||||
if (!vias.length || from === to) return EMPTY_REANCHORING;
|
||||
|
||||
if (stopCount <= 2) return EMPTY_REANCHORING;
|
||||
// Two stops swapped keep their one leg, driven the other way round. The drive into the
|
||||
// next day left from the stop that is first now, so its points go.
|
||||
if (stopCount <= 2) {
|
||||
const seam = vias.filter((v) => v.after_order_index === stopCount - 1).map((v) => v.id);
|
||||
return seam.length ? { vias: [], remove: seam } : EMPTY_REANCHORING;
|
||||
}
|
||||
const afterRemove = reanchorAfterRemove(vias, from, stopCount);
|
||||
const dropped = new Set(afterRemove.remove);
|
||||
const movedTo = new Map(afterRemove.vias.map((v) => [v.id, v.after_order_index] as const));
|
||||
|
||||
Reference in New Issue
Block a user