PageSourceSearch

https://soundtrip-ads.com/assets/pulse-page.js

js soundtrip-ads.com collected 2026-10-01 14:27:38 UTC 22,695 bytes, 736 lines download raw bytes

1/**
2 * Sound Trip Pulse — load place pulse metrics, H3 heatmap, and demo audio.
3 * Supports full /pulse page and embedded hero panels via SoundTripPulse.createController().
4 */
5(function () {
6  'use strict';
7
8  var MAP_TILE_URL =
9    'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png?key=cb1_2y2u_1_e5c34798af34e3bfaca29519';
10  var MAP_TILE_ATTRIBUTION =
11    '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>';
12  var FILL_COLOR = '#7c5cff';
13  var ZERO_FILL = 0.055;
14  var MIN_FILL = 0.12;
15  var MAX_FILL = 0.58;
16  var GAMMA = 0.5;
17  var LOW_DATA_THRESHOLD = 20;
18  var DEFAULT_ZOOM = 14;
19  var PULSE_DISPLAY_MULTIPLIER = 4;
20
21  var DEFAULT_IDS = {
22    loading: 'pulse-loading',
23    error: 'pulse-error',
24    search: 'pulse-search',
25    content: 'pulse-content',
26    searchInput: 'pulse-search-input',
27    searchResults: 'pulse-search-results',
28    errorTitle: 'pulse-error-title',
29    errorMessage: 'pulse-error-message',
30    placeName: 'pulse-place-name',
31    placeAddress: 'pulse-place-address',
32    disclaimer: 'pulse-disclaimer',
33    statRideCount: 'stat-ride-count',
34    statUniqueDrivers: 'stat-unique-drivers',
35    statPeakHour: 'stat-peak-hour',
36    statPeakDay: 'stat-peak-day',
37    statUnmet: 'stat-unmet',
38    statPaid: 'stat-paid',
39    statRadiusHint: 'stat-radius-hint',
40    map: 'pulse-map',
41    mapLoading: 'pulse-map-loading',
42    audioContainer: 'audio-player-container',
43  };
44
45  function pulseT(key) {
46    return window.SoundTripI18n ? SoundTripI18n.t(key) : key;
47  }
48
49  function pulseTWithVars(key, vars) {
50    return window.SoundTripI18n ? SoundTripI18n.tWithVars(key, vars) : key;
51  }
52
53  function getNumberLocale() {
54    if (!window.SoundTripI18n) return 'en-US';
55    var lang = SoundTripI18n.detectLanguage();
56    if (lang === 'pt-BR') return 'pt-BR';
57    if (lang === 'es') return 'es';
58    return 'en-US';
59  }
60
61  function getSearchLanguageCode() {
62    if (!window.SoundTripI18n) return 'en';
63    var lang = SoundTripI18n.detectLanguage();
64    if (lang === 'pt-BR') return 'pt-BR';
65    if (lang === 'es') return 'es';
66    return 'en';
67  }
68
69  function isValidPlaceId(placeId) {
70    return typeof placeId === 'string' && /^[a-zA-Z0-9_-]{1,200}$/.test(placeId);
71  }
72
73  function extractPlaceId() {
74    var pathMatch = window.location.pathname.match(/\/pulse\/([^/?#]+)/);
75    if (pathMatch && pathMatch[1]) {
76      return decodeURIComponent(pathMatch[1]);
77    }
78    var params = new URLSearchParams(window.location.search);
79    return params.get('id') || params.get('placeId') || '';
80  }
81
82  function buildQueryPlaceUrl(placeId) {
83    var url = new URL(window.location.href);
84    url.searchParams.set('placeId', placeId);
85    url.searchParams.delete('id');
86    return url.pathname + url.search + url.hash;
87  }
88
89  function clearQueryPlaceUrl() {
90    var url = new URL(window.location.href);
91    url.searchParams.delete('placeId');
92    url.searchParams.delete('id');
93    var search = url.searchParams.toString();
94    return url.pathname + (search ? '?' + search : '') + url.hash;
95  }
96
97  function createPulseController(options) {
98    options = options || {};
99    var ids = Object.assign({}, DEFAULT_IDS, options.ids || {});
100    var updateHistory = options.updateHistory !== false;
101    var historyMode = options.historyMode || 'path';
102    var syncMeta = options.syncMeta !== false;
103    var hotelRedirect = options.hotelRedirect !== false;
104    var hiddenClass = options.hiddenClass || 'pulse-hidden';
105
106    var lastPulse = null;
107    var lastPlaceName = null;
108    var lastError = null;
109    var searchInitialized = false;
110    var searchTimeout = null;
111    var currentMap = null;
112
113    function el(key) {
114      return document.getElementById(ids[key]);
115    }
116
117    function show(node) {
118      if (node) node.classList.remove(hiddenClass);
119    }
120
121    function hide(node) {
122      if (node) node.classList.add(hiddenClass);
123    }
124
125    function formatNumber(n) {
126      var x = Number(n);
127      if (!Number.isFinite(x)) return '—';
128      try {
129        return x.toLocaleString(getNumberLocale());
130      } catch (e) {
131        return String(x);
132      }
133    }
134
135    function applyErrorCopy() {
136      if (!lastError) return;
137      var titleEl = el('errorTitle');
138      var msgEl = el('errorMessage');
139      if (titleEl) titleEl.removeAttribute('data-i18n');
140      if (msgEl) msgEl.removeAttribute('data-i18n');
141      if (lastError.raw) {
142        if (titleEl) titleEl.textContent = lastError.title;
143        if (msgEl) msgEl.textContent = lastError.message;
144        return;
145      }
146      if (titleEl) titleEl.textContent = pulseT(lastError.titleKey);
147      if (msgEl) {
148        msgEl.textContent = lastError.messageVars
149          ? pulseTWithVars(lastError.messageKey, lastError.messageVars)
150          : pulseT(lastError.messageKey);
151      }
152    }
153
154    function showError(titleKey, messageKey, messageVars) {
155      lastError = { titleKey: titleKey, messageKey: messageKey, messageVars: messageVars };
156      hide(el('loading'));
157      hide(el('content'));
158      hide(el('search'));
159      show(el('error'));
160      applyErrorCopy();
161    }
162
163    function showErrorRaw(title, message) {
164      lastError = { raw: true, title: title, message: message };
165      hide(el('loading'));
166      hide(el('content'));
167      hide(el('search'));
168      show(el('error'));
169      applyErrorCopy();
170    }
171
172    function updatePlaceHistory(placeId) {
173      if (!updateHistory || !placeId) return;
174      if (historyMode === 'query') {
175        window.history.replaceState({}, '', buildQueryPlaceUrl(placeId));
176        return;
177      }
178      window.history.replaceState({}, '', '/pulse/' + encodeURIComponent(placeId));
179    }
180
181    function clearPlaceHistory() {
182      if (!updateHistory) return;
183      if (historyMode === 'query') {
184        window.history.replaceState({}, '', clearQueryPlaceUrl());
185      }
186    }
187
188    function showSearch() {
189      lastError = null;
190      if (currentMap) {
191        currentMap.remove();
192        currentMap = null;
193      }
194      clearPlaceHistory();
195      hide(el('loading'));
196      hide(el('content'));
197      hide(el('error'));
198      show(el('search'));
199      initPlaceSearch();
200      var input = el('searchInput');
201      if (input) {
202        setTimeout(function () {
203          input.focus();
204        }, 0);
205      }
206    }
207
208    async function searchPlaces(query) {
209      try {
210        var res = await fetch('/api/places/search', {
211          method: 'POST',
212          headers: { 'Content-Type': 'application/json' },
213          body: JSON.stringify({
214            textQuery: query,
215            maxResultCount: 10,
216            languageCode: getSearchLanguageCode(),
217          }),
218        });
219        var json = await res.json();
220        if (json.success && json.data && json.data.places) {
221          return json.data.places;
222        }
223        return [];
224      } catch (err) {
225        console.error('Place search failed', err);
226        return [];
227      }
228    }
229
230    function beginLoad(placeId) {
231      updatePlaceHistory(placeId);
232      hide(el('search'));
233      show(el('loading'));
234    }
235
236    function displaySearchResults(places) {
237      var resultsContainer = el('searchResults');
238      if (!resultsContainer) return;
239
240      resultsContainer.textContent = '';
241
242      if (!places || !places.length) {
243        var noResults = document.createElement('div');
244        noResults.className = 'search-no-results';
245        noResults.textContent = pulseT('personalize.searchNoResults');
246        resultsContainer.appendChild(noResults);
247        resultsContainer.classList.add('show');
248        return;
249      }
250
251      places.forEach(function (place) {
252        var name = (place.displayName && place.displayName.text) || place.name || 'Unknown Place';
253        var address =
254          place.formattedAddress || place.formatted_address || 'Address unavailable';
255        var placeId = place.id || place.place_id;
256
257        var item = document.createElement('div');
258        item.className = 'search-result-item';
259        item.setAttribute('data-place-id', placeId);
260        item.setAttribute('data-place-name', name);
261
262        var nameEl = document.createElement('div');
263        nameEl.className = 'search-result-name';
264        nameEl.textContent = name;
265
266        var addressEl = document.createElement('div');
267        addressEl.className = 'search-result-address';
268        addressEl.textContent = address;
269
270        item.appendChild(nameEl);
271        item.appendChild(addressEl);
272        item.addEventListener('click', function () {
273          var selectedId = item.getAttribute('data-place-id');
274          var selectedName = item.getAttribute('data-place-name');
275          resultsContainer.classList.remove('show');
276
277          if (typeof gtag !== 'undefined') {
278            gtag('event', 'place_search', {
279              event_category: 'engagement',
280              event_label: selectedName,
281            });
282          }
283
284          beginLoad(selectedId);
285          loadPulse(selectedId);
286        });
287
288        resultsContainer.appendChild(item);
289      });
290
291      resultsContainer.classList.add('show');
292    }
293
294    function initPlaceSearch() {
295      if (searchInitialized) return;
296      searchInitialized = true;
297
298      var input = el('searchInput');
299      var resultsContainer = el('searchResults');
300      var searchSection = el('search');
301      if (!input || !resultsContainer) return;
302
303      input.addEventListener('input', function (e) {
304        var query = e.target.value.trim();
305
306        if (searchTimeout) {
307          clearTimeout(searchTimeout);
308        }
309
310        if (!query || query.length < 2) {
311          resultsContainer.classList.remove('show');
312          resultsContainer.textContent = '';
313          return;
314        }
315
316        resultsContainer.textContent = '';
317        var loading = document.createElement('div');
318        loading.className = 'search-loading';
319        loading.textContent = pulseT('pulse.search.searching');
320        resultsContainer.appendChild(loading);
321        resultsContainer.classList.add('show');
322
323        searchTimeout = setTimeout(async function () {
324          var places = await searchPlaces(query);
325          displaySearchResults(places);
326        }, 500);
327      });
328
329      input.addEventListener('focus', function () {
330        if (resultsContainer.children.length > 0 && input.value.trim().length >= 2) {
331          resultsContainer.classList.add('show');
332        }
333      });
334
335      document.addEventListener('click', function (e) {
336        if (
337          searchSection &&
338          (!searchSection.contains(e.target) || !e.target.closest('.place-search-input-wrapper'))
339        ) {
340          resultsContainer.classList.remove('show');
341        }
342      });
343    }
344
345    function computeDisplayMax(positives) {
346      var rawMax = positives.length ? positives[positives.length - 1] : 0;
347      var displayMax = Math.max(rawMax, 1);
348      if (positives.length >= 5) {
349        var pIdx = Math.min(
350          positives.length - 1,
351          Math.max(0, Math.floor(0.9 * (positives.length - 1))),
352        );
353        var p90 = positives[pIdx];
354        if (p90 > 0 && rawMax > p90 * 2.5) {
355          displayMax = Math.max(p90, 1);
356        }
357      }
358      return displayMax;
359    }
360
361    function cellActivityCount(cell) {
362      if (cell == null) return 0;
363      var activity = Number(cell.activityCount);
364      if (Number.isFinite(activity)) return activity;
365      return Number(cell.rideCount) || 0;
366    }
367
368    function renderHexCoverage(map, hexCells) {
369      if (map._hexLayerGroup) {
370        map.removeLayer(map._hexLayerGroup);
371      }
372      var hexLayerGroup = L.layerGroup();
373      var positives = [];
374      (hexCells || []).forEach(function (cell) {
375        var n = cellActivityCount(cell);
376        if (n > 0) positives.push(n);
377      });
378      positives.sort(function (a, b) {
379        return a - b;
380      });
381      var displayMax = computeDisplayMax(positives);
382
383      (hexCells || []).forEach(function (cell) {
384        var ring = cell.ring;
385        if (!ring || ring.length < 3) return;
386        var count = cellActivityCount(cell);
387        var fillOp;
388        if (count <= 0) {
389          fillOp = ZERO_FILL;
390        } else {
391          var norm = Math.min(count, displayMax) / displayMax;
392          var t = Math.pow(norm, GAMMA);
393          fillOp = MIN_FILL + t * (MAX_FILL - MIN_FILL);
394        }
395        hexLayerGroup.addLayer(
396          L.polygon(ring, {
397            stroke: false,
398            fillColor: FILL_COLOR,
399            fillOpacity: fillOp,
400            interactive: false,
401          }),
402        );
403      });
404
405      if (hexLayerGroup.getLayers().length) {
406        hexLayerGroup.addTo(map);
407      }
408      map._hexLayerGroup = hexLayerGroup;
409    }
410
411    function setMapLoading(on) {
412      var mapLoadingEl = el('mapLoading');
413      if (!mapLoadingEl) return;
414      if (on) mapLoadingEl.classList.add('is-visible');
415      else mapLoadingEl.classList.remove('is-visible');
416    }
417
418    function initMap(pulse) {
419      if (typeof L === 'undefined') {
420        setMapLoading(false);
421        return null;
422      }
423      if (currentMap) {
424        currentMap.remove();
425        currentMap = null;
426      }
427      var lat = pulse.place.latitude;
428      var lng = pulse.place.longitude;
429      var map = L.map(ids.map, {
430        zoomControl: true,
431        attributionControl: true,
432        scrollWheelZoom: false,
433      }).setView([lat, lng], DEFAULT_ZOOM);
434
435      L.tileLayer(MAP_TILE_URL, {
436        attribution: MAP_TILE_ATTRIBUTION,
437        maxZoom: 19,
438      }).addTo(map);
439
440      L.circleMarker([lat, lng], {
441        radius: 8,
442        color: '#ffffff',
443        weight: 2,
444        fillColor: FILL_COLOR,
445        fillOpacity: 1,
446      }).addTo(map);
447
448      L.circle([lat, lng], {
449        radius: pulse.radiusM || 1000,
450        color: FILL_COLOR,
451        weight: 1.5,
452        fillColor: FILL_COLOR,
453        fillOpacity: 0.08,
454        interactive: false,
455      }).addTo(map);
456
457      setTimeout(function () {
458        map.invalidateSize();
459      }, 120);
460
461      currentMap = map;
462      return map;
463    }
464
465    async function loadCoverage(map, viewport) {
466      if (!map || !viewport) return;
467      setMapLoading(true);
468      try {
469        var params = new URLSearchParams({
470          centerLat: String(viewport.centerLat),
471          centerLng: String(viewport.centerLng),
472          minLat: String(viewport.minLat),
473          maxLat: String(viewport.maxLat),
474          minLng: String(viewport.minLng),
475          maxLng: String(viewport.maxLng),
476        });
477        var res = await fetch('/api/public/coverage-preview?' + params.toString());
478        var json = await res.json();
479        if (json.success && json.data && json.data.hexCells) {
480          renderHexCoverage(map, json.data.hexCells);
481        }
482      } catch (err) {
483        console.error('Coverage preview failed', err);
484      } finally {
485        setMapLoading(false);
486      }
487    }
488
489    var PEAK_DOW_LABELS = [
490      'Sunday',
491      'Monday',
492      'Tuesday',
493      'Wednesday',
494      'Thursday',
495      'Friday',
496      'Saturday',
497    ];
498
499    function resolvePeakDow(pulse) {
500      if (!pulse) return null;
501      if (pulse.patterns && pulse.patterns.byDayOfWeek && pulse.patterns.byDayOfWeek.length) {
502        var best = null;
503        pulse.patterns.byDayOfWeek.forEach(function (day) {
504          if (!best || day.rides > best.rides) best = day;
505        });
506        if (best && best.rides > 0 && Number.isFinite(best.dow)) return best.dow;
507      }
508      if (pulse.peakDayLabel) {
509        var idx = PEAK_DOW_LABELS.indexOf(pulse.peakDayLabel);
510        if (idx >= 0) return idx;
511      }
512      return null;
513    }
514
515    function formatPeakDayHint(pulse) {
516      var dow = resolvePeakDow(pulse);
517      if (dow == null) return pulseT('pulse.stats.localTime');
518      return pulseTWithVars('pulse.stats.busiestDay', { day: pulseT('pulse.weekdays.' + dow) });
519    }
520
521    function displayPulseValue(value) {
522      var n = Number(value);
523      if (!Number.isFinite(n)) return value;
524      return n * PULSE_DISPLAY_MULTIPLIER;
525    }
526
527    function pulseActivityCount(pulse) {
528      if (!pulse) return 0;
529      var activity = Number(pulse.activityCount);
530      if (Number.isFinite(activity)) return activity;
531      return Number(pulse.rideCount) || 0;
532    }
533
534    function fillStats(pulse) {
535      lastPulse = pulse;
536      var rideCountEl = el('statRideCount');
537      if (rideCountEl) {
538        rideCountEl.textContent = formatNumber(displayPulseValue(pulseActivityCount(pulse)));
539      }
540      var driversEl = el('statUniqueDrivers');
541      if (driversEl) driversEl.textContent = formatNumber(displayPulseValue(pulse.uniqueDrivers));
542      var peakHourEl = el('statPeakHour');
543      if (peakHourEl) peakHourEl.textContent = pulse.peakHourLabel || '—';
544      var peakDayEl = el('statPeakDay');
545      if (peakDayEl) peakDayEl.textContent = formatPeakDayHint(pulse);
546      var unmetEl = el('statUnmet');
547      if (unmetEl) unmetEl.textContent = formatNumber(displayPulseValue(pulse.unmetAdDemand));
548      var paidEl = el('statPaid');
549      if (paidEl) paidEl.textContent = formatNumber(displayPulseValue(pulse.paidAdRidesNearby));
550      var radiusEl = el('statRadiusHint');
551      if (radiusEl) {
552        radiusEl.textContent = pulseTWithVars('pulse.stats.withinRadius', {
553          radius: String(pulse.radiusM || 1000),
554        });
555      }
556
557      var disclaimer = el('disclaimer');
558      if (disclaimer) {
559        if (pulseActivityCount(pulse) < LOW_DATA_THRESHOLD) {
560          disclaimer.classList.add('is-visible');
561        } else {
562          disclaimer.classList.remove('is-visible');
563        }
564      }
565    }
566
567    function refreshPlaceHeading() {
568      var nameEl = el('placeName');
569      if (!nameEl) return;
570      if (lastPlaceName) {
571        nameEl.removeAttribute('data-i18n');
572        nameEl.textContent = lastPlaceName;
573        return;
574      }
575      nameEl.textContent = pulseT('pulse.defaultBusinessName');
576    }
577
578    function refreshDynamicCopy() {
579      applyErrorCopy();
580      refreshPlaceHeading();
581      if (lastPulse) fillStats(lastPulse);
582      var mapLoading = el('mapLoading');
583      if (mapLoading && mapLoading.classList.contains('is-visible')) {
584        mapLoading.textContent = pulseT('pulse.map.loading');
585      }
586      var mapEl = el('map');
587      if (mapEl) {
588        mapEl.setAttribute('aria-label', pulseT('pulse.map.ariaLabel'));
589      }
590    }
591
592    function initAudio(placeId) {
593      if (!window.SoundTripAudioPlayer) return;
594      var containerId = ids.audioContainer;
595      SoundTripAudioPlayer.init({
596        containerId: containerId,
597        showVideo: false,
598        variant: 'slim',
599      });
600      SoundTripAudioPlayer.loadPlaceContent(placeId, containerId);
601    }
602
603    async function fetchPlace(placeId) {
604      var res = await fetch('/api/places/' + encodeURIComponent(placeId));
605      var json = await res.json();
606      if (!res.ok || !json.success || !json.data) {
607        throw new Error(
608          (json && (json.message || json.error)) || pulseT('pulse.error.placeNotFound'),
609        );
610      }
611      return json.data;
612    }
613
614    async function fetchPulse(placeId) {
615      var res = await fetch('/api/public/places/' + encodeURIComponent(placeId) + '/pulse');
616      var json = await res.json();
617      if (!res.ok || !json.success || !json.data) {
618        throw new Error(
619          (json && (json.message || json.error)) || pulseT('pulse.error.pulseUnavailable'),
620        );
621      }
622      return json.data;
623    }
624
625    async function loadPulse(placeId) {
626      if (!isValidPlaceId(placeId)) {
627        showError('pulse.error.invalidId', 'pulse.error.invalidIdMessage');
628        return;
629      }
630
631      if (
632        updateHistory &&
633        historyMode === 'path' &&
634        !window.location.pathname.match(/\/pulse\/[^/?#]+/) &&
635        placeId
636      ) {
637        updatePlaceHistory(placeId);
638      }
639
640      try {
641        var place = await fetchPlace(placeId);
642        if (hotelRedirect && place && place.hotelPage) {
643          window.location.href = '/for-hospitality/' + encodeURIComponent(placeId);
644          return;
645        }
646
647        var pulse = await fetchPulse(placeId);
648
649        var name =
650          (pulse.place && pulse.place.name) ||
651          (place.displayName && place.displayName.text) ||
652          place.displayName ||
653          pulseT('pulse.defaultBusinessName');
654        var address = (pulse.place && pulse.place.address) || place.formattedAddress || '';
655
656        lastPlaceName = name;
657        if (syncMeta) {
658          window.__pulsePlaceName = name;
659        }
660        refreshPlaceHeading();
661        var addressEl = el('placeAddress');
662        if (addressEl) addressEl.textContent = address;
663        if (syncMeta && typeof window.__pulseSyncMeta === 'function') {
664          window.__pulseSyncMeta(name);
665        }
666
667        fillStats(pulse);
668
669        hide(el('loading'));
670        hide(el('search'));
671        hide(el('error'));
672        show(el('content'));
673
674        var createHref =
675          '/advertiser/campaigns/create?placeId=' + encodeURIComponent(placeId);
676        if (window.SoundTripI18n) {
677          var pulseLocale = SoundTripI18n.detectLanguage();
678          if (pulseLocale) {
679            createHref += '&locale=' + encodeURIComponent(pulseLocale);
680          }
681        }
682        document.querySelectorAll('.hero-pulse-cta, .pulse-cta-card a.button').forEach(function (cta) {
683          cta.setAttribute('href', createHref);
684        });
685
686        var map = initMap(pulse);
687        await loadCoverage(map, pulse.viewport);
688        initAudio(placeId);
689      } catch (err) {
690        console.error(err);
691        showErrorRaw(
692          pulseT('pulse.error.unableToLoad'),
693          (err && err.message) || pulseT('pulse.error.generic'),
694        );
695      }
696    }
697
698    async function boot() {
699      var placeId = extractPlaceId();
700      if (!placeId) {
701        showSearch();
702        return;
703      }
704      show(el('loading'));
705      await loadPulse(placeId);
706    }
707
708    return {
709      loadPulse: loadPulse,
710      showSearch: showSearch,
711      initPlaceSearch: initPlaceSearch,
712      refreshDynamicCopy: refreshDynamicCopy,
713      boot: boot,
714    };
715  }
716
717  window.SoundTripPulse = {
718    createController: createPulseController,
719    DEFAULT_IDS: DEFAULT_IDS,
720  };
721
722  if (document.getElementById('pulse-loading') || document.getElementById('pulse-search')) {
723    var pageController = createPulseController({
724      updateHistory: true,
725      syncMeta: true,
726    });
727    window.__pulseRefreshDynamicCopy = pageController.refreshDynamicCopy;
728    window.addEventListener('languageChanged', pageController.refreshDynamicCopy);
729
730    if (document.readyState === 'loading') {
731      document.addEventListener('DOMContentLoaded', pageController.boot);
732    } else {
733      pageController.boot();
734    }
735  }
736})();

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.