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;
|
||||
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) {
|
||||
const [y, mo, da] = String(dateStr).split('-').map(Number);
|
||||
const dt = new Date(Date.UTC(y, mo - 1, da));
|
||||
|
|
@ -216,6 +229,7 @@ function healTravelAnchors() {
|
|||
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.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) {
|
||||
const ap = dep.stations && dep.stations.from;
|
||||
|
|
@ -455,11 +469,10 @@ const TOOLS = {
|
|||
|
||||
// 3) arrival day: start after landing + transfer; drop the meaningless
|
||||
// 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--)
|
||||
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);
|
||||
|
||||
// 4) departure day: keep only check-out, and REPLACE the (placeholder)
|
||||
|
|
@ -628,11 +641,11 @@ async function routePts(points, mode = 'foot') {
|
|||
};
|
||||
} 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
|
||||
async function enrichLegs(d = day) {
|
||||
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
|
||||
// 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
|
||||
|
|
@ -640,9 +653,11 @@ async function enrichLegs(d = day) {
|
|||
if (d.isArrival && d.transferIn && d.transferIn.at && d.transferIn.hotelAt) {
|
||||
const t = d.transferIn, want = t.mode || 'car';
|
||||
if (!(t.conf === 3 && t.geometry && t._routedMode === want)) {
|
||||
const before = t.dur;
|
||||
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; }
|
||||
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 (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
|
||||
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);
|
||||
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];
|
||||
if (res) {
|
||||
const changed = leg.dur !== res.durMin || !leg.geometry;
|
||||
leg.dur = res.durMin; leg.conf = 3; leg.geometry = res.geometry;
|
||||
leg.src = 'local router';
|
||||
if (changed) renderAll();
|
||||
if (changed && d === day) renderAll(); // only re-render if it's still the day on screen
|
||||
} else {
|
||||
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)
|
||||
|
|
@ -670,8 +685,8 @@ async function enrichLegs(d = day) {
|
|||
const last = d.stops[d.stops.length - 1];
|
||||
if (last.kind !== 'region' && last.kind !== 'transit') {
|
||||
const res = await routePts([last.at, dayBase(d).at], travelMode);
|
||||
if (token !== legToken) return;
|
||||
if (res) { d.retRouted = res; renderAll(); }
|
||||
if (legTokens[key] !== token) return;
|
||||
if (res) { d.retRouted = res; if (d === day) renderAll(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1292,8 +1307,12 @@ function renderHotelCard(body) {
|
|||
meta.append(el('span', 'leg-sub', stayRange(b)));
|
||||
if (n > 1) meta.append(el('span', 'leg-sub', `${n} options — worst-case anchor`));
|
||||
c.append(meta);
|
||||
c.addEventListener('mouseenter', () => hotelMks.forEach(m => flashMarker(m, true)));
|
||||
c.addEventListener('mouseleave', () => hotelMks.forEach(m => flashMarker(m, false)));
|
||||
// flash only THIS day's hotel(s) — flashing every stay's marker makes a
|
||||
// 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'));
|
||||
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)))
|
||||
: estLegMin(m, l3.from, l3.to);
|
||||
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
|
||||
const t = (leg._rt = (leg._rt || 0) + 1);
|
||||
routePts(leg._pts || [l3.from, l3.to], m).then(res => {
|
||||
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();
|
||||
l3.refreshCard();
|
||||
|
|
@ -2338,7 +2359,7 @@ function enterTrip(id) {
|
|||
const ob = $('#offline-badge'); if (ob) { ob.classList.remove('ok'); ob.textContent = badgeText(); }
|
||||
$('#chat-body').innerHTML = '';
|
||||
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);
|
||||
renderTripMenu();
|
||||
buildDayTabs();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ const ROUTERS = {
|
|||
},
|
||||
colombia: {
|
||||
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
|
||||
},
|
||||
};
|
||||
|
|
@ -264,7 +268,8 @@ http.createServer((req, res) => {
|
|||
const targets = Array.from({ length: n }, (_, i) => i).join(',');
|
||||
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);
|
||||
fetchUp.then(body => {
|
||||
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)"
|
||||
# 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"
|
||||
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"
|
||||
|
||||
echo "=== extract (foot.lua) + partition + customize"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user