diff --git a/mock/app.js b/mock/app.js index 8a4c433..5b5f380 100644 --- a/mock/app.js +++ b/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 diff --git a/mock/data.js b/mock/data.js index 1651b0c..f46e746 100644 --- a/mock/data.js +++ b/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,