From 7bdb455074535fc098086a59b29be16200ce86ba Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 9 Sep 2026 17:56:58 -0400 Subject: [PATCH] Route card: put walk/taxi on the leg popup; make the drop-off ride hoverable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes from the review of the rail rework: - The airport→hotel "drop bags" ride was a special case: it was a structural row with no map line and no hover. It now draws a real polyline (airport → hotel) in renderMap and the rail row highlights it on hover / flies to it on click, exactly like every other route. - The walk/taxi toggle no longer sits as a permanent "Getting around" block at the top of the rail. It lives on the route card — the popup a leg click opens (#l3-mode). It shows only for on-ground legs (a flight / train+bus segment has a fixed mode); flipping it re-estimates every on-ground leg and re-binds the open editor to the new leg (l3.idx). The L3 waypoint-drag handler now routes at the leg's actual mode instead of hardcoding 'foot'. Verified: CDP — drop-off line drawn + hover highlights then restores (weight 5→7→5); clicking a leg opens the card with the toggle; Taxi flips travelMode + shortens legs, card stays open + rebound (idx preserved); back to foot restores; top toggle gone; 0 JS exceptions. Structural suite 13/13 still green. --- mock/app.js | 56 +++++++++++++++++++++++++++++++++++-------------- mock/index.html | 8 ++++++- mock/styles.css | 9 ++++---- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/mock/app.js b/mock/app.js index 046a41c..9d371ba 100644 --- a/mock/app.js +++ b/mock/app.js @@ -642,7 +642,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 = []; +let markers = {}, legEls = {}, otherLayers = [], tempMarkers = [], retLine = null, retLines = [], arrLine = null, arrLines = []; 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) @@ -805,7 +805,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 = []; + retLine = null; retLines = []; arrLine = null; arrLines = []; // every day, faintly — the whole trip is always visible M.days.forEach(d => { const obj = days[d.id]; @@ -839,6 +839,14 @@ function renderMap() { retLines.push(retLine); }); } + if (obj.isArrival && obj.arrivalFlight && obj.arrivalFlight.at) { // airport → hotel drop-off (hoverable via its rail row, like any other route) + const h = dayBase(obj); + if (h) { + arrLine = L.polyline([obj.arrivalFlight.at, h.at], { color: '#274b8f', weight: 5, opacity: .85, dashArray: '6 4' }).addTo(map); + otherLayers.push(arrLine); + arrLines.push(arrLine); + } + } obj.stops.forEach((s, i) => { const dim = s.state !== 'planned'; markers[s.id] = L.marker(s.at, { @@ -1205,6 +1213,13 @@ function renderArrivalHeader(body) { tr.append(document.createTextNode('to ' + (dayBase() ? dayBase().name : 'hotel') + ' — drop bags')); tr.append(document.createTextNode(' ')); tr.append(tchip(t.dur || 30, 2)); + // a real route like any other: hovering highlights it on the map, clicking + // flies there. (Always by car — you taxi in from the airport.) + const on = { weight: 7, opacity: 1, color: '#274b8f', dashArray: null }; + const off = { color: '#274b8f', 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', () => { const h = dayBase(); if (f.at && h) map.fitBounds(L.latLngBounds([f.at, h.at]).pad(0.5)); }); body.append(tr); } @@ -1644,12 +1659,12 @@ function openL3(legIdx) { const p1 = m1.getLatLng(), p2 = m2.getLatLng(), p3 = m3.getLatLng(); line.setLatLngs([[p1.lat, p1.lng], [p2.lat, p2.lng], [p3.lat, p3.lng]]); const dist = haversine([p1.lat, p1.lng], [p2.lat, p2.lng]) + haversine([p2.lat, p2.lng], [p3.lat, p3.lng]); - leg.dur = Math.max(4, Math.round(dist / 4800 * 60)); // instant straight-line estimate + leg.dur = Math.max(4, Math.round(dist / 1000 / (legSpeedKmh(leg.mode) / 60))); // instant straight-line estimate at the leg's mode leg.conf = 1; box.innerHTML = 'leg time: est. '; box.append(tchip(leg.dur, 1)); renderBudget(); const t = ++rt; - routePts([[p1.lat, p1.lng], [p2.lat, p2.lng], [p3.lat, p3.lng]], 'foot').then(res => { + routePts([[p1.lat, p1.lng], [p2.lat, p2.lng], [p3.lat, p3.lng]], leg.mode).then(res => { if (t !== rt || !l3) return; if (res) { leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry; line.setLatLngs(res.geometry); } box.innerHTML = 'leg time: ' + (res ? '' : 'est. '); @@ -1660,9 +1675,10 @@ function openL3(legIdx) { [m1, m2, m3].forEach(m => m.on('drag', upd)); $('#l3-title').textContent = `${day.stops[legIdx].name} → ${day.stops[legIdx + 1].name}`; box.innerHTML = 'leg time: '; box.append(tchip(leg.dur, 3)); + renderL3Mode(leg); $('#l3-editor').classList.remove('hidden'); map.fitBounds(L.latLngBounds([a, b]).pad(0.4)); - l3 = { layer }; + l3 = { layer, idx: legIdx }; const obs = setInterval(() => { if (!$('#l3-editor').classList.contains('hidden')) return; clearInterval(obs); @@ -1676,6 +1692,20 @@ function closeL3() { } $('#l3-close').onclick = closeL3; +// the walk/taxi toggle lives on the route card itself — the popup a leg click +// opens. It's shown only for on-ground legs (a flight or train+bus segment has +// a fixed mode); flipping it re-estimates every on-ground leg in the trip. +function renderL3Mode(leg) { + const box = $('#l3-mode'); if (!box) return; + const ground = !!leg && (leg.mode === 'foot' || leg.mode === 'car'); + box.classList.toggle('hidden', !ground); + if (!ground) return; + $('#l3-foot').classList.toggle('on', travelMode === 'foot'); + $('#l3-car').classList.toggle('on', travelMode === 'car'); +} +$('#l3-foot').onclick = () => setTravelMode('foot'); +$('#l3-car').onclick = () => setTravelMode('car'); + // ---------------- version history (every edit = a version) ---------------- // Both editors (user + chat) funnel through commit(): the snapshot captures // days + stays, so undo/redo/revert is a single code path. @@ -2266,17 +2296,10 @@ function startApp() { // ---------------- detail sheet ---------------- // the travel-mode toggle: on foot vs taxi between stops (the hotel itself now // lives in the day timeline as the "tonight" card, not in this block) +// the toggle UI lives on the route card (openL3); this just refreshes its +// state whenever the mode changes or the rail re-renders function renderModeToggle() { - const tb = $('#transit-block'); if (!tb) return; tb.innerHTML = ''; - const wrap = el('div', 'mode-toggle'); - wrap.append(el('span', 'mode-label', 'getting around')); - [['foot', '🚶 Walk'], ['car', '🚗 Taxi']].forEach(([m, lab]) => { - const b = el('button', 'mode-btn' + (travelMode === m ? ' on' : ''), lab); - b.title = m === 'foot' ? 'On foot between stops' : 'By taxi between stops'; - b.onclick = () => setTravelMode(m); - wrap.append(b); - }); - tb.append(wrap); + renderL3Mode(l3 && day && day.legs[l3.idx] ? day.legs[l3.idx] : null); } function setTravelMode(m) { if (m === travelMode) return; @@ -2286,9 +2309,10 @@ function setTravelMode(m) { // queued here — one fetch per day would cancel the token out — only the day // you're looking at is re-routed in the background M.days.forEach(md => { const d = days[md.id]; if (d) rebuildLegs(d, false); }); - renderModeToggle(); renderAll(); if (day) enrichLegs(day); + renderModeToggle(); + if (l3) openL3(l3.idx); // rebind the open route card to the re-estimated leg } function openDetail(s, actions) { diff --git a/mock/index.html b/mock/index.html index 680ebda..bf9c618 100644 --- a/mock/index.html +++ b/mock/index.html @@ -74,7 +74,6 @@