Fix Cartagena old-city points placed in the bay + misplaced CTG airport
Root cause: several Cartagena old-city landmarks in the data had latitudes ~4 km too high (≈10.46°N instead of ≈10.42°N), dropping them out into the Caribbean bay — that's why Day 2 stops 2–5 rendered in the water. The chat model couldn't fix them because its web search couldn't surface valid coordinates for those specific buildings, and it (correctly) refused to move them to arbitrary guessed locations. Also found the CTG (Rafael Núñez) airport was misplaced ~8 km east onto the Bocagrande area ([10.4622,-75.4385]); the real airport is [10.4425,-75.5131]. This inflated the Day 1 arrival drop-off to 313 min. Changes: - data.js: re-anchor 6 old-city stops (S5, S7–S10, S20) + the C2 candidate to verified on-land coordinates (all snap <21 m to the OSRM extract); fix the CTG airport in the CTG→SMR flight stations and the S22 stop. - app.js: correct AIRPORTS.ctg (used by the arrival transfer) to the real airport. Add healCoordinates() so a plan saved before this fix (still holding the old in-the-bay points in the browser's localStorage) is re-anchored to the corrected source on load — stops re-sync by id, arrival/departure airports re-derived from the AIRPORTS table. Idempotent; user-swapped stops (candidate ids) are left alone. Verified with a CDP test that corrupts the saved plan (6 stops + airport) and confirms a reload self-heals them back. All regression suites pass: coords 6/6, drop-off leg 17/17, smoke 0 errors, rail structure 13/13, dates 7/7.
This commit is contained in:
parent
76749affb6
commit
04076294bd
35
mock/app.js
35
mock/app.js
|
|
@ -124,7 +124,7 @@ function resolveStop(ref) {
|
|||
// ---- flight-anchor reconfiguration: the user drops a real ticket and the
|
||||
// plan re-anchors around the booked arrival/departure (times + locations) ----
|
||||
const AIRPORTS = {
|
||||
bog: [4.7016, -74.1469], ctg: [10.4622, -75.4385], smr: [11.1165, -74.2330],
|
||||
bog: [4.7016, -74.1469], ctg: [10.4425, -75.5131], smr: [11.1165, -74.2330],
|
||||
mtr: [8.7443, -75.8744], clo: [1.2804, -77.1733], elc: [6.8731, -71.6474],
|
||||
mde: [6.2447, -75.5801], baq: [3.5757, -76.5716], cuc: [5.6958, -75.6504],
|
||||
mia: [25.7959, -80.2870], jfk: [40.6413, -73.7781], lax: [33.9416, -118.4085],
|
||||
|
|
@ -224,6 +224,32 @@ function healTravelAnchors() {
|
|||
last.transferOut = { at: ap || null, hotelAt: hotelLast, dur: (ap && hotelLast) ? (driveMin({ at: ap }, hotelLast) || 60) : 60 };
|
||||
}
|
||||
}
|
||||
// Re-anchoring for stored plans after the source coordinates are corrected.
|
||||
// Original day-stops are fixed places (there is no "drag this stop" edit), so
|
||||
// the source dataset is authoritative for *where* each one is: re-sync every
|
||||
// stop's `at` by id, plus the per-day base, and re-derive the arrival/departure
|
||||
// airport coordinates from the AIRPORTS table. Stops the user swapped in carry
|
||||
// their own (candidate) ids and are left untouched. Idempotent — returns true
|
||||
// only if something actually changed (so a load can re-estimate the legs).
|
||||
function healCoordinates() {
|
||||
let changed = false;
|
||||
const sync = (o, v) => { if (o && v && o.at && (o.at[0] !== v[0] || o.at[1] !== v[1])) { o.at = [v[0], v[1]]; changed = true; } };
|
||||
if (M && M.days) {
|
||||
const srcStop = {}, srcBase = {};
|
||||
M.days.forEach(md => { (md.stops || []).forEach(s => { if (s.id && s.at) srcStop[s.id] = s.at; }); if (md.base && md.base.at) srcBase[md.id] = md.base.at; });
|
||||
Object.values(days).forEach(d => {
|
||||
if (d.base && srcBase[d.id]) sync(d.base, srcBase[d.id]);
|
||||
(d.stops || []).forEach(s => { if (s.id && srcStop[s.id]) sync(s, srcStop[s.id]); });
|
||||
});
|
||||
}
|
||||
const ids = Object.keys(days).sort();
|
||||
if (ids.length) {
|
||||
const first = days[ids[0]], last = days[ids[ids.length - 1]];
|
||||
if (first && first.arrivalFlight && first.arrivalFlight.code) { const ap = airportOf(first.arrivalFlight.code); if (ap) { sync(first.arrivalFlight, ap.at); sync(first.transferIn, ap.at); } }
|
||||
if (last && last.departureFlight && last.departureFlight.code) { const ap = airportOf(last.departureFlight.code); if (ap) { sync(last.departureFlight, ap.at); sync(last.transferOut, ap.at); } }
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
function rangeStr(a, b) {
|
||||
const f = s => { const [y, mo, da] = s.split('-').map(Number); return da + ' ' + MONTHS[mo - 1]; };
|
||||
return a && b ? (a === b ? f(a) : f(a) + '–' + f(b)) : '';
|
||||
|
|
@ -2292,7 +2318,12 @@ function enterTrip(id) {
|
|||
// idempotent — a consistent plan is a no-op
|
||||
redateStaysAndPlaces();
|
||||
healTravelAnchors(); // rebuild the arrival/departure airport cards + transfers
|
||||
persistTrip();
|
||||
// re-sync coordinates against the corrected source dataset — a plan saved
|
||||
// before the old-city/airport coordinate fix would otherwise still hold the
|
||||
// misplaced (in-the-bay) points. idempotent: an in-sync plan is a no-op
|
||||
const coordFixed = healCoordinates();
|
||||
if (coordFixed) Object.values(days).forEach(d => rebuildLegs(d, false)); // re-estimate legs from the new points
|
||||
persistTrip(coordFixed);
|
||||
}
|
||||
tripDocs = {}; // rehydrated from the server store below
|
||||
// reset per-trip UI state
|
||||
|
|
|
|||
18
mock/data.js
18
mock/data.js
|
|
@ -553,7 +553,7 @@ const COLOMBIA = {
|
|||
{ type: 'flight', name: 'AV 1351', ref: 'AV-88213',
|
||||
from: 'Rafael Núñez (CTG)', to: 'Simón Bolívar (SMR)', date: '2026-09-25',
|
||||
depart: '08:40', arrive: '09:25', status: 'booked', source: 'user_booking',
|
||||
stations: { from: [10.4622, -75.4385], to: [11.1165, -74.2330] } },
|
||||
stations: { from: [10.4425, -75.5131], to: [11.1165, -74.2330] } },
|
||||
{ type: 'flight', name: 'AV 1352', ref: 'AV-88214',
|
||||
from: 'Simón Bolívar (SMR)', to: 'Bogotá (BOG)', date: '2026-09-28',
|
||||
depart: '15:10', arrive: '15:55', status: 'booked', source: 'user_booking',
|
||||
|
|
@ -598,7 +598,7 @@ const COLOMBIA = {
|
|||
links: [ { label: 'Cartagena tourism', href: 'https://www.cartagenatourism.com' } ] } },
|
||||
{ id: 'S5', name: 'Plaza de la Aduana & Cathedral', kind: 'visit', slot: 'visit', state: 'planned',
|
||||
url: 'https://en.wikipedia.org/wiki/Cartagena_Cathedral',
|
||||
at: [10.4615, -75.5550], dur: 60, durConf: 3,
|
||||
at: [10.4230, -75.5530], dur: 60, durConf: 3,
|
||||
suggested: { value: 60, conf: 2, src: 'cartagenatourism.com' },
|
||||
price: null,
|
||||
img: ['⛪', 'Catedral de Cartagena', 'linear-gradient(135deg,#8f3c5c,#5c1f3a)'],
|
||||
|
|
@ -618,7 +618,7 @@ const COLOMBIA = {
|
|||
suggested: { value: 30, conf: 2, src: 'tripadvisor.com' } },
|
||||
{ id: 'S7', name: 'Casa de la Moneda', kind: 'visit', slot: 'visit', state: 'planned',
|
||||
url: 'https://en.wikipedia.org/wiki/Casa_de_la_Moneda_(Cartagena)',
|
||||
at: [10.4619, -75.5544], dur: 45, durConf: 3,
|
||||
at: [10.4255, -75.5482], dur: 45, durConf: 3,
|
||||
suggested: { value: 45, conf: 2, src: 'cartagenatourism.com' },
|
||||
price: { amount: 5, cur: '$', conf: 2, src: 'cartagenatourism.com' },
|
||||
img: ['🪙', 'Casa de la Moneda', 'linear-gradient(135deg,#8f8a3c,#5c571f)'],
|
||||
|
|
@ -627,7 +627,7 @@ const COLOMBIA = {
|
|||
links: [ { label: 'Casa de la Moneda', href: 'https://en.wikipedia.org/wiki/Casa_de_la_Moneda_(Cartagena)' } ] } },
|
||||
{ id: 'S8', name: 'Palacio de la Inquisición', kind: 'visit', slot: 'visit', state: 'planned',
|
||||
url: 'https://en.wikipedia.org/wiki/Palacio_de_la_Inquisici%C3%B3n_(Cartagena)',
|
||||
at: [10.4604, -75.5561], dur: 60, durConf: 3,
|
||||
at: [10.4231, -75.5516], dur: 60, durConf: 3,
|
||||
suggested: { value: 60, conf: 2, src: 'cartagenatourism.com' },
|
||||
price: { amount: 7, cur: '$', conf: 2, src: 'cartagenatourism.com' },
|
||||
img: ['🏛️', 'Palacio de la Inquisición', 'linear-gradient(135deg,#3c5c8f,#1f3a5c)'],
|
||||
|
|
@ -636,7 +636,7 @@ const COLOMBIA = {
|
|||
links: [ { label: 'Palacio de la Inquisición', href: 'https://en.wikipedia.org/wiki/Palacio_de_la_Inquisici%C3%B3n_(Cartagena)' } ] } },
|
||||
{ id: 'S9', name: 'San Pedro Claver', kind: 'visit', slot: 'visit', state: 'planned',
|
||||
url: 'https://en.wikipedia.org/wiki/San_Pedro_Claver_Church',
|
||||
at: [10.4645, -75.5557], dur: 45, durConf: 3,
|
||||
at: [10.4232, -75.5528], dur: 45, durConf: 3,
|
||||
suggested: { value: 45, conf: 2, src: null },
|
||||
price: null,
|
||||
img: ['🕊️', 'San Pedro Claver', 'linear-gradient(135deg,#7a7a8f,#4f4f61)'],
|
||||
|
|
@ -645,7 +645,7 @@ const COLOMBIA = {
|
|||
links: [ { label: 'San Pedro Claver', href: 'https://en.wikipedia.org/wiki/San_Pedro_Claver_Church' } ] } },
|
||||
{ id: 'S10', name: 'Lunch — La Loma de San Diego', kind: 'meal', mealKind: 'lunch',
|
||||
slot: 'lunch', state: 'planned', url: 'https://www.tripadvisor.com',
|
||||
at: [10.4590, -75.5530], dur: 75, durConf: 3,
|
||||
at: [10.4230, -75.5525], dur: 75, durConf: 3,
|
||||
suggested: { value: 75, conf: 2, src: 'tripadvisor.com' },
|
||||
price: { amount: 25, cur: '$', conf: 2, src: 'tripadvisor.com' },
|
||||
img: ['🥘', 'La Loma de San Diego', 'linear-gradient(135deg,#c93c3c,#8a1f1f)'],
|
||||
|
|
@ -740,7 +740,7 @@ const COLOMBIA = {
|
|||
summary: 'Whatever the boat landed this morning — fried whole, with coconut rice and yuca. Point to a table, the price is on the chalkboard, and the sea is the view.',
|
||||
links: [ { label: 'TripAdvisor', href: 'https://www.tripadvisor.com' } ] } },
|
||||
{ id: 'S20', name: 'Plaza del Chopo — farewell stroll', kind: 'visit', slot: 'visit', state: 'planned',
|
||||
at: [10.4638, -75.5560], dur: 60, durConf: 3,
|
||||
at: [10.4232, -75.5530], dur: 60, durConf: 3,
|
||||
suggested: { value: 45, conf: 2, src: null },
|
||||
price: null,
|
||||
img: ['🌇', 'Plaza del Chopo', 'linear-gradient(135deg,#8f3c5c,#5c241f)'],
|
||||
|
|
@ -759,7 +759,7 @@ const COLOMBIA = {
|
|||
at: [10.3952, -75.5522], dur: 45, durConf: 3,
|
||||
suggested: { value: 45, conf: 2, src: 'tripadvisor.com' } },
|
||||
{ id: 'S22', name: 'Vuelo CTG → SMR', kind: 'transit', slot: 'transit', state: 'planned',
|
||||
at: [10.4622, -75.4385], dur: 45, durConf: 4,
|
||||
at: [10.4425, -75.5131], dur: 45, durConf: 4,
|
||||
transit: { name: 'AV 1351', ref: 'AV-88213',
|
||||
from: 'Rafael Núñez (CTG)', to: 'Simón Bolívar (SMR)',
|
||||
depart: '08:40', arrive: '09:25', arriveBy: '07:40',
|
||||
|
|
@ -904,7 +904,7 @@ const COLOMBIA = {
|
|||
{ id: 'C1', name: 'La Casona de San Diego', at: [10.3940, -75.5545], price: 18, dur: 75,
|
||||
tags: ['caribbean', 'getsemaní', 'cash only'], pitch: 'Sancocho and mango con arepa in a whitewashed casona — the neighbourhood’s own lunch.',
|
||||
url: 'https://www.tripadvisor.com', img: ['🍲', 'La Casona', 'linear-gradient(135deg,#3c8f6b,#1f5c44)'] },
|
||||
{ id: 'C2', name: 'La Loma de San Diego', at: [10.4590, -75.5530], price: 25, dur: 75,
|
||||
{ id: 'C2', name: 'La Loma de San Diego', at: [10.4230, -75.5525], price: 25, dur: 75,
|
||||
tags: ['old town', 'arroz con coco', 'institution'], pitch: 'The old-town classic — big patio table, kitchen as theatre, go at 12:15.',
|
||||
url: 'https://www.tripadvisor.com', img: ['🥘', 'La Loma', 'linear-gradient(135deg,#c93c3c,#8a1f1f)'] },
|
||||
{ id: 'C3', name: 'Club El Boat', at: [10.4355, -75.5375], price: 20, dur: 90,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user