// Scenic waypoints editor used inside the day editor for transport entries. // Geocode-backed search (mirrors the day editor's location field) to append // ordered via-points, each removable and reorderable with up/down buttons. // Kept as its own module so dayEditor.js stays small. import { el, clear, toast } from '../dom.js'; import { api } from '../api.js'; const MAX_WAYPOINTS = 8; // createWaypoints({ initialWaypoints, onChange }) -> { node, read(), load(), hasWaypoints() } // read() returns { waypoints: [...] } — an array, possibly empty. export function createWaypoints({ initialWaypoints = null, onChange = () => {} } = {}) { const points = []; // { lat, lng, name } const listEl = el('div', { class: 'wp-list' }); const searchInput = el('input', { class: 'input', type: 'text', placeholder: 'Search a scenic detour (OpenStreetMap)…', autocomplete: 'off', }); const resultsBox = el('div', { class: 'loc-results' }); const searchField = el('div', { class: 'wp-search' }, searchInput, resultsBox); const maxHint = el('p', { class: 'hint muted wp-max-hint' }, 'Maximum of 8 waypoints reached.'); const node = el( 'div', { class: 'wp-section' }, el('div', { class: 'cost-heading' }, 'Scenic waypoints (optional)'), el('p', { class: 'hint muted' }, 'Add via-points to route a drive through a scenic detour.'), listEl, searchField, maxHint, ); function renderRows() { clear(listEl); points.forEach((p, i) => { const upBtn = el('button', { class: 'icon-btn wp-move', type: 'button', title: 'Move up', disabled: i === 0, onClick: () => { [points[i - 1], points[i]] = [points[i], points[i - 1]]; renderRows(); onChange(); }, }, '↑'); const downBtn = el('button', { class: 'icon-btn wp-move', type: 'button', title: 'Move down', disabled: i === points.length - 1, onClick: () => { [points[i + 1], points[i]] = [points[i], points[i + 1]]; renderRows(); onChange(); }, }, '↓'); const removeBtn = el('button', { class: 'icon-btn danger', type: 'button', title: 'Remove waypoint', onClick: () => { points.splice(i, 1); renderRows(); onChange(); }, }, '×'); listEl.appendChild( el( 'div', { class: 'wp-row' }, el('span', { class: 'wp-num' }, String(i + 1)), el('span', { class: 'wp-name' }, p.name || `${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}`), el('div', { class: 'wp-row-actions' }, upBtn, downBtn, removeBtn), ), ); }); syncSearchVisibility(); } function syncSearchVisibility() { const full = points.length >= MAX_WAYPOINTS; searchField.style.display = full ? 'none' : ''; maxHint.style.display = full ? '' : 'none'; } let timer = null; let seq = 0; searchInput.addEventListener('input', () => { const q = searchInput.value.trim(); clearTimeout(timer); if (q.length < 2) { clear(resultsBox); return; } timer = setTimeout(async () => { const mySeq = ++seq; resultsBox.classList.add('loading'); try { const data = await api.geocode(q); if (mySeq !== seq) return; // a newer query superseded this one showResults(data.results || []); } catch (err) { if (mySeq !== seq) return; clear(resultsBox); toast(err.message || 'Location search failed'); } finally { resultsBox.classList.remove('loading'); } }, 400); }); function showResults(results) { clear(resultsBox); if (!results.length) { resultsBox.appendChild(el('div', { class: 'loc-empty muted' }, 'No matches')); return; } for (const r of results) { resultsBox.appendChild( el( 'button', { class: 'loc-result', type: 'button', onClick: () => { if (points.length >= MAX_WAYPOINTS) return; points.push({ lat: r.lat, lng: r.lng, name: r.name }); searchInput.value = ''; clear(resultsBox); renderRows(); onChange(); }, }, el('span', { class: 'loc-result-name' }, r.name), ), ); } } function read() { return { waypoints: points.map((p) => ({ lat: p.lat, lng: p.lng, name: p.name || undefined })) }; } // Replace all waypoints (used when editing an existing transport entry). function load(waypoints) { points.length = 0; if (Array.isArray(waypoints)) { for (const w of waypoints) points.push({ lat: w.lat, lng: w.lng, name: w.name || '' }); } renderRows(); onChange(); } if (Array.isArray(initialWaypoints)) { for (const w of initialWaypoints) points.push({ lat: w.lat, lng: w.lng, name: w.name || '' }); } renderRows(); return { node, read, load, hasWaypoints: () => points.length > 0 }; }