const DATA_ROOT = '/data/';
let CATEGORIES = {};
let PLACES = {};
let TEXT = {};
let DETAIL_MAP;
let DETAIL_MAP_LAYER;
// ---------------------
// Helper
// ---------------------
async function loadJson(url) {
const res = await fetch(url);
return await res.json();
}
function highlight(div) {
const prev = div.style;
div.style.transition = 'background-color .7s';
div.style.backgroundColor = 'yellow';
setTimeout(() => div.style = prev, 800);
}
// ---------------------
// HTML utils
// ---------------------
function makeMarker(place) {
return L.marker(L.latLng(place.loc), {
pid: place.id,
title: place.later ? 'Noch nicht verfügbar' : place.name,
icon: L.divIcon({
className: 'pin ff-c' + place.cat + (place.later ? ' later' : ''),
html: '',
iconSize: [34, 55],
iconAnchor: [17, 55],
popupAnchor: [0, -20],
tooltipAnchor: [0, -20],
offset: [10, 20],
}),
});
}
function setBadge(div, category) {
const badge = div.querySelector('.badge');
// clear previous color
badge.classList.remove('inv');
for (const cls of badge.classList) {
if (cls.startsWith('bg-c')) {
badge.classList.remove(cls);
}
}
// set new values
badge.innerText = category.name || '';
badge.classList.add('bg-c' + category.id);
if (category.inv) {
badge.classList.add('inv');
}
}
function loadAudio(detailDiv, srcUrl) {
const x = detailDiv.querySelector('audio');
x.hidden = !srcUrl;
x.querySelectorAll('source').forEach(x => x.remove());
if (srcUrl) {
const audioSrc = document.createElement('source');
audioSrc.src = srcUrl;
audioSrc.type = 'audio/mpeg';
x.appendChild(audioSrc);
}
x.load(); // stops playing and reloads source
}
function comeBackLater() {
showNotice('come-back-later');
}
// ---------------------
// Interactive
// ---------------------
function selectPin(e) {
document.getElementById('pin-' + e.target.dataset.pk)
.parentNode.classList.add('selected');
}
function unselectPin(e) {
document.getElementById('pin-' + e.target.dataset.pk)
.parentNode.classList.remove('selected');
}
function openDetails(placeId, password) {
initDetails(placeId);
new bootstrap.Modal('#detail').show(); // trigger modal
}
function showNotice(id) {
const txt = TEXT[id];
if (txt) {
const div = document.getElementById('notice');
const sz = txt.wide ? 'lg' : 'md';
div.firstElementChild.className = `modal-dialog modal-${sz} modal-fullscreen-${sz}-down`;
div.querySelector('.modal-title').innerText = txt.title;
div.querySelector('.modal-title').innerText = txt.title;
div.querySelector('.modal-body').innerHTML = txt.body;
new bootstrap.Modal(div).show();
} else {
console.error(`Missing text for "${id}"`)
}
return false;
}
// ---------------------
// Initializer
// ---------------------
function initColors() {
let rv = '';
for (const cat of Object.values(CATEGORIES)) {
rv += `.ff-c${cat.id} { fill: ${cat.color} }\n`;
rv += `.bg-c${cat.id} { background: ${cat.color} }\n`;
}
document.getElementById('colors').innerHTML = rv;
}
function initCard(placeId) {
const place = PLACES[placeId];
const category = CATEGORIES[place.cat];
const x = document.getElementById('card-template').cloneNode(true);
document.getElementById('cards').append(x);
x.id = 'card-' + placeId;
x.dataset.pk = placeId;
if (place.loc) {
x.onmouseenter = selectPin;
x.onmouseleave = unselectPin;
}
x.querySelector('a').href = '#' + placeId;
x.querySelector('img').dataset.src = place.cov || '';
x.querySelector('.card-title').innerText = place.name || '';
setBadge(x, category);
}
function initLoadingCard() {
const x = document.getElementById('card-template').cloneNode(true);
x.id = 'card-loading';
x.classList.add('placeholder-glow');
document.getElementById('cards').append(x);
x.querySelectorAll('img,h3,span').forEach(elem => {
elem.classList.add('placeholder');
elem.alt = '';
if (elem.tagName === 'H3') {
elem.classList.add('w-100');
}
});
return x;
}
function initDetails(placeId) {
const place = PLACES[placeId];
const category = CATEGORIES[place.cat];
const x = document.getElementById('detail');
x.querySelector('img').src = place.img || '';
x.querySelector('.modal-title').innerText = place.name || '';
x.querySelector('#detail-desc').innerHTML = place.desc || '';
setBadge(x, category);
loadAudio(x, place.audio);
// external map links
x.querySelector('#detail-map-container').hidden = !place.loc;
if (place.loc) {
const [lat, long] = place.loc;
x.querySelector('#osm-link').href = `https://www.openstreetmap.org/?mlat=${lat}&mlon=${long}&zoom=18`;
x.querySelector('#g-maps').href = `https://www.google.com/maps/search/?api=1&query=${lat}%2C${long}&zoom=18`;
}
setDetailMarker(place);
document.title = place.name + ' – ' + document.title;
}
function clearDetails() {
const x = document.getElementById('detail');
loadAudio(x, '');
// in case of youtube videos or other media: stops everything else
x.querySelector('#detail-desc').innerHTML = '';
setDetailMarker(null);
document.title = document.title.split(' – ').pop();
}
// ---------------------
// Map stuff
// ---------------------
function initGPS(map) {
L.control.locate({
returnToPrevBounds: true,
showPopup: false,
}).addTo(map);
}
function initDetailMap() {
const map = L.map('detail-map');
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
}).addTo(map);
initGPS(map);
DETAIL_MAP_LAYER = L.layerGroup([]).addTo(map);
DETAIL_MAP = map;
}
function setDetailMarker(place) {
if (place && place.loc) {
const pos = makeMarker(place).addTo(DETAIL_MAP_LAYER).getLatLng();
DETAIL_MAP.setView(pos, 17);
DETAIL_MAP.setMaxBounds(pos.toBounds(0));
setTimeout(() => DETAIL_MAP.invalidateSize(), 300);
setTimeout(() => DETAIL_MAP.invalidateSize(), 1000);
} else {
DETAIL_MAP_LAYER.clearLayers();
}
}
async function initMainMap() {
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
});
const map = L.map('map', {
layers: [osm],
center: [49.894413, 10.880028],
zoom: 14,
// minZoom: 11,
});
initGPS(map);
// load data
const layers = {};
var bounds = L.latLngBounds();
var layerControl = L.control.layers(null, null, {
position: 'bottomright',
// sortLayers: true,
}).addTo(map);
// init checkbox to toggle groups
for (const cat of await loadJson(DATA_ROOT + 'categories.json')) {
CATEGORIES[cat.id] = cat;
layers[cat.id] = L.layerGroup([]).addTo(map);
layerControl.addOverlay(layers[cat.id],
' '
+ cat.name);
}
// init places
for (const place of await loadJson(DATA_ROOT + 'places.json')) {
PLACES[place.id] = place;
const group = layers[place.cat];
if (place.loc) {
const marker = makeMarker(place).addTo(group);
marker.on('click', place.later ? comeBackLater : onMarkerClick);
bounds.extend(marker.getLatLng());
}
if (!place.later) {
initCard(place.id);
}
}
// adjust bounds & zoom
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [100, 100] });
}
// tooltip
function onTooltip(pin) {
const div = document.getElementById("card-" + pin.options.pid);
div.scrollIntoView({ behavior: 'smooth' });
highlight(div);
return '';
// const place = PLACES[pin.options.pid];
// return place.name;
}
// click events
function onMarkerClick(e) {
location.hash = e.target.options.pid; // triggers openDetails()
}
}
async function start() {
const temp = initLoadingCard();
await initMainMap();
initDetailMap();
initColors();
document.getElementById('spin').remove();
onHashChange();
loadJson(DATA_ROOT + 'text.json').then(x => TEXT = x)
temp.remove();
document.getElementById('detail').addEventListener('hidden.bs.modal', e => {
location.hash = '';
});
const observer = lozad();
observer.observe();
}
// event listener
function onHashChange() {
if (location.hash.length > 1) {
const [id, pw] = location.hash.slice(1).split(':', 1);
openDetails(id, pw);
} else {
clearDetails();
}
}
addEventListener('hashchange', onHashChange);