1/** 2 * js/map-view/tour-launcher.js 3 * 4 * A way into the trip planner that does not require knowing to click the 5 * route line. 6 * 7 * Built 2026-08-24. Steve, who commissioned every one of these tools, 8 * said: "part of the problem is that you have to know to click on the 9 * line." That is the whole finding. The planner is good and it is 10 * invisible, because the only door to it is a gesture nobody is told 11 * about, on a feature that looks like scenery. 12 * 13 * So this is a card on the map that names the next step and opens the 14 * route when clicked. It is not a tip and not a tour: it reads the same 15 * project state the checklist does, so it can never advertise a step 16 * that is already done. 17 * 18 * WHY IT SHOWS THE STEP RATHER THAN JUST A BUTTON. A bare "Open the 19 * planner" button is another thing to be curious about. "Next: split the 20 * route into riding days" is a reason to press it, and if the rider does 21 * not want to do that right now, they have still learned where they are 22 * without opening anything. 23 * 24 * ISOLATION. Behind enable_day_stages, which is on for one live project, 25 * and behind canManageProject(). Nine projects have enable_osm_lookup, 26 * several of them client work; none of them should grow a card on the 27 * map because a bike tour needed one. 28 */ 29 30import { readTourState, tourSteps, findRouteFeature } from './tour-checklist.js'; 31import { canManageProject } from './project-role.js'; 32import { escapeHtml as esc } from '/js/utils/escape-html.js'; 33 34const EL_ID = 'wm-tour-launcher'; 35 36/* Dismissal lasts the session only. A card that stays gone forever 37 after one stray click is a support question later. */ 38let dismissed = false; 39 40function remove() { 41 document.getElementById(EL_ID)?.remove(); 42} 43 44/** 45 * Draw (or redraw) the card. 46 * 47 * Returns the element so a test can read it without a map. 48 */ 49export function renderTourLauncher() { 50 remove(); 51 if (dismissed) return null; 52 53 const route = findRouteFeature(); 54 if (!route) return null; 55 56 const state = readTourState(route); 57 const steps = tourSteps(state); 58 const idx = steps.findIndex(s => !s.done); 59 const done = idx === -1; 60 61 // Something wrong with the project outranks the next step, because it 62 // is the thing that will waste the rider's time if they carry on. 63 const problem = steps.find(s => s.warn); 64 65 const line = problem 66 ? `<span class="text-danger">${esc(problem.warn)}</span>` 67 : done 68 ? 'Every step is done. Open it to adjust stops and re-cut the days.' 69 : `Next: <strong>${esc(steps[idx].label.toLowerCase())}</strong>`; 70 71 const el = document.createElement('div'); 72 el.id = EL_ID; 73 /* One line, not a card. It was a two-line panel with a heading until 74 Steve said "the Next step can be smaller at the base" on 75 2026-08-24. The heading was repeating what the Trip tab already 76 says, and this sits over the map, where every pixel belongs to the 77 map. */ 78 el.style.cssText = ` 79 position:absolute; z-index:5; 80 left:50%; transform:translateX(-50%); bottom:30px; 81 background:rgba(255,255,255,0.94); 82 border-radius:1rem; 83 padding:4px 6px 4px 12px; 84 max-width:min(440px, calc(100vw - 32px)); 85 box-shadow:0 1px 6px rgba(0,0,0,0.18); 86 font-size:0.78rem; 87 display:flex; align-items:center; gap:8px; 88 white-space:nowrap; 89 `; 90 91 el.innerHTML = ` 92 <span class="wm-tour-launcher-step text-truncate">${line}</span> 93 <button type="button" class="btn btn-sm btn-primary flex-shrink-0 wm-tour-launcher-open" 94 style="padding:1px 10px;font-size:0.78rem;border-radius:1rem;"> 95 Plan 96 </button> 97 <button type="button" class="btn-close flex-shrink-0 wm-tour-launcher-close" 98 aria-label="Hide the planner hint" style="font-size:.5rem;"></button>`; 99 100 el.querySelector('.wm-tour-launcher-open').addEventListener('click', () => { 101 // The same door the line click uses, so there is one code path into 102 // the panel rather than a second, quietly diverging one. 103 const open = window.openFeatureInfoModal; 104 if (typeof open === 'function') open(route.id); 105 }); 106 107 el.querySelector('.wm-tour-launcher-close').addEventListener('click', () => { 108 dismissed = true; 109 remove(); 110 }); 111 112 document.getElementById('map-wrapper')?.appendChild(el); 113 return el; 114} 115
116export function initTourLauncher() { 117 try { 118 const ps = window.projectSettings || {}; 119 if (!ps.enable_day_stages) return; 120 if (!canManageProject()) return; 121 122 renderTourLauncher(); 123 124 /* Redraw when the feature panel closes, so the next step keeps up 125 with what was just done in it. */ 126 document.getElementById('feature-modal') 127 ?.addEventListener('hidden.bs.modal', () => renderTourLauncher()); 128 } catch (err) { 129 console.warn('[TourLauncher] init failed:', err.message); 130 } 131}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.