Route card: put walk/taxi on the leg popup; make the drop-off ride hoverable
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.
This commit is contained in:
parent
733843d096
commit
7bdb455074
56
mock/app.js
56
mock/app.js
|
|
@ -642,7 +642,7 @@ let days = {};
|
||||||
let curDay, day;
|
let curDay, day;
|
||||||
const stopById = id => day.stops.find(s => s.id === id);
|
const stopById = id => day.stops.find(s => s.id === id);
|
||||||
const allStops = id => M.days.flatMap(d => d.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 map, l3 = null, scriptBusy = false;
|
||||||
let dragCand = null; // candidate object currently being dragged (drawer / dock)
|
let dragCand = null; // candidate object currently being dragged (drawer / dock)
|
||||||
let compareItems = []; // items in the comparison dock (max 4)
|
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(markers).forEach(m => map.removeLayer(m)); markers = {};
|
||||||
Object.values(legEls).forEach(l => map.removeLayer(l)); legEls = {};
|
Object.values(legEls).forEach(l => map.removeLayer(l)); legEls = {};
|
||||||
otherLayers.forEach(l => map.removeLayer(l)); otherLayers = [];
|
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
|
// every day, faintly — the whole trip is always visible
|
||||||
M.days.forEach(d => {
|
M.days.forEach(d => {
|
||||||
const obj = days[d.id];
|
const obj = days[d.id];
|
||||||
|
|
@ -839,6 +839,14 @@ function renderMap() {
|
||||||
retLines.push(retLine);
|
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) => {
|
obj.stops.forEach((s, i) => {
|
||||||
const dim = s.state !== 'planned';
|
const dim = s.state !== 'planned';
|
||||||
markers[s.id] = L.marker(s.at, {
|
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('to ' + (dayBase() ? dayBase().name : 'hotel') + ' — drop bags'));
|
||||||
tr.append(document.createTextNode(' '));
|
tr.append(document.createTextNode(' '));
|
||||||
tr.append(tchip(t.dur || 30, 2));
|
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);
|
body.append(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1644,12 +1659,12 @@ function openL3(legIdx) {
|
||||||
const p1 = m1.getLatLng(), p2 = m2.getLatLng(), p3 = m3.getLatLng();
|
const p1 = m1.getLatLng(), p2 = m2.getLatLng(), p3 = m3.getLatLng();
|
||||||
line.setLatLngs([[p1.lat, p1.lng], [p2.lat, p2.lng], [p3.lat, p3.lng]]);
|
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]);
|
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;
|
leg.conf = 1;
|
||||||
box.innerHTML = 'leg time: est. '; box.append(tchip(leg.dur, 1));
|
box.innerHTML = 'leg time: est. '; box.append(tchip(leg.dur, 1));
|
||||||
renderBudget();
|
renderBudget();
|
||||||
const t = ++rt;
|
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 (t !== rt || !l3) return;
|
||||||
if (res) { leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry; line.setLatLngs(res.geometry); }
|
if (res) { leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry; line.setLatLngs(res.geometry); }
|
||||||
box.innerHTML = 'leg time: ' + (res ? '' : 'est. ');
|
box.innerHTML = 'leg time: ' + (res ? '' : 'est. ');
|
||||||
|
|
@ -1660,9 +1675,10 @@ function openL3(legIdx) {
|
||||||
[m1, m2, m3].forEach(m => m.on('drag', upd));
|
[m1, m2, m3].forEach(m => m.on('drag', upd));
|
||||||
$('#l3-title').textContent = `${day.stops[legIdx].name} → ${day.stops[legIdx + 1].name}`;
|
$('#l3-title').textContent = `${day.stops[legIdx].name} → ${day.stops[legIdx + 1].name}`;
|
||||||
box.innerHTML = 'leg time: '; box.append(tchip(leg.dur, 3));
|
box.innerHTML = 'leg time: '; box.append(tchip(leg.dur, 3));
|
||||||
|
renderL3Mode(leg);
|
||||||
$('#l3-editor').classList.remove('hidden');
|
$('#l3-editor').classList.remove('hidden');
|
||||||
map.fitBounds(L.latLngBounds([a, b]).pad(0.4));
|
map.fitBounds(L.latLngBounds([a, b]).pad(0.4));
|
||||||
l3 = { layer };
|
l3 = { layer, idx: legIdx };
|
||||||
const obs = setInterval(() => {
|
const obs = setInterval(() => {
|
||||||
if (!$('#l3-editor').classList.contains('hidden')) return;
|
if (!$('#l3-editor').classList.contains('hidden')) return;
|
||||||
clearInterval(obs);
|
clearInterval(obs);
|
||||||
|
|
@ -1676,6 +1692,20 @@ function closeL3() {
|
||||||
}
|
}
|
||||||
$('#l3-close').onclick = 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) ----------------
|
// ---------------- version history (every edit = a version) ----------------
|
||||||
// Both editors (user + chat) funnel through commit(): the snapshot captures
|
// Both editors (user + chat) funnel through commit(): the snapshot captures
|
||||||
// days + stays, so undo/redo/revert is a single code path.
|
// days + stays, so undo/redo/revert is a single code path.
|
||||||
|
|
@ -2266,17 +2296,10 @@ function startApp() {
|
||||||
// ---------------- detail sheet ----------------
|
// ---------------- detail sheet ----------------
|
||||||
// the travel-mode toggle: on foot vs taxi between stops (the hotel itself now
|
// 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)
|
// 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() {
|
function renderModeToggle() {
|
||||||
const tb = $('#transit-block'); if (!tb) return; tb.innerHTML = '';
|
renderL3Mode(l3 && day && day.legs[l3.idx] ? day.legs[l3.idx] : null);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
function setTravelMode(m) {
|
function setTravelMode(m) {
|
||||||
if (m === travelMode) return;
|
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
|
// queued here — one fetch per day would cancel the token out — only the day
|
||||||
// you're looking at is re-routed in the background
|
// you're looking at is re-routed in the background
|
||||||
M.days.forEach(md => { const d = days[md.id]; if (d) rebuildLegs(d, false); });
|
M.days.forEach(md => { const d = days[md.id]; if (d) rebuildLegs(d, false); });
|
||||||
renderModeToggle();
|
|
||||||
renderAll();
|
renderAll();
|
||||||
if (day) enrichLegs(day);
|
if (day) enrichLegs(day);
|
||||||
|
renderModeToggle();
|
||||||
|
if (l3) openL3(l3.idx); // rebind the open route card to the re-estimated leg
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDetail(s, actions) {
|
function openDetail(s, actions) {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@
|
||||||
<div id="app" class="hidden">
|
<div id="app" class="hidden">
|
||||||
<aside id="rail">
|
<aside id="rail">
|
||||||
<div id="daystrip"></div>
|
<div id="daystrip"></div>
|
||||||
<div class="rail-block" id="transit-block"></div>
|
|
||||||
<div class="rail-block" id="sugg-block"></div>
|
<div class="rail-block" id="sugg-block"></div>
|
||||||
<div id="rail-body"></div>
|
<div id="rail-body"></div>
|
||||||
<div class="rail-foot" id="rail-foot">
|
<div class="rail-foot" id="rail-foot">
|
||||||
|
|
@ -105,6 +104,13 @@
|
||||||
<div id="l3-editor" class="hidden">
|
<div id="l3-editor" class="hidden">
|
||||||
<div class="l3-title" id="l3-title">Route</div>
|
<div class="l3-title" id="l3-title">Route</div>
|
||||||
<div class="l3-hint">Drag the ● waypoint (or the endpoints) — the duration updates live; the local router re-routes on the road network when the area is covered.</div>
|
<div class="l3-hint">Drag the ● waypoint (or the endpoints) — the duration updates live; the local router re-routes on the road network when the area is covered.</div>
|
||||||
|
<div class="l3-mode hidden" id="l3-mode">
|
||||||
|
<span class="mode-label">getting around</span>
|
||||||
|
<div class="mode-toggle">
|
||||||
|
<button class="mode-btn" id="l3-foot" title="On foot between stops">🚶 Walk</button>
|
||||||
|
<button class="mode-btn" id="l3-car" title="By taxi between stops">🚗 Taxi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="l3-time">leg time: <span id="l3-time" class="tc t-computed">…</span></div>
|
<div class="l3-time">leg time: <span id="l3-time" class="tc t-computed">…</span></div>
|
||||||
<button class="btn ghost small" id="l3-close">Done</button>
|
<button class="btn ghost small" id="l3-close">Done</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -177,11 +177,12 @@ button { font: inherit; }
|
||||||
.leg-ico { font-size: 13px; }
|
.leg-ico { font-size: 13px; }
|
||||||
.leg-sub { font-size: 11px; color: var(--ink2); opacity: .8; }
|
.leg-sub { font-size: 11px; color: var(--ink2); opacity: .8; }
|
||||||
.leg-row.dim { opacity: .55; }
|
.leg-row.dim { opacity: .55; }
|
||||||
.leg-row.struct-leg { cursor: default; }
|
|
||||||
.leg-row.struct-leg:hover { background: transparent; }
|
|
||||||
|
|
||||||
/* travel-mode toggle (on foot vs taxi) — sits where the old base-chip did */
|
/* travel-mode toggle (on foot vs taxi) — lives on the route card (the popup
|
||||||
.mode-toggle { display: flex; align-items: center; gap: 6px; }
|
a leg click opens) */
|
||||||
|
.mode-toggle { display: flex; align-items: center; gap: 6px; flex: 1; }
|
||||||
|
.l3-mode { display: flex; align-items: center; gap: 8px; margin: 2px 0 8px; }
|
||||||
|
.l3-mode .mode-btn { padding: 5px 6px; font-size: 12px; }
|
||||||
.mode-label { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink2);
|
.mode-label { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink2);
|
||||||
margin-right: 2px; white-space: nowrap; }
|
margin-right: 2px; white-space: nowrap; }
|
||||||
.mode-btn { flex: 1; border: 1px solid var(--line); background: var(--card); color: var(--ink2);
|
.mode-btn { flex: 1; border: 1px solid var(--line); background: var(--card); color: var(--ink2);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user