Fix arrival drop-off, taxi routing, and Santa Marta coverage
Four user-reported bugs from the Colombia trip: 1. Santa Marta legs not routing (extract gap). The SAMARTA OSM box had MAXLAT 11.22, which clipped the old town (~11.24 N) so every city point snapped to the same boundary node. Raised it to 11.28 (covers the Malecón + Plaza Colesio). Rebuilt the Colombia extract. 2. "Can't take a taxi / wrong times." The :5003 Colombia router is a *walking* build (foot.lua) and cannot return a driving route, so 'car' legs silently came back as long walks (CTG->hotel showed 105 min). Added a second, *driving* build (car.lua, same cropped OSM) on :5004 and made the /route proxy send driving-profile requests there. Now the airport drop-off is a real 16-min drive, and the walk<->taxi toggle changes the estimate (16 min car vs 105 min walk). 3. Arrival timeline inconsistent. The arrival day's startMin was set from a static ~20-min estimate and never updated when the drop-off actually routed. Added syncArrivalStart(): the start tracks landing + the transfer's (asynchronously routed, mode-dependent) duration, and every stop follows. Lunch no longer lands before the traveller reaches the hotel. 4. Hovering a hotel card zoomed to the other city. renderHotelCard flashed EVERY stay's marker, so a multi-city trip flew the map to the off-screen hotel (and alternated on each hover). Now it flashes only the current day's hotel(s). Also: enrichment used a single global token, so when apply_flight_anchors enriched all days in one pass only the LAST day's legs actually routed (the earlier days bailed on the token check). Made the token per-day so every day's legs route independently, and scoped the background re-renders to the on-screen day. Verified via CDP: 11/11 fix checks + cdp_smoke, cdp_struct (13), cdp_dates (7), cdp_leg (17), cdp_coords (6) all green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
04076294bd
commit
ca32d20a50
47
mock/app.js
47
mock/app.js
|
|
@ -164,6 +164,19 @@ function driveMin(ap, hotelAt) { // rough airport<->hotel taxi time
|
||||||
const km = haversine(ap.at, hotelAt) / 1000;
|
const km = haversine(ap.at, hotelAt) / 1000;
|
||||||
return Math.min(90, Math.max(20, Math.round(km / 35 * 60)));
|
return Math.min(90, Math.max(20, Math.round(km / 35 * 60)));
|
||||||
}
|
}
|
||||||
|
// An arrival day's activities can't start until the taxi from the airport has
|
||||||
|
// dropped the traveller at the hotel, so its start tracks (landing + transfer).
|
||||||
|
// The transfer is routed asynchronously and can change (routing resolves, or the
|
||||||
|
// walk<->taxi toggle), so re-sync whenever its duration changes — this keeps the
|
||||||
|
// day tab, rail start and every stop time agreeing (previously the day started
|
||||||
|
// ~20 min after landing while the drop-off took an hour, so "lunch" was scheduled
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
function relabelDay(d, idx, dateStr) {
|
function relabelDay(d, idx, dateStr) {
|
||||||
const [y, mo, da] = String(dateStr).split('-').map(Number);
|
const [y, mo, da] = String(dateStr).split('-').map(Number);
|
||||||
const dt = new Date(Date.UTC(y, mo - 1, da));
|
const dt = new Date(Date.UTC(y, mo - 1, da));
|
||||||
|
|
@ -216,6 +229,7 @@ function healTravelAnchors() {
|
||||||
first.isArrival = true;
|
first.isArrival = true;
|
||||||
first.arrivalFlight = { code: codeOf(arr.to) || codeOf(arr.name) || arr.to || 'airport', at: ap || null, time: arr.arrive || '', flight: String(arr.name || '').split(' · ')[0], from: arr.from || '', to: arr.to || '' };
|
first.arrivalFlight = { code: codeOf(arr.to) || codeOf(arr.name) || arr.to || 'airport', at: ap || null, time: arr.arrive || '', flight: String(arr.name || '').split(' · ')[0], from: arr.from || '', to: arr.to || '' };
|
||||||
first.transferIn = { at: ap || null, hotelAt: hotelFirst, dur: (ap && hotelFirst) ? (driveMin({ at: ap }, hotelFirst) || 60) : 60, mode: 'car', conf: 2, geometry: null, hotelName: hotelFirst ? hotelFirst.name : 'hotel' };
|
first.transferIn = { at: ap || null, hotelAt: hotelFirst, dur: (ap && hotelFirst) ? (driveMin({ at: ap }, hotelFirst) || 60) : 60, mode: 'car', conf: 2, geometry: null, hotelName: hotelFirst ? hotelFirst.name : 'hotel' };
|
||||||
|
first.arrLandingMin = parseHM(arr.arrive);
|
||||||
}
|
}
|
||||||
if (dep && !last.departureFlight) {
|
if (dep && !last.departureFlight) {
|
||||||
const ap = dep.stations && dep.stations.from;
|
const ap = dep.stations && dep.stations.from;
|
||||||
|
|
@ -455,11 +469,10 @@ const TOOLS = {
|
||||||
|
|
||||||
// 3) arrival day: start after landing + transfer; drop the meaningless
|
// 3) arrival day: start after landing + transfer; drop the meaningless
|
||||||
// breakfast; shorten the waking window; trim what no longer fits
|
// breakfast; shorten the waking window; trim what no longer fits
|
||||||
first.startMin = arrMin + arrTransfer;
|
first.arrLandingMin = arrMin; // landing time — startMin is derived from it + the transfer
|
||||||
|
syncArrivalStart(first); // startMin = landing + transferIn.dur (arrTransfer for now)
|
||||||
for (let i = first.stops.length - 1; i >= 0; i--)
|
for (let i = first.stops.length - 1; i >= 0; i--)
|
||||||
if (first.stops[i].slot === 'breakfast') { removed.push(first.stops[i].name); first.stops.splice(i, 1); }
|
if (first.stops[i].slot === 'breakfast') { removed.push(first.stops[i].name); first.stops.splice(i, 1); }
|
||||||
const arrEnd = Math.min(ARR_DAY_END, first.startMin + (first.wakingHours || 12) * 60);
|
|
||||||
first.wakingHours = Math.max(2, Math.round((arrEnd - first.startMin) / 60));
|
|
||||||
trimDayTo(first, first.startMin + first.wakingHours * 60, removed);
|
trimDayTo(first, first.startMin + first.wakingHours * 60, removed);
|
||||||
|
|
||||||
// 4) departure day: keep only check-out, and REPLACE the (placeholder)
|
// 4) departure day: keep only check-out, and REPLACE the (placeholder)
|
||||||
|
|
@ -628,11 +641,11 @@ async function routePts(points, mode = 'foot') {
|
||||||
};
|
};
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
let legToken = 0;
|
let legTokens = {}; // per-day enrichment token — each day enriches independently
|
||||||
// background enrichment: upgrade each straight-line leg to a real route
|
// background enrichment: upgrade each straight-line leg to a real route
|
||||||
async function enrichLegs(d = day) {
|
async function enrichLegs(d = day) {
|
||||||
if (!routerOn) return;
|
if (!routerOn) return;
|
||||||
const token = ++legToken;
|
const key = d.id, token = (legTokens[key] = (legTokens[key] || 0) + 1); // only a newer enrich OF THIS DAY cancels it
|
||||||
// the arrival drop-off (airport→hotel) is a first-class leg — route it FIRST, before the
|
// the arrival drop-off (airport→hotel) is a first-class leg — route it FIRST, before the
|
||||||
// stop-leg loop, so the stop-leg token bails below can't starve it. The value is day-scoped
|
// stop-leg loop, so the stop-leg token bails below can't starve it. The value is day-scoped
|
||||||
// and idempotent (apply even if a newer enrich started); skip when already routed in this
|
// and idempotent (apply even if a newer enrich started); skip when already routed in this
|
||||||
|
|
@ -640,9 +653,11 @@ async function enrichLegs(d = day) {
|
||||||
if (d.isArrival && d.transferIn && d.transferIn.at && d.transferIn.hotelAt) {
|
if (d.isArrival && d.transferIn && d.transferIn.at && d.transferIn.hotelAt) {
|
||||||
const t = d.transferIn, want = t.mode || 'car';
|
const t = d.transferIn, want = t.mode || 'car';
|
||||||
if (!(t.conf === 3 && t.geometry && t._routedMode === want)) {
|
if (!(t.conf === 3 && t.geometry && t._routedMode === want)) {
|
||||||
|
const before = t.dur;
|
||||||
const res = await routePts([t.at, t.hotelAt], want);
|
const res = await routePts([t.at, t.hotelAt], want);
|
||||||
if (res) { t.dur = res.durMin; t.conf = 3; t.geometry = res.geometry; t._routedMode = want; }
|
if (res) { t.dur = res.durMin; t.conf = 3; t.geometry = res.geometry; t._routedMode = want; }
|
||||||
else { t.conf = 1; }
|
else { t.conf = 1; }
|
||||||
|
if (t.dur !== before) syncArrivalStart(d); // the arrival day starts after this drop-off
|
||||||
if (l3 && l3.leg === t) l3.refreshCard && l3.refreshCard();
|
if (l3 && l3.leg === t) l3.refreshCard && l3.refreshCard();
|
||||||
if (d === day) renderAll();
|
if (d === day) renderAll();
|
||||||
}
|
}
|
||||||
|
|
@ -653,16 +668,16 @@ async function enrichLegs(d = day) {
|
||||||
// legs touching a transit stop are fixed (flight/taxi/bus), never re-routed
|
// legs touching a transit stop are fixed (flight/taxi/bus), never re-routed
|
||||||
if (a.kind === 'transit' || b.kind === 'transit') continue;
|
if (a.kind === 'transit' || b.kind === 'transit') continue;
|
||||||
const res = await routePts([a.at, b.at], d.legs[i].mode === 'multimodal' ? 'foot' : d.legs[i].mode);
|
const res = await routePts([a.at, b.at], d.legs[i].mode === 'multimodal' ? 'foot' : d.legs[i].mode);
|
||||||
if (token !== legToken) return; // the day changed mid-flight
|
if (legTokens[key] !== token) return; // this day was re-enriched mid-flight
|
||||||
const leg = d.legs[i];
|
const leg = d.legs[i];
|
||||||
if (res) {
|
if (res) {
|
||||||
const changed = leg.dur !== res.durMin || !leg.geometry;
|
const changed = leg.dur !== res.durMin || !leg.geometry;
|
||||||
leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry;
|
leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry;
|
||||||
leg.src = 'local router';
|
leg.src = 'local router';
|
||||||
if (changed) renderAll();
|
if (changed && d === day) renderAll(); // only re-render if it's still the day on screen
|
||||||
} else {
|
} else {
|
||||||
leg.conf = 1; leg.src = 'estimate — outside router coverage';
|
leg.conf = 1; leg.src = 'estimate — outside router coverage';
|
||||||
renderAll();
|
if (d === day) renderAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// return-to-hotel leg when a single stay anchors the day (uses the travel mode)
|
// return-to-hotel leg when a single stay anchors the day (uses the travel mode)
|
||||||
|
|
@ -670,8 +685,8 @@ async function enrichLegs(d = day) {
|
||||||
const last = d.stops[d.stops.length - 1];
|
const last = d.stops[d.stops.length - 1];
|
||||||
if (last.kind !== 'region' && last.kind !== 'transit') {
|
if (last.kind !== 'region' && last.kind !== 'transit') {
|
||||||
const res = await routePts([last.at, dayBase(d).at], travelMode);
|
const res = await routePts([last.at, dayBase(d).at], travelMode);
|
||||||
if (token !== legToken) return;
|
if (legTokens[key] !== token) return;
|
||||||
if (res) { d.retRouted = res; renderAll(); }
|
if (res) { d.retRouted = res; if (d === day) renderAll(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1292,8 +1307,12 @@ function renderHotelCard(body) {
|
||||||
meta.append(el('span', 'leg-sub', stayRange(b)));
|
meta.append(el('span', 'leg-sub', stayRange(b)));
|
||||||
if (n > 1) meta.append(el('span', 'leg-sub', `${n} options — worst-case anchor`));
|
if (n > 1) meta.append(el('span', 'leg-sub', `${n} options — worst-case anchor`));
|
||||||
c.append(meta);
|
c.append(meta);
|
||||||
c.addEventListener('mouseenter', () => hotelMks.forEach(m => flashMarker(m, true)));
|
// flash only THIS day's hotel(s) — flashing every stay's marker makes a
|
||||||
c.addEventListener('mouseleave', () => hotelMks.forEach(m => flashMarker(m, false)));
|
// multi-city trip fly the map to the other city's off-screen hotel (and it
|
||||||
|
// alternated back and forth on every hover).
|
||||||
|
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'));
|
c.addEventListener('click', () => openDiscover('hotel'));
|
||||||
body.append(c);
|
body.append(c);
|
||||||
}
|
}
|
||||||
|
|
@ -1774,12 +1793,14 @@ function setLegMode(leg, m) {
|
||||||
? Math.max(4, Math.round((haversine(leg._pts[0], leg._pts[1]) + haversine(leg._pts[1], leg._pts[2])) / 1000 / (legSpeedKmh(m) / 60)))
|
? Math.max(4, Math.round((haversine(leg._pts[0], leg._pts[1]) + haversine(leg._pts[1], leg._pts[2])) / 1000 / (legSpeedKmh(m) / 60)))
|
||||||
: estLegMin(m, l3.from, l3.to);
|
: estLegMin(m, l3.from, l3.to);
|
||||||
leg.conf = 1;
|
leg.conf = 1;
|
||||||
|
if (l3.kind === 'arrival') syncArrivalStart(day); // reflect the new (estimated) transfer right away
|
||||||
renderRail(); renderL3Mode(leg);
|
renderRail(); renderL3Mode(leg);
|
||||||
if (l3.kind === 'arrival') renderMap(); // keep the drop-off line colour (walk=green, taxi=blue) in sync
|
if (l3.kind === 'arrival') renderMap(); // keep the drop-off line colour (walk=green, taxi=blue) in sync
|
||||||
const t = (leg._rt = (leg._rt || 0) + 1);
|
const t = (leg._rt = (leg._rt || 0) + 1);
|
||||||
routePts(leg._pts || [l3.from, l3.to], m).then(res => {
|
routePts(leg._pts || [l3.from, l3.to], m).then(res => {
|
||||||
if (leg._rt !== t || !l3 || l3.leg !== leg) return;
|
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 (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();
|
renderRail();
|
||||||
if (l3.kind === 'arrival') renderMap();
|
if (l3.kind === 'arrival') renderMap();
|
||||||
l3.refreshCard();
|
l3.refreshCard();
|
||||||
|
|
@ -2338,7 +2359,7 @@ function enterTrip(id) {
|
||||||
const ob = $('#offline-badge'); if (ob) { ob.classList.remove('ok'); ob.textContent = badgeText(); }
|
const ob = $('#offline-badge'); if (ob) { ob.classList.remove('ok'); ob.textContent = badgeText(); }
|
||||||
$('#chat-body').innerHTML = '';
|
$('#chat-body').innerHTML = '';
|
||||||
scriptBusy = false;
|
scriptBusy = false;
|
||||||
legToken++; // drop in-flight route enrichment from the previous trip
|
legTokens = {}; // drop in-flight route enrichment from the previous trip
|
||||||
map.setView(M.trip.places[0].center, 14);
|
map.setView(M.trip.places[0].center, 14);
|
||||||
renderTripMenu();
|
renderTripMenu();
|
||||||
buildDayTabs();
|
buildDayTabs();
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ const ROUTERS = {
|
||||||
},
|
},
|
||||||
colombia: {
|
colombia: {
|
||||||
url: process.env.OSRM_COLOMBIA || 'http://localhost:5003',
|
url: process.env.OSRM_COLOMBIA || 'http://localhost:5003',
|
||||||
|
// The :5003 extract is a *walking* build (foot.lua) — it cannot produce a
|
||||||
|
// driving route, so 'car' legs would silently come back as a long walk. The
|
||||||
|
// :5004 build (car.lua, same cropped OSM) serves taxis/airport transfers.
|
||||||
|
drivingUrl: process.env.OSRM_COLOMBIA_DRIVE || 'http://localhost:5004',
|
||||||
probe: '-75.5478,10.3954;-75.5400,10.4020', // Cartagena, foot extract
|
probe: '-75.5478,10.3954;-75.5400,10.4020', // Cartagena, foot extract
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -264,7 +268,8 @@ http.createServer((req, res) => {
|
||||||
const targets = Array.from({ length: n }, (_, i) => i).join(',');
|
const targets = Array.from({ length: n }, (_, i) => i).join(',');
|
||||||
osrmPath += `?annotations=duration,distance&sources=0&targets=${targets}`;
|
osrmPath += `?annotations=duration,distance&sources=0&targets=${targets}`;
|
||||||
}
|
}
|
||||||
const up = routerFor(u).url;
|
const rt = routerFor(u);
|
||||||
|
const up = (profile === 'driving' && rt.drivingUrl) ? rt.drivingUrl : rt.url;
|
||||||
const fetchUp = fetch(up + osrmPath).then(r => r.text()).catch(() => null);
|
const fetchUp = fetch(up + osrmPath).then(r => r.text()).catch(() => null);
|
||||||
fetchUp.then(body => {
|
fetchUp.then(body => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@ ls -la "$PBF"
|
||||||
|
|
||||||
echo "=== crop to Cartagena + Santa Marta boxes (3 streaming passes, ~3 min)"
|
echo "=== crop to Cartagena + Santa Marta boxes (3 streaming passes, ~3 min)"
|
||||||
# boxes: MINLON MINLAT MAXLON MAXLAT (Cartagena city+beaches, Santa Marta+Tayrona)
|
# boxes: MINLON MINLAT MAXLON MAXLAT (Cartagena city+beaches, Santa Marta+Tayrona)
|
||||||
|
# NOTE: SAMARTA MAXLAT must stay north of the old town (~11.24 N); 11.22 clipped it and
|
||||||
|
# made every city point snap to the same boundary node. 11.28 covers Malecón + Plaza Colesio.
|
||||||
CARTAGENA="-75.66,10.26,-75.36,10.56"
|
CARTAGENA="-75.66,10.26,-75.36,10.56"
|
||||||
SAMARTA="-74.36,10.84,-74.02,11.22"
|
SAMARTA="-74.36,10.84,-74.02,11.28"
|
||||||
python3 crop_pbf.py "$PBF" /tmp/colombia-trip.pbf "$CARTAGENA" "$SAMARTA"
|
python3 crop_pbf.py "$PBF" /tmp/colombia-trip.pbf "$CARTAGENA" "$SAMARTA"
|
||||||
|
|
||||||
echo "=== extract (foot.lua) + partition + customize"
|
echo "=== extract (foot.lua) + partition + customize"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user