diff --git a/mock/app.js b/mock/app.js index 05c9021..a2c07e0 100644 --- a/mock/app.js +++ b/mock/app.js @@ -214,7 +214,10 @@ function llmContext() { layout(dd); const stops = dd.stops.filter(s => s.state === 'planned').map( s => `${fmt(s.start)} ${s.name} (${s.dur} min${s.price ? ', ' + s.price.amount + s.price.cur : ''})`); - L.push(`${dd.label} [${dd.id} · ${dd.date}]: ${stops.length ? stops.join(' · ') : '(empty)'} — day ends ~${fmt(dd.startMin + (dd.wakingHours || 11) * 60)}`); + const arrInfo = (dd.isArrival && dd.transferIn && dd.arrLandingMin != null) + ? ` — lands ${fmt(dd.arrLandingMin)}, ${dd.transferIn.mode || 'car'} to hotel ${dd.transferIn.dur} min, first stop at ${fmt((dd.stops[0] || { start: dd.startMin }).start)}` + : ''; + L.push(`${dd.label} [${dd.id} · ${dd.date}]: ${stops.length ? stops.join(' · ') : '(empty)'}${arrInfo} — day ends ~${fmt(layout(dd))}`); } return L.join('\n'); } @@ -320,9 +323,10 @@ function driveMin(ap, hotelAt) { // rough airport<->hotel taxi time // before the traveller had even reached the hotel). function syncArrivalStart(d) { if (!d || !d.isArrival || d.arrLandingMin == null || !d.transferIn) return; - d.startMin = d.arrLandingMin + d.transferIn.dur; - const arrEnd = Math.min(ARR_DAY_END, d.startMin + (d.wakingHours || 12) * 60); - d.wakingHours = Math.max(2, Math.round((arrEnd - d.startMin) / 60)); + d.startMin = d.arrLandingMin + d.transferIn.dur; // time at the HOTEL (bag drop) + const eff = d.startMin + firstLegDur(d); // + hotel → first stop = time at the first stop + const arrEnd = Math.min(ARR_DAY_END, eff + (d.wakingHours || 12) * 60); + d.wakingHours = Math.max(2, Math.round((arrEnd - eff) / 60)); } function relabelDay(d, idx, dateStr) { const [y, mo, da] = String(dateStr).split('-').map(Number); @@ -466,13 +470,21 @@ function healTransfers() { // geometry ends anywhere else than the current hotel point, drop it so it re-routes ids.forEach(id => { const dd = days[id]; - const leg = dd.retLeg; - if (!leg || !leg.geometry || !leg.geometry.length) return; const h = dd.base && dd.base.at; - if (h && haversine(leg.geometry[leg.geometry.length - 1], h) > 300) { - const lastSt = dd.stops[dd.stops.length - 1]; - leg.dur = (lastSt && h) ? estLegMin(leg.mode || travelMode, lastSt.at, h) : leg.dur; - leg.conf = 1; leg.geometry = null; delete leg._routedMode; changed = true; + const leg = dd.retLeg; + if (leg && leg.geometry && leg.geometry.length) { + if (h && haversine(leg.geometry[leg.geometry.length - 1], h) > 300) { + const lastSt = dd.stops[dd.stops.length - 1]; + leg.dur = (lastSt && h) ? estLegMin(leg.mode || travelMode, lastSt.at, h) : leg.dur; + leg.conf = 1; leg.geometry = null; delete leg._routedMode; changed = true; + } + } + const fl = dd.firstLeg; // the hotel → first-stop leg gets the same endpoint-drift check + if (fl && fl.geometry && fl.geometry.length) { + const f0 = dd.stops[0]; + if (f0 && h && (haversine(fl.geometry[0], h) > 300 || haversine(fl.geometry[fl.geometry.length - 1], f0.at) > 300)) { + fl.dur = estLegMin(fl.mode || travelMode, h, f0.at); fl.conf = 1; fl.geometry = null; delete fl._routedMode; changed = true; + } } }); const first = days[ids[0]], last = days[ids[ids.length - 1]]; @@ -708,7 +720,12 @@ const TOOLS = { const d = days[a.dayId]; if (!d) return { error: `unknown day "${a.dayId}"` }; const m = /^(\d{1,2}):(\d{2})$/.exec(String(a.time || '').trim()); if (!m) return { error: 'time must be HH:MM' }; const h = +m[1], mn = +m[2]; if (h > 23 || mn > 59) return { error: 'time must be HH:MM' }; - d.startMin = h * 60 + mn; + if (d.isArrival && d.arrLandingMin != null) { + // the arrival day's start is DERIVED (landing → transfer → hotel → first stop); + // moving "start" moves the landing time so the whole chain stays consistent + d.arrLandingMin = h * 60 + mn; + syncArrivalStart(d); + } else d.startMin = h * 60 + mn; rebuildLegs(d); layout(d); commit(`[llm] ${d.label} starts ${fmt(d.startMin)}`); renderAll(); return { ok: true, action: 'set_start', day: d.label, start: fmt(d.startMin), day_ends: fmt(layout(d)) }; }, @@ -1018,6 +1035,25 @@ async function enrichLegs(d = day) { if (d === day) renderAll(); } } + // hotel → first stop: routed like any other leg (same router, same mode, same sanity + // guards) — the arrival day and every ordinary day get a real, editable time for it + if (d.stops.length && staysForDay(d).length === 1 && !stopAtHotel(d)) { + const f0 = d.stops[0]; + if (f0.kind !== 'region' && f0.kind !== 'transit') { + const leg = getFirstLeg(d); + if (leg) { + const want = leg.mode; + if (!(leg.conf === 3 && leg.geometry && leg._routedMode === want)) { + const h = dayBase(d).at; + const res = saneRoute(await routePts([h, f0.at], want), [h, f0.at], want); + if (legTokens[key] !== token) return; + if (res) { leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry; leg._routedMode = want; } + else { leg.conf = 1; leg.src = 'estimate — outside router coverage'; } + if (d === day) renderAll(); + } + } + } + } for (let i = 0; i < d.stops.length - 1; i++) { const a = d.stops[i], b = d.stops[i + 1]; if (a.kind === 'region' || b.kind === 'region' || !d.legs[i]) continue; @@ -1063,7 +1099,7 @@ let days = {}; let curDay, day; const stopById = id => day.stops.find(s => s.id === id); const allStops = id => M.days.flatMap(d => d.stops).find(s => s.id === id); -let markers = {}, legEls = {}, otherLayers = [], tempMarkers = [], retLine = null, retLines = [], arrLine = null, arrLines = []; +let markers = {}, legEls = {}, otherLayers = [], tempMarkers = [], retLine = null, retLines = [], arrLine = null, arrLines = [], firstLine = null, firstLines = []; let map, l3 = null, scriptBusy = false; let dragCand = null; // candidate object currently being dragged (drawer / dock) let compareItems = []; // items in the comparison dock (max 4) @@ -1168,6 +1204,39 @@ function getReturnLeg(d = day) { } return d.retLeg; } +// Hotel → first stop: the OUT-leg of a day, kept as a first-class leg object exactly +// like getReturnLeg (the in-leg), so it has its own mode, duration and route, renders +// as the same leg-row, and is click-to-edit in the same L3 card. null = there is no +// leg to render (no single base for the day, region/transit first stop). +// The hotel → first-stop leg only makes sense when the day actually STARTS at the +// base hotel. A travel day (city change) starts at the PREVIOUS city’s hotel — e.g. +// “Breakfast & check-out” in Cartagena before the flight to Santa Marta, whose stay +// is the Santa Marta hotel — so a hotel→first-stop leg from the new hotel would be a +// 500 km nonsense line. First stops beyond the city radius ⇒ the day starts elsewhere. +const LEG_CITY_KM = 25; +function firstLegApplies(d = day) { + if (d.isArrival && !d.transferIn) return false; // nothing to leave from before the arrival drop-off + if (!d.stops.length || staysForDay(d).length !== 1) return false; + const first = d.stops[0]; + if (first.kind === 'region' || first.kind === 'transit') return false; + const b = dayBase(d); + return !!(b && b.at && first.at && haversine(b.at, first.at) <= LEG_CITY_KM * 1000); +} +function getFirstLeg(d = day) { + if (!firstLegApplies(d)) { delete d.firstLeg; return null; } // self-heal a stale leg from an earlier layout + const first = d.stops[0], b = dayBase(d); + if (!d.firstLeg) { + const mode = d.firstMode || travelMode; + d.firstLeg = { mode, dur: estLegMin(mode, b.at, first.at), conf: 1 }; + delete d.firstMode; + } + return d.firstLeg; +} +const stopAtHotel = (d = day) => firstLegApplies(d) && haversine(dayBase(d).at, d.stops[0].at) < 120; +function firstLegDur(d = day) { + const leg = getFirstLeg(d); + return (leg && !stopAtHotel(d)) ? leg.dur : 0; +} function returnDur(d = day) { if (d.isDeparture) return 0; // departure day: the train is the end of the day const last = d.stops[d.stops.length - 1]; @@ -1180,7 +1249,11 @@ function returnDur(d = day) { return Math.max(1, Math.ceil(Math.max(...list.map(o => haversine(last.at, o.at))) / 1000 / (legSpeedKmh(travelMode) / 60))); } function layout(d = day) { - let t = d.startMin; + // Arrival day: startMin is the time you reach the HOTEL (landing + transfer); the + // hotel → first-stop leg is added on top. Other days: startMin is the time you are + // AT the first stop — the hotel leg happens before it (shown as the "depart HH:MM" + // hotel node), so it never shifts the stop times. + let t = d.startMin + (d.isArrival ? firstLegDur(d) : 0); d.stops.forEach((s, i) => { s.start = t; t += s.dur; if (d.legs[i]) t += d.legs[i].dur; }); return t + returnDur(d); } @@ -1192,6 +1265,9 @@ function rebuildLegs(d = day, enrich = true) { delete d.retRouted; delete d.retLeg; if (prevRet) d.retMode = prevRet; + const prevFirst = d.firstLeg && (d.firstLeg.mode === 'foot' || d.firstLeg.mode === 'car') ? d.firstLeg.mode : null; // and the out-leg's choice + delete d.firstLeg; + if (prevFirst) d.firstMode = prevFirst; for (let i = 0; i < d.stops.length - 1; i++) { const a = d.stops[i], b = d.stops[i + 1]; // a vague (region) stop at either end → worst-case leg until refined @@ -1252,7 +1328,7 @@ function renderMap() { Object.values(markers).forEach(m => map.removeLayer(m)); markers = {}; Object.values(legEls).forEach(l => map.removeLayer(l)); legEls = {}; otherLayers.forEach(l => map.removeLayer(l)); otherLayers = []; - retLine = null; retLines = []; arrLine = null; arrLines = []; + retLine = null; retLines = []; arrLine = null; arrLines = []; firstLine = null; firstLines = []; // every day, faintly — the whole trip is always visible M.days.forEach(d => { const obj = days[d.id]; @@ -1286,6 +1362,16 @@ function renderMap() { retLines.push(retLine); }); } + const flg = getFirstLeg(obj); // hotel → first stop: drawn like the other legs, coloured by mode + if (pts.length && !obj.isDeparture && flg && !stopAtHotel(obj)) { + const hb = dayBase(obj); + if (hb && hb.at) { + const g = flg.geometry || [hb.at, pts[0]]; + firstLine = L.polyline(g, { color: flg.mode === 'foot' ? '#5a7d5a' : '#274b8f', weight: 4, opacity: .85, dashArray: '5 5' }).addTo(map); + otherLayers.push(firstLine); + firstLines.push(firstLine); + } + } if (obj.isArrival && obj.transferIn && obj.transferIn.at && obj.transferIn.hotelAt) { // airport → hotel drop-off: a first-class leg, drawn from its own routed geometry const t = obj.transferIn; const pts = t.geometry || [t.at, t.hotelAt]; @@ -1437,7 +1523,22 @@ function renderStrip() { track.append(b); }; let cur = t0; - if (cur < day.startMin) block(cur, day.startMin - cur, 'gap', '', 'morning free'); + const pre = firstLegDur(); // hotel → first stop + if (day.isArrival) { + if (cur < day.startMin) block(cur, day.startMin - cur, 'gap', '', 'landing + drop-off'); + cur = day.startMin; + if (pre) { block(cur, pre, 'leg', '', `to first stop · ${pre} min`); cur += pre; } + } else { + const leaveAt = day.startMin - pre; // the hotel leg is BEFORE startMin + if (leaveAt <= t0) { + if (cur < day.startMin) block(cur, day.startMin - cur, 'gap', '', pre ? `morning free + ${pre}′ from hotel` : 'morning free'); + cur = day.startMin; + } else { + if (cur < leaveAt) block(cur, leaveAt - cur, 'gap', '', 'morning free'); + if (pre) block(leaveAt, pre, 'leg', '', `leave hotel · ${pre} min`); + cur = day.startMin; + } + } day.stops.forEach((s, i) => { if (cur < s.start) block(cur, s.start - cur, 'gap', '', 'free time'); const kindCls = s.kind === 'meal' ? 'meal' : s.kind === 'transit' ? 'transit' : s.kind === 'region' ? 'region' : 'visit'; @@ -1703,8 +1804,31 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') { if (disc) function renderRail(flashIds = []) { layout(); // assign start times before the cards render them const body = $('#rail-body'); body.innerHTML = ''; - // a travel day OPENS with the arrival: airport card, then the ride in to drop bags - if (day.isArrival && day.arrivalFlight) renderArrivalHeader(body); + // every day lives between two hotel nodes: you LEAVE the hotel (or land and drop + // bags on the travel day), move to the first stop, and come back at the end. Each + // movement is a leg row — identical in style and behaviour to the stop-to-stop ones. + if (day.isArrival && day.arrivalFlight) { + renderArrivalFlightCard(body); // ✈️ landed + const t = day.transferIn; + if (t && t.at && t.hotelAt) + renderGroundLegRow(body, { + leg: t, ico: MODE_ICO[t.mode] || '🚗', + label: `airport → ${t.hotelName || (dayBase() ? dayBase().name : 'hotel')}`, + hover: arrLines, color: t.mode === 'foot' ? '#5a7d5a' : '#274b8f', + title: 'Click to edit — drag the drop-off, walk or taxi', onClick: openArrivalEditor, + }); + renderHotelNode(body, 'arrive'); // 🏨 arrive HH:MM · drop bags + } else if (firstLegApplies()) { // travel days (city change) start elsewhere — no node + renderHotelNode(body, 'depart'); // 🏨 depart HH:MM + } + const fl = getFirstLeg(); + if (fl && day.stops.length && !stopAtHotel()) + renderGroundLegRow(body, { + leg: fl, ico: MODE_ICO[fl.mode] || '🚶', + label: `${dayBase() ? dayBase().name : 'hotel'} → ${day.stops[0].name}`, + hover: firstLines, color: fl.mode === 'foot' ? '#5a7d5a' : '#274b8f', + title: 'Click to edit — drag the route, walk or taxi', onClick: openFirstEditor, + }); day.stops.forEach((s, i) => renderStopCard(body, s, i, flashIds)); // every day but the departure closes back at the hotel — the ride back, then // the hotel card for the night, in the timeline where it belongs @@ -1715,8 +1839,9 @@ function renderRail(flashIds = []) { renderStrip(); renderBudget(); renderNudges(); renderSugg(); } -// arrival-day header: the flight you landed on, then the car to the hotel -function renderArrivalHeader(body) { +// arrival-day flight card. The airport → hotel drop-off is NOT embedded here: it is a +// stand-alone first-class leg row (renderGroundLegRow), exactly like every other leg. +function renderArrivalFlightCard(body) { const f = day.arrivalFlight; if (!f) return; const c = el('div', 'stop-card struct-card arr-card'); const top = el('div', 'sc-top'); @@ -1731,42 +1856,83 @@ function renderArrivalHeader(body) { c.append(meta); if (f.at) c.addEventListener('click', () => focusPoint(f.at)); body.append(c); - // the ride to the hotel to drop bags — a first-class leg (walk or taxi), before the first stop. - // Click it to edit the route / toggle walk vs taxi, exactly like any stop-to-stop leg. - const t = day.transferIn || {}; - const tr = el('div', 'leg-row'); - tr.append(el('span', 'leg-ico', t.mode === 'foot' ? '🚶' : '🚗')); - tr.append(document.createTextNode('to ' + (t.hotelName || (dayBase() ? dayBase().name : 'hotel')) + ' — drop bags')); - tr.append(document.createTextNode(' ')); - tr.append(tchip(t.dur || 30, t.conf || 2)); - const ac = t.mode === 'foot' ? '#5a7d5a' : '#274b8f'; - const on = { weight: 7, opacity: 1, color: ac, dashArray: null }; - const off = { color: ac, weight: 5, opacity: .85, dashArray: '6 4' }; - tr.addEventListener('mouseenter', () => arrLines.forEach(l => l.setStyle(on))); - tr.addEventListener('mouseleave', () => arrLines.forEach(l => l.setStyle(off))); - tr.addEventListener('click', () => openArrivalEditor()); - body.append(tr); +} +// every ground leg in the timeline renders through this one function — the arrival +// drop-off, hotel → first stop, and the return ride. Same row, same chip, same click +// → the same route card (drag the route, walk/taxi toggle). +function renderGroundLegRow(body, o) { + const leg = o.leg; + const lr = el('div', 'leg-row' + (o.dim ? ' dim' : '')); + lr.append(el('span', 'leg-ico', o.ico)); + lr.append(document.createTextNode(o.label + ' ')); + lr.append(tchip(leg.dur || 0, leg.conf || 2)); + if (o.title) lr.title = o.title; + const on = { weight: 7, opacity: 1, color: o.color, dashArray: null }; + const off = { color: o.color, weight: 5, opacity: .85, dashArray: '5 5' }; + if (o.hover) { + lr.addEventListener('mouseenter', () => o.hover.forEach(l => l.setStyle(on))); + lr.addEventListener('mouseleave', () => o.hover.forEach(l => l.setStyle(off))); + } + lr.addEventListener('click', o.onClick); + body.append(lr); + return lr; +} +// the hotel as a timeline NODE — "arrive HH:MM · drop bags" on the arrival day, "depart +// HH:MM" on every other day (startMin minus the hotel → first-stop leg). Same card +// family as the closing "tonight" card. +function renderHotelNode(body, phase) { + const b = dayBase(); if (!b) return; + const c = el('div', 'stop-card struct-card hotel-card'); + const top = el('div', 'sc-top'); + top.append(el('span', 'leg-ico', '🏨')); + top.append(el('span', 'sc-name', b.name)); + top.append(el('span', 'sc-times', + phase === 'arrive' ? `arrive ${fmt(day.startMin)} · drop bags` + : `depart ${fmt(Math.max(0, day.startMin - firstLegDur()))}`)); + c.append(top); + const meta = el('div', 'sc-meta'); + meta.append(el('span', 'tc t-sched', b.state === 'booked' ? 'booked' : 'base')); + meta.append(el('span', 'leg-sub', stayRange(b))); + c.append(meta); + const hm = hotelMks.filter((_, i) => staysForDay().some(s => s === stays[i])); + c.addEventListener('mouseenter', () => hm.forEach(m => flashMarker(m, true))); + c.addEventListener('mouseleave', () => hm.forEach(m => flashMarker(m, false))); + c.addEventListener('click', () => openDiscover('hotel')); + body.append(c); } -// the ride back to the hotel at the end of a (non-departure) day +// the ride back to the hotel at the end of a (non-departure) day — same row style and +// click-to-edit as every other leg function renderReturnRow(body) { - const multi = staysForDay().length > 1; const leg = getReturnLeg(day); - const ret = el('div', 'leg-row', `${MODE_ICO[(leg && leg.mode) || travelMode] || '•'}back to ${dayBase() ? dayBase().name : 'hotel'} `); - ret.append(tchip(returnDur(), multi ? 1 : (leg ? leg.conf : 3))); - if (multi) ret.append(el('span', 'leg-sub', `worst-case across ${staysForDay().length} hotels`)); - else if (leg) ret.title = 'Click to edit the ride — drag the drop-off, walk or taxi'; - const on = { weight: 7, opacity: 1, color: '#4a5160', dashArray: null }; - const off = { color: '#7a8291', weight: 4, opacity: .8, dashArray: '1 7' }; - ret.addEventListener('mouseenter', () => retLines.forEach(l => l.setStyle(on))); - ret.addEventListener('mouseleave', () => retLines.forEach(l => l.setStyle(off))); - // exactly like any stop-to-stop leg: click → route card with drag + walk/taxi toggle - if (leg) ret.addEventListener('click', () => { + if (leg) { const last = day.stops[day.stops.length - 1]; - openLegEditor(leg, last.at, dayBase().at, last.name + ' → ' + (dayBase() ? dayBase().name : 'hotel'), 'return'); - }); + renderGroundLegRow(body, { + leg, ico: MODE_ICO[leg.mode] || '🚶', + label: `${last.name} → ${dayBase() ? dayBase().name : 'hotel'}`, + hover: retLines, color: '#7a8291', + title: 'Click to edit the ride — drag the drop-off, walk or taxi', + onClick: openReturnEditor, + }); + return; + } + // no single anchor (multi-stay day or region/transit last stop): show the estimate + const ret = el('div', 'leg-row', `${MODE_ICO[travelMode] || '•'}back to ${dayBase() ? dayBase().name : 'hotel'} `); + ret.append(tchip(returnDur(), 1)); + if (staysForDay().length > 1) ret.append(el('span', 'leg-sub', `worst-case across ${staysForDay().length} hotels`)); body.append(ret); } +function openReturnEditor() { + const leg = getReturnLeg(day); if (!leg) return; + const b = dayBase(); if (!b) return; + const last = day.stops[day.stops.length - 1]; + openLegEditor(leg, last.at, b.at, `${last.name} → ${b.name}`, 'return'); +} +function openFirstEditor() { + const leg = getFirstLeg(day); if (!leg) return; + const b = dayBase(); if (!b) return; + openLegEditor(leg, b.at, day.stops[0].at, `${b.name} → ${day.stops[0].name}`, 'first'); +} // the hotel for tonight — in the timeline, after the ride back function renderHotelCard(body) { @@ -2019,7 +2185,7 @@ function renderBudget() { const prim = day.stops.filter(s => s.state === 'planned'); const flex = day.stops.length - prim.length; const planned = prim.reduce((a, s) => a + s.dur, 0) + day.legs.reduce((a, g) => a + g.dur, 0) - + (prim.length ? returnDur() : 0); + + (prim.length ? firstLegDur() + returnDur() : 0); const flexMin = day.stops.filter(s => s.state !== 'planned').reduce((a, s) => a + s.dur, 0); const end = layout(); const cap = day.wakingHours * 60; @@ -2273,14 +2439,14 @@ function setLegMode(leg, m) { leg.conf = 1; if (l3.kind === 'arrival') syncArrivalStart(day); // reflect the new (estimated) transfer right away renderRail(); renderL3Mode(leg); - if (l3.kind === 'arrival') renderMap(); // keep the drop-off line colour (walk=green, taxi=blue) in sync + if (l3.kind === 'arrival' || l3.kind === 'first') renderMap(); // keep the leg line colour (walk=green, taxi=blue) in sync const t = (leg._rt = (leg._rt || 0) + 1); routePts(leg._pts || [l3.from, l3.to], m).then(r => { const res = saneRoute(r, leg._pts || [l3.from, l3.to], m); if (leg._rt !== t || !l3 || l3.leg !== leg) return; if (res) { leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry; leg._routedMode = m; } if (l3.kind === 'arrival') syncArrivalStart(day); // and again once the real route lands renderRail(); - if (l3.kind === 'arrival') renderMap(); + if (l3.kind === 'arrival' || l3.kind === 'first') renderMap(); l3.refreshCard(); }); } @@ -2504,7 +2670,7 @@ $('#scope-trip').onclick = () => setScope('trip'); function dayStats(d) { const planned = d.stops.filter(s => s.state === 'planned'); const dur = planned.reduce((a, s) => a + s.dur, 0) + d.legs.reduce((a, g) => a + g.dur, 0) - + (planned.length && !d.isDeparture ? returnDur(d) : 0); + + (planned.length ? firstLegDur(d) + (d.isDeparture ? 0 : returnDur(d)) : 0); return { count: planned.length, dur, end: layout(d), price: planned.reduce((a, s) => a + (s.price?.amount || 0), 0), flex: d.stops.filter(s => s.state !== 'planned').length, cap: d.wakingHours * 60 }; @@ -2967,6 +3133,7 @@ function setTravelMode(m) { M.days.forEach(md => { const d = days[md.id]; if (!d) return; (d.legs || []).forEach(g => { if (g && (g.mode === 'foot' || g.mode === 'car' || g.vague)) g.mode = m; }); + if (d.firstLeg) { d.firstLeg.mode = m; d.firstLeg._routedMode = null; } if (d.transferIn && d.transferIn.at) { d.transferIn.mode = m; d.transferIn._routedMode = null; } if (d.retLeg) { d.retLeg.mode = m; d.retLeg._routedMode = null; } // the return ride follows the global choice (rebuildLegs below carries the mode) rebuildLegs(d, false); @@ -2974,7 +3141,7 @@ function setTravelMode(m) { renderAll(); if (day) enrichLegs(day); renderModeToggle(); - if (l3) (l3.kind === 'arrival' ? openArrivalEditor() : openL3(l3.idx)); // rebind the open route card to the re-estimated leg + if (l3) (l3.kind === 'arrival' ? openArrivalEditor() : l3.kind === 'return' ? openReturnEditor() : l3.kind === 'first' ? openFirstEditor() : openL3(l3.idx)); // rebind the open route card to the re-estimated leg } function openDetail(s, actions) { diff --git a/mock/data.js b/mock/data.js index 83a2be3..b3831ca 100644 --- a/mock/data.js +++ b/mock/data.js @@ -686,7 +686,7 @@ const COLOMBIA = { ] } }, { id: 'S14', name: 'Castillo San Diego', kind: 'visit', slot: 'visit', state: 'planned', url: 'https://en.wikipedia.org/wiki/Castillo_San_Diego', - at: [10.42720, -75.55370], dur: 75, durConf: 3, + at: [10.42050, -75.55170], dur: 75, durConf: 3, suggested: { value: 75, conf: 2, src: 'cartagenatourism.com' }, price: { amount: 8, cur: '$', conf: 2, src: 'cartagenatourism.com' }, img: ['⚓', 'Castillo San Diego', 'linear-gradient(135deg,#3c8f8a,#1f5c59)'], @@ -703,7 +703,7 @@ const COLOMBIA = { summary: 'Seafood on the Bocagrande malecón — the langosta is the plate, the sunset the side order. Arrive 12:45 for the table with the water view before the 14:00 heat makes the terrace empty.', links: [ { label: 'TripAdvisor', href: 'https://www.tripadvisor.com' } ] } }, { id: 'S16', name: 'Malecón de Bocagrande', kind: 'visit', slot: 'visit', state: 'planned', - at: [10.4365, -75.5390], dur: 60, durConf: 3, + at: [10.43545, -75.53900], dur: 60, durConf: 3, suggested: { value: 60, conf: 2, src: null }, price: null, img: ['🌊', 'Malecón de Bocagrande', 'linear-gradient(135deg,#4f9ea0,#2c6b6d)'], @@ -723,7 +723,7 @@ const COLOMBIA = { suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } }, { id: 'S18', name: 'Punta Canoa', kind: 'visit', slot: 'visit', state: 'planned', url: 'https://en.wikipedia.org/wiki/Punta_Canoa', - at: [10.44600, -75.53100], dur: 90, durConf: 3, + at: [10.44030, -75.53000], dur: 90, durConf: 3, suggested: { value: 90, conf: 2, src: 'cartagenatourism.com' }, price: null, img: ['🏖️', 'Punta Canoa', 'linear-gradient(135deg,#3cb5c9,#1f7a8a)'], @@ -732,7 +732,7 @@ const COLOMBIA = { links: [ { label: 'Punta Canoa', href: 'https://en.wikipedia.org/wiki/Punta_Canoa' } ] } }, { id: 'S19', name: 'Lunch — beachside at Punta Canoa', kind: 'meal', mealKind: 'lunch', slot: 'lunch', state: 'planned', url: 'https://www.tripadvisor.com', - at: [10.44550, -75.53150], dur: 75, durConf: 3, + at: [10.44020, -75.52940], dur: 75, durConf: 3, suggested: { value: 75, conf: 2, src: 'tripadvisor.com' }, price: { amount: 15, cur: '$', conf: 2, src: 'tripadvisor.com' }, img: ['🍤', 'Punta Canoa beach', 'linear-gradient(135deg,#c9803c,#8a541f)'],