Add a generic tour-map shortcode and a data format to supply the necessary info
All checks were successful
/ build (pull_request) Successful in 27s
All checks were successful
/ build (pull_request) Successful in 27s
This commit is contained in:
parent
a3978700b3
commit
14aed5e649
15 changed files with 1433 additions and 2 deletions
125
assets/js/tour-map.js
Normal file
125
assets/js/tour-map.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// 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.
|
||||
|
||||
// Marker symbols for the icon names in the data files (uMap pictogram names).
|
||||
const ICONS = {
|
||||
computer: '💻',
|
||||
ferry: '⛴️',
|
||||
'tram-stop': '🚊',
|
||||
'railway-halt': '🚂',
|
||||
'fire-station': '🚒',
|
||||
garden: '🌳',
|
||||
'mast-communications': '📡',
|
||||
ford: '🚇',
|
||||
viewpoint: '🔭',
|
||||
};
|
||||
|
||||
// Google's encoded polyline algorithm, returning [lat, lon] pairs.
|
||||
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);
|
||||
|
||||
const markerIcon = (color, icon, modifier) => {
|
||||
const symbol = document.createElement('span');
|
||||
symbol.textContent = ICONS[icon] ?? '';
|
||||
if (color) symbol.style.setProperty('--tour-map-color', color);
|
||||
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).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 });
|
||||
}
|
||||
Loading…
Reference in a new issue