PageSourceSearch

https://bikesontransit.org/js/map-view/tour-launcher.js

js bikesontransit.org collected 2026-09-27 01:48:53 UTC 4,777 bytes, 131 lines download raw bytes

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.