// Tour map shortcode: draws the tour locations and their routes from the Congress on // OpenStreetMap tiles with Leaflet. The data comes from the data-tour-map attribute. const POPUP = { minWidth: 220, maxWidth: 320 }; const decodePolyline = (encoded, precision = 5) => { const factor = 10 ** precision; const points = []; let index = 0; let lat = 0; let lon = 0; const next = () => { let result = 0; let shift = 0; let byte; do { byte = encoded.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); return result & 1 ? ~(result >> 1) : result >> 1; }; while (index < encoded.length) { lat += next(); lon += next(); points.push([lat / factor, lon / factor]); } return points; }; const legPoints = (leg) => leg.coordinates ? leg.coordinates.map(([lon, lat]) => [lat, lon]) : decodePolyline(leg.polyline ?? '', leg.precision ?? 5); // icon is a data: URI from the vendored icon sets (see the tour-map-icon partial), or empty. const markerIcon = (color, icon, modifier) => { const symbol = document.createElement('span'); if (color) symbol.style.setProperty('--tour-map-color', color); if (icon) { const image = document.createElement('i'); image.style.setProperty('--tour-map-icon', `url("${icon}")`); symbol.append(image); } return L.divIcon({ className: `tour-map__marker${modifier ? ` tour-map__marker--${modifier}` : ''}`, html: symbol, iconSize: null, }); }; const popup = (tour) => { const content = document.createElement('div'); const title = document.createElement(tour.url ? 'a' : 'strong'); title.className = 'tour-map__popup-title'; title.textContent = tour.title; if (tour.url) title.href = tour.url; content.append(title); if (tour.directions) { // Rendered from the site's own data files by Hugo. const directions = document.createElement('div'); directions.className = 'tour-map__directions'; directions.innerHTML = tour.directions; content.append(directions); } return content; }; for (const element of document.querySelectorAll('[data-tour-map]')) { const { origin, tours } = JSON.parse(element.dataset.tourMap); // Show the map container first: Leaflet needs its size. element.classList.add('tour-map--ready'); const map = L.map(element.querySelector('.tour-map__canvas'), { scrollWheelZoom: false }); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap', }).addTo(map); // Zooming with the scroll wheel only once the map is in use, so it doesn't catch page scrolling. map.on('click focus', () => map.scrollWheelZoom.enable()); map.on('mouseout blur', () => map.scrollWheelZoom.disable()); const bounds = L.latLngBounds([]); for (const tour of tours) { for (const leg of tour.route) { const points = legPoints(leg); L.polyline(points, { color: tour.color || '#555', weight: 4, opacity: 0.8, dashArray: leg.mode === 'walk' ? '2 8' : null, }) .bindPopup(popup(tour), POPUP) .addTo(map); points.forEach((point) => bounds.extend(point)); } L.marker([tour.lat, tour.lon], { icon: markerIcon(tour.color, tour.icon), title: tour.title }) .bindPopup(popup(tour), POPUP) .addTo(map); bounds.extend([tour.lat, tour.lon]); } if (origin.lat !== undefined) { L.marker([origin.lat, origin.lon], { icon: markerIcon('', origin.icon, 'origin'), title: origin.name, zIndexOffset: 1000, }) .bindPopup(popup({ title: origin.name }), POPUP) .addTo(map); bounds.extend([origin.lat, origin.lon]); } map.fitBounds(bounds, { padding: [24, 24], maxZoom: 16 }); }