Compare commits
5
Commits
75da83d6e1
...
924cfc18e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
924cfc18e2 | ||
|
|
e2ba8f413a | ||
|
|
19a8fbcbf0 | ||
|
|
d79a67cbe6 | ||
|
|
29535e39b8 |
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Journal
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: 'The Journey'
|
title: Journal
|
||||||
template: dailies
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
content:
|
content:
|
||||||
items: '@self.children'
|
items: '@self.children'
|
||||||
order:
|
order:
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Map'
|
|
||||||
template: map
|
|
||||||
---
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Trip Stats'
|
|
||||||
template: stats
|
|
||||||
---
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Stories
|
title: Stories
|
||||||
template: stories
|
template: default
|
||||||
|
routable: false
|
||||||
|
visible: false
|
||||||
published: true
|
published: true
|
||||||
---
|
---
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -417,8 +417,138 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Config-driven entry-map initialisation — the single orchestration shared by
|
||||||
|
* the trip page and both home views. Mirrors the trip page's canonical handler:
|
||||||
|
* compact collapsed attribution bottom-left, an on('load') marker loop with a
|
||||||
|
* map-tip hover popup, bounds fit (jumpTo for one entry), GPX journey rendering,
|
||||||
|
* and the fullscreen toggle.
|
||||||
|
*
|
||||||
|
* opts:
|
||||||
|
* container string — map div id ('trip-map', 'home-map')
|
||||||
|
* entries array — parsed map_entries [{lat,lng,slug,title,url,type?,force_connect?}, ...]
|
||||||
|
* cardPrefix string? — e.g. 'entry-'; when set and a card #prefix+slug exists,
|
||||||
|
* click scrolls+flashes that card; otherwise click navigates to entry.url
|
||||||
|
* storyMarkers bool — render createStoryMarker() for entry.type === 'story' (default: dot markers)
|
||||||
|
* markLatest bool — enlarge the final non-story entry's dot (default true; pass false for
|
||||||
|
* unordered sets like home highlights so no marker is singled out)
|
||||||
|
* fullscreen object? — { btnId, colSelector }; wires the fullscreen toggle + fullscreen-aware click
|
||||||
|
* gpx object? — { urls, use, autoconnect, sourcePrefix, journeyId }; forwarded to renderGpxJourney
|
||||||
|
* fit object? — { padding=60, maxZoom=11, singleZoom=10 }
|
||||||
|
*
|
||||||
|
* Returns the maplibregl.Map instance.
|
||||||
|
*/
|
||||||
|
function initEntryMap(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var entries = opts.entries || [];
|
||||||
|
var fit = opts.fit || {};
|
||||||
|
var padding = fit.padding != null ? fit.padding : 60;
|
||||||
|
var maxZoom = fit.maxZoom != null ? fit.maxZoom : 11;
|
||||||
|
var singleZoom = fit.singleZoom != null ? fit.singleZoom : 10;
|
||||||
|
var markLatest = opts.markLatest !== false;
|
||||||
|
|
||||||
|
var map = new maplibregl.Map({
|
||||||
|
container: opts.container,
|
||||||
|
style: MAP_STYLE,
|
||||||
|
center: [20, 20],
|
||||||
|
zoom: 2,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
||||||
|
|
||||||
|
map.on('load', function () {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
map.jumpTo({ center: [0, 20], zoom: 2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = new maplibregl.LngLatBounds();
|
||||||
|
|
||||||
|
entries.forEach(function (entry, i) {
|
||||||
|
var isLatest = markLatest && (entry.type !== 'story') && (i === entries.length - 1);
|
||||||
|
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||||
|
bounds.extend(lngLat);
|
||||||
|
|
||||||
|
var el = (opts.storyMarkers && entry.type === 'story')
|
||||||
|
? createStoryMarker()
|
||||||
|
: createDotMarker(isLatest);
|
||||||
|
el.dataset.url = entry.url;
|
||||||
|
|
||||||
|
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
||||||
|
.setLngLat(lngLat)
|
||||||
|
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
||||||
|
el.addEventListener('mouseenter', function () { popup.addTo(map); });
|
||||||
|
el.addEventListener('mouseleave', function () { popup.remove(); });
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
var card = opts.cardPrefix ? document.getElementById(opts.cardPrefix + entry.slug) : null;
|
||||||
|
if (!card) { window.location.href = entry.url; return; }
|
||||||
|
var hashId = opts.cardPrefix + entry.slug;
|
||||||
|
function scrollAndHighlight() {
|
||||||
|
window.location.hash = hashId;
|
||||||
|
setTimeout(function () {
|
||||||
|
card.classList.add('is-highlighted');
|
||||||
|
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
var isFs = col && col.classList.contains('is-fullscreen');
|
||||||
|
if (isFs) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
if (fsBtn) fsBtn.click();
|
||||||
|
setTimeout(scrollAndHighlight, 450);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollAndHighlight();
|
||||||
|
});
|
||||||
|
|
||||||
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
map.jumpTo({ center: [parseFloat(entries[0].lng), parseFloat(entries[0].lat)], zoom: singleZoom });
|
||||||
|
} else {
|
||||||
|
map.fitBounds(bounds, { padding: padding, maxZoom: maxZoom });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.gpx) {
|
||||||
|
renderGpxJourney(
|
||||||
|
map,
|
||||||
|
opts.gpx.use ? (opts.gpx.urls || []) : [],
|
||||||
|
entries,
|
||||||
|
opts.gpx.sourcePrefix,
|
||||||
|
opts.gpx.journeyId,
|
||||||
|
{ connectMode: opts.gpx.autoconnect }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse attribution <details> which MapLibre may open on load */
|
||||||
|
var attrib = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
||||||
|
if (attrib) attrib.removeAttribute('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () { map.resize(); }, 100);
|
||||||
|
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
if (fsBtn && col) {
|
||||||
|
fsBtn.addEventListener('click', function () {
|
||||||
|
var isFs = col.classList.toggle('is-fullscreen');
|
||||||
|
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
||||||
|
document.body.style.overflow = isFs ? 'hidden' : '';
|
||||||
|
setTimeout(function () { map.resize(); }, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
global.MapUtils = {
|
global.MapUtils = {
|
||||||
MAP_STYLE: MAP_STYLE,
|
MAP_STYLE: MAP_STYLE,
|
||||||
|
initEntryMap: initEntryMap,
|
||||||
ACCENT: ACCENT,
|
ACCENT: ACCENT,
|
||||||
haversineKm: haversineKm,
|
haversineKm: haversineKm,
|
||||||
parseGpxFiles: parseGpxFiles,
|
parseGpxFiles: parseGpxFiles,
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
{% extends 'default.html.twig' %}
|
|
||||||
|
|
||||||
{% block map_assets %}
|
|
||||||
{% do assets.addCss('theme://css-compiled/map.css') %}
|
|
||||||
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set journal_entries = page.collection() %}
|
|
||||||
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
|
|
||||||
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{% set all_items = [] %}
|
|
||||||
{% for e in journal_entries %}
|
|
||||||
{% set all_items = all_items|merge([{'type': 'journal', 'page': e, 'date': e.date}]) %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for s in story_entries %}
|
|
||||||
{% set all_items = all_items|merge([{'type': 'story', 'page': s, 'date': s.date}]) %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# page.collection() returns date-descending; reverse to match ascending default on trip page. #}
|
|
||||||
{% set all_items = all_items|reverse %}
|
|
||||||
|
|
||||||
{# Collect GPS entries for mini-map #}
|
|
||||||
{% set map_entries = [] %}
|
|
||||||
{% for item in all_items %}
|
|
||||||
{% if item.type == 'journal' and item.page.header.lat is not empty and item.page.header.lng is not empty %}
|
|
||||||
{% set map_entries = map_entries|merge([{
|
|
||||||
'lat': item.page.header.lat,
|
|
||||||
'lng': item.page.header.lng,
|
|
||||||
'title': item.page.title,
|
|
||||||
'slug': item.page.slug,
|
|
||||||
'url': item.page.url,
|
|
||||||
'force_connect': item.page.header.force_connect ? true : false,
|
|
||||||
'transport_mode': item.page.header.transport_mode ? item.page.header.transport_mode : null
|
|
||||||
}]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
|
|
||||||
{% include 'partials/feed-map.html.twig' with {
|
|
||||||
'map_entries': map_entries,
|
|
||||||
'map_id': 'feed-map',
|
|
||||||
'map_var': 'feedMap',
|
|
||||||
'link_href': page.parent().url ~ '/map',
|
|
||||||
'card_prefix': 'entry-',
|
|
||||||
'trip_page': trip_page,
|
|
||||||
'show_journey': true
|
|
||||||
} only %}
|
|
||||||
|
|
||||||
<div class="feed-sort-bar">
|
|
||||||
<button class="trip-stats-btn" id="feed-sort-toggle" aria-label="Sort: oldest first">↑ Oldest first</button>
|
|
||||||
</div>
|
|
||||||
<div class="feed">
|
|
||||||
{% if all_items|length > 0 %}
|
|
||||||
{% for item in all_items %}
|
|
||||||
{% set entry = item.page %}
|
|
||||||
{% if item.type == 'journal' %}
|
|
||||||
{% include 'partials/entry-journal.html.twig' %}
|
|
||||||
{% else %}
|
|
||||||
{% include 'partials/entry-story.html.twig' %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p class="feed-empty">No entries yet. The journey is about to begin.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a class="back-pill entry-back-fixed" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="back-pill entry-back-fixed" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
|
|
||||||
{% set entry = page %}
|
{% set entry = page %}
|
||||||
{% include 'partials/entry-journal.html.twig' %}
|
{% include 'partials/entry-journal.html.twig' %}
|
||||||
|
|
||||||
<footer class="entry-footer">
|
<footer class="entry-footer">
|
||||||
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="back-pill" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -60,9 +60,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
{% include 'partials/entry-map.html.twig' with {
|
||||||
<div class="home-map" id="home-map"></div>
|
map_id: 'home-map',
|
||||||
</div>
|
map_global: 'homeMap',
|
||||||
|
entries: map_entries,
|
||||||
|
card_prefix: 'entry-',
|
||||||
|
story_markers: false,
|
||||||
|
gpx_urls: home_gpx_urls,
|
||||||
|
use_gpx: trip and trip.header.use_gpx is not null ? trip.header.use_gpx : true,
|
||||||
|
autoconnect: trip ? (trip.header.autoconnect ?? 'on') : 'on',
|
||||||
|
gpx_source_prefix: 'home-gpx',
|
||||||
|
journey_id: 'home-journey'
|
||||||
|
} only %}
|
||||||
|
|
||||||
{% if all_items|length == 0 %}
|
{% if all_items|length == 0 %}
|
||||||
{% include 'partials/home-predeparture.html.twig' with {
|
{% include 'partials/home-predeparture.html.twig' with {
|
||||||
@@ -83,61 +92,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if map_entries|length > 0 %}
|
|
||||||
<script>
|
|
||||||
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var HOME_GPX_URLS = {{ home_gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ trip and trip.header.use_gpx is not null ? (trip.header.use_gpx ? 'true' : 'false') : 'true' }};
|
|
||||||
var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var homeMap = new maplibregl.Map({
|
|
||||||
container: 'home-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
homeMap.on('load', function () {
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
var coords = [];
|
|
||||||
|
|
||||||
HOME_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === HOME_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
coords.push(lngLat);
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (!card) return;
|
|
||||||
window.location.hash = 'entry-' + entry.slug;
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HOME_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: coords[0], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
|
|
||||||
MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
}); // DOMContentLoaded
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
|
||||||
|
|
||||||
@@ -247,43 +201,11 @@ homeMap.on('load', function () {
|
|||||||
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var homeMap = new maplibregl.Map({
|
window.homeMap = MapUtils.initEntryMap({
|
||||||
container: 'home-map',
|
container: 'home-map',
|
||||||
style: MapUtils.MAP_STYLE,
|
entries: HIGHLIGHTS_ENTRIES,
|
||||||
center: [20, 20],
|
markLatest: false,
|
||||||
zoom: 2
|
fit: { padding: 60, maxZoom: 8, singleZoom: 8 }
|
||||||
});
|
|
||||||
|
|
||||||
homeMap.on('load', function () {
|
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 0) return;
|
|
||||||
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
HIGHLIGHTS_ENTRIES.forEach(function (entry) {
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(false);
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('highlight-' + entry.slug);
|
|
||||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: [parseFloat(HIGHLIGHTS_ENTRIES[0].lng), parseFloat(HIGHLIGHTS_ENTRIES[0].lat)], zoom: 8 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 8 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
});
|
});
|
||||||
}); // DOMContentLoaded
|
}); // DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block map_assets %}
|
|
||||||
{% do assets.addCss('theme://css-compiled/map.css') %}
|
|
||||||
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
{% set tracker_page = grav.pages.find(page.parent().route ~ '/dailies') %}
|
|
||||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{% set gpx_urls = [] %}
|
|
||||||
{% for name, media in trip_page.media.all %}
|
|
||||||
{% if name|split('.')|last == 'gpx' %}
|
|
||||||
{% set gpx_urls = gpx_urls|merge([trip_page.url ~ '/' ~ name]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set map_entries = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
|
||||||
{% set hero_url = null %}
|
|
||||||
{% if entry.header.hero_image and entry.media[entry.header.hero_image] is defined %}
|
|
||||||
{% set hero_url = entry.media[entry.header.hero_image].cropResize(240, 135).url %}
|
|
||||||
{% elseif entry.media.images|length > 0 %}
|
|
||||||
{% set hero_url = entry.media.images|first.cropResize(240, 135).url %}
|
|
||||||
{% endif %}
|
|
||||||
{% set map_entries = map_entries|merge([{
|
|
||||||
'lat': entry.header.lat|number_format(6, '.', ''),
|
|
||||||
'lng': entry.header.lng|number_format(6, '.', ''),
|
|
||||||
'title': entry.title,
|
|
||||||
'date': entry.date|date('d M Y'),
|
|
||||||
'url': entry.url,
|
|
||||||
'hero': hero_url,
|
|
||||||
'force_connect': entry.header.force_connect ? true : false,
|
|
||||||
'transport_mode': entry.header.transport_mode ? entry.header.transport_mode : null
|
|
||||||
}]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="map-container" id="trip-map"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ trip_page.header.use_gpx ?? true ? 'true' : 'false' }};
|
|
||||||
var AUTOCONNECT = "{{ trip_page.header.autoconnect ?? 'on' }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var map = new maplibregl.Map({
|
|
||||||
container: 'trip-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
|
||||||
|
|
||||||
if (ENTRIES.length === 0) {
|
|
||||||
var empty = document.createElement('div');
|
|
||||||
empty.className = 'map-empty';
|
|
||||||
empty.textContent = 'No locations yet — entries with GPS will appear here.';
|
|
||||||
document.getElementById('trip-map').appendChild(empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on('load', function () {
|
|
||||||
if (ENTRIES.length === 0) return;
|
|
||||||
|
|
||||||
/* ── Markers + bounds ──────────────────────────────────────── */
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(map); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () { window.location.href = entry.url; });
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(map);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Fit bounds ─────────────────────────────────────────────── */
|
|
||||||
if (ENTRIES.length === 1) {
|
|
||||||
map.jumpTo({ center: [parseFloat(ENTRIES[0].lng), parseFloat(ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
map.fitBounds(bounds, { padding: 100, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
|
||||||
MapUtils.renderGpxJourney(map, USE_GPX ? GPX_URLS : [], ENTRIES, 'gpx', 'journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
}); // DOMContentLoaded
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ assets.css()|raw }}
|
{{ assets.css()|raw }}
|
||||||
{{ assets.js()|raw }}
|
{{ assets.js()|raw }}
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if page.template == 'map' %}map-page{% endif %}{% if page.template == 'home' or page.template == 'trip' %} home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
|
<body class="{% if page.template == 'home' or page.template == 'trip' %}home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
|
||||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a class="site-title" href="{{ base_url_absolute }}">into the east</a>
|
<a class="site-title" href="{{ base_url_absolute }}">into the east</a>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{#
|
||||||
|
Entry-map partial — shared by trip.html.twig and home.html.twig (active-trip branch).
|
||||||
|
|
||||||
|
Renders the `.home-map-col` map column plus the thin MapUtils.initEntryMap()
|
||||||
|
invocation. The map engine itself lives in js/map.js (MapUtils.initEntryMap);
|
||||||
|
this partial only supplies markup + a parameterised call. Callers register the
|
||||||
|
map assets (map.css / map.js) themselves via their own {% block map_assets %}.
|
||||||
|
|
||||||
|
The map column always renders (home shows an empty map in pre-departure); the
|
||||||
|
init script only runs when there are entries to plot.
|
||||||
|
|
||||||
|
Required variables (via {% include ... with {...} only %}):
|
||||||
|
map_id — string: map div id + fullscreen-btn id prefix ('trip-map' | 'home-map')
|
||||||
|
map_global — string: window global the Map is assigned to ('tripMap' | 'homeMap')
|
||||||
|
entries — array: map entries [{lat, lng, slug, title, url, type?, force_connect, ...}]
|
||||||
|
card_prefix — string: scroll-to card id prefix ('entry-')
|
||||||
|
story_markers — bool: render story entries as diamond markers (trip: true, home: false)
|
||||||
|
gpx_urls — array: GPX file URLs
|
||||||
|
use_gpx — bool: whether to draw GPX tracks
|
||||||
|
autoconnect — string: journey connect mode ('on' | 'off' | 'manual' | ...)
|
||||||
|
gpx_source_prefix — string: MapLibre GPX source id prefix ('gpx' | 'home-gpx')
|
||||||
|
journey_id — string: journey layer id ('trip-journey' | 'home-journey')
|
||||||
|
#}
|
||||||
|
<div class="home-map-col">
|
||||||
|
<div class="home-map" id="{{ map_id }}">
|
||||||
|
<button class="feed-map-fullscreen-btn" id="{{ map_id }}-fullscreen" aria-label="Expand map">
|
||||||
|
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
|
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if entries|length > 0 %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.{{ map_global }} = MapUtils.initEntryMap({
|
||||||
|
container: '{{ map_id }}',
|
||||||
|
entries: {{ entries|json_encode|raw }},
|
||||||
|
cardPrefix: '{{ card_prefix }}',
|
||||||
|
{% if story_markers %}storyMarkers: true,
|
||||||
|
{% endif %}fullscreen: { btnId: '{{ map_id }}-fullscreen', colSelector: '.home-map-col' },
|
||||||
|
gpx: { urls: {{ gpx_urls|json_encode|raw }}, use: {{ use_gpx ? 'true' : 'false' }}, autoconnect: "{{ autoconnect }}", sourcePrefix: '{{ gpx_source_prefix }}', journeyId: '{{ journey_id }}' },
|
||||||
|
fit: { padding: 60, maxZoom: 11, singleZoom: 10 }
|
||||||
|
});
|
||||||
|
}); // DOMContentLoaded
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
{#
|
|
||||||
Feed mini-map partial — shared by dailies.html.twig and stories.html.twig.
|
|
||||||
|
|
||||||
Required variables (via {% include ... with {...} only %}):
|
|
||||||
map_entries — array: [{lat, lng, title, slug, url, type, force_connect, transport_mode}]
|
|
||||||
map_id — string: HTML id for the map div (e.g. 'feed-map', 'stories-map')
|
|
||||||
map_var — string: JS variable name for the MapLibre Map (e.g. 'feedMap', 'storiesMap')
|
|
||||||
link_href — string|null: URL for "View full map" link; null/empty hides the link
|
|
||||||
card_prefix — string: prefix for scroll-to card IDs ('entry-' or 'story-')
|
|
||||||
trip_page — Grav page: trip page for autoconnect setting (used when show_journey is true)
|
|
||||||
show_journey — bool: whether to draw the route connector line between markers
|
|
||||||
|
|
||||||
Callers must register map assets via {% block map_assets %} in their own template.
|
|
||||||
#}
|
|
||||||
{% if map_entries|length > 0 %}
|
|
||||||
<div class="feed-map-wrap">
|
|
||||||
<div class="feed-map" id="{{ map_id }}">
|
|
||||||
<button class="feed-map-fullscreen-btn" id="{{ map_id }}-fullscreen" aria-label="Expand map">
|
|
||||||
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
||||||
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if link_href %}
|
|
||||||
<a class="feed-map-link" href="{{ link_href }}">View full map →</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
{% set js_suffix = map_id|replace({'-': '_'})|upper %}
|
|
||||||
{% if show_journey %}
|
|
||||||
{% set _ac = trip_page ? (trip_page.header.autoconnect ?? 'on') : 'on' %}
|
|
||||||
{% endif %}
|
|
||||||
var MAP_ENTRIES_{{ js_suffix }} = {{ map_entries|json_encode|raw }};
|
|
||||||
{% if show_journey %}
|
|
||||||
var AUTOCONNECT_{{ js_suffix }} = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var {{ map_var }} = new maplibregl.Map({
|
|
||||||
container: '{{ map_id }}',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2,
|
|
||||||
attributionControl: false
|
|
||||||
});
|
|
||||||
{{ map_var }}.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
|
||||||
|
|
||||||
{{ map_var }}.on('load', function () {
|
|
||||||
var attrib = {{ map_var }}.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
|
||||||
if (attrib) attrib.removeAttribute('open');
|
|
||||||
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
var entries = MAP_ENTRIES_{{ js_suffix }};
|
|
||||||
|
|
||||||
entries.forEach(function (entry, i) {
|
|
||||||
var isLatest = (entry.type !== 'story') && (i === entries.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = entry.type === 'story' ? MapUtils.createStoryMarker() : MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo({{ map_var }}); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('{{ card_prefix }}' + entry.slug);
|
|
||||||
var mapWrap = document.querySelector('.feed-map-wrap');
|
|
||||||
var isFs = mapWrap && mapWrap.classList.contains('is-fullscreen');
|
|
||||||
function scrollAndHighlight() {
|
|
||||||
if (!card) { window.location.href = entry.url; return; }
|
|
||||||
window.location.hash = '{{ card_prefix }}' + entry.slug;
|
|
||||||
setTimeout(function () {
|
|
||||||
card.classList.add('is-highlighted');
|
|
||||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
|
||||||
}, 350);
|
|
||||||
}
|
|
||||||
if (isFs) {
|
|
||||||
var fsBtn = document.getElementById('{{ map_id }}-fullscreen');
|
|
||||||
if (fsBtn) fsBtn.click();
|
|
||||||
setTimeout(scrollAndHighlight, 450);
|
|
||||||
} else {
|
|
||||||
scrollAndHighlight();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo({{ map_var }});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (entries.length === 1) {
|
|
||||||
{{ map_var }}.jumpTo({ center: [parseFloat(entries[0].lng), parseFloat(entries[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
{{ map_var }}.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
{% if show_journey %}
|
|
||||||
var segments = MapUtils.buildJourneySegments(entries, { connectMode: AUTOCONNECT_{{ js_suffix }} });
|
|
||||||
MapUtils.addJourneySegments({{ map_var }}, segments, '{{ map_id }}-journey');
|
|
||||||
{% endif %}
|
|
||||||
});
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var fsBtn = document.getElementById('{{ map_id }}-fullscreen');
|
|
||||||
var mapWrap = document.querySelector('.feed-map-wrap');
|
|
||||||
if (!fsBtn || !mapWrap) return;
|
|
||||||
fsBtn.addEventListener('click', function() {
|
|
||||||
var isFs = mapWrap.classList.toggle('is-fullscreen');
|
|
||||||
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
|
||||||
document.body.style.overflow = isFs ? 'hidden' : '';
|
|
||||||
setTimeout(function() { typeof {{ map_var }} !== 'undefined' && {{ map_var }}.resize(); }, 50);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}); // DOMContentLoaded
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
{% set tracker_page = grav.pages.find(trip_page.route ~ '/dailies') %}
|
|
||||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
|
||||||
|
|
||||||
{# Basic counts #}
|
|
||||||
{% set entry_count = all_entries|length %}
|
|
||||||
|
|
||||||
{# Days on road — past trip uses declared date_end; active trip uses first entry to now #}
|
|
||||||
{% set days_on_road = 0 %}
|
|
||||||
{% if trip_page.header.date_end is not empty %}
|
|
||||||
{# Past trip: use declared end date #}
|
|
||||||
{% set start_ts = trip_page.header.date_start|date('U') %}
|
|
||||||
{% set end_ts = trip_page.header.date_end|date('U') %}
|
|
||||||
{% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
|
|
||||||
{% else %}
|
|
||||||
{# Active trip: first entry to now #}
|
|
||||||
{% set first_ts = null %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% set ts = entry.date|date('U') %}
|
|
||||||
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if first_ts is not null %}
|
|
||||||
{% set diff_seconds = "now"|date('U') - first_ts %}
|
|
||||||
{% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
|
|
||||||
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Countries — unique, case-insensitive dedup, preserve original casing #}
|
|
||||||
{% set seen_lower = [] %}
|
|
||||||
{% set country_display = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.location_country is not empty %}
|
|
||||||
{% set lower = entry.header.location_country|trim|lower %}
|
|
||||||
{% if lower not in seen_lower %}
|
|
||||||
{% set seen_lower = seen_lower|merge([lower]) %}
|
|
||||||
{% set country_display = country_display|merge([entry.header.location_country|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# Cities — unique, case-insensitive dedup, preserve original casing #}
|
|
||||||
{% set seen_city_lower = [] %}
|
|
||||||
{% set city_display = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.location_city is not empty %}
|
|
||||||
{% set lower = entry.header.location_city|trim|lower %}
|
|
||||||
{% if lower not in seen_city_lower %}
|
|
||||||
{% set seen_city_lower = seen_city_lower|merge([lower]) %}
|
|
||||||
{% set city_display = city_display|merge([entry.header.location_city|trim]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# Temperature range #}
|
|
||||||
{% set temp_min = null %}
|
|
||||||
{% set temp_max = null %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.weather_temp_c is defined and entry.header.weather_temp_c is not empty %}
|
|
||||||
{% set t = entry.header.weather_temp_c %}
|
|
||||||
{% if temp_min is null or t < temp_min %}{% set temp_min = t %}{% endif %}
|
|
||||||
{% if temp_max is null or t > temp_max %}{% set temp_max = t %}{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# GPS points for distance — collect as JSON for JS computation #}
|
|
||||||
{% set gps_points = [] %}
|
|
||||||
{% for entry in all_entries %}
|
|
||||||
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
|
|
||||||
{% set gps_points = gps_points|merge([[entry.header.lat, entry.header.lng]]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{# GPX detection — trip has GPX files if any .gpx media exists on the trip page #}
|
|
||||||
{% set gpx_urls = [] %}
|
|
||||||
{% for name, media in trip_page.media.all %}
|
|
||||||
{% if name|split('.')|last == 'gpx' %}
|
|
||||||
{% set gpx_urls = gpx_urls|merge([trip_page.url ~ '/' ~ name]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% set has_gpx = gpx_urls|length > 0 %}
|
|
||||||
|
|
||||||
<div class="stats-page">
|
|
||||||
<h1 class="stats-heading">Trip Statistics</h1>
|
|
||||||
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ days_on_road }}</span>
|
|
||||||
<span class="stat-label">{{ days_on_road == 1 ? 'day' : 'days' }} on the road</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ entry_count }}</span>
|
|
||||||
<span class="stat-label">{{ entry_count == 1 ? 'entry' : 'entries' }} posted</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ country_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value">{{ city_display|length }}</span>
|
|
||||||
<span class="stat-label">{{ city_display|length == 1 ? 'city' : 'cities' }} visited</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
<span class="stat-value" id="stat-distance">—</span>
|
|
||||||
<span class="stat-label">{{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-block">
|
|
||||||
{% if temp_min is not null %}
|
|
||||||
<span class="stat-value">{{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="stat-value">—</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="stat-label">°C range</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if country_display|length > 0 %}
|
|
||||||
<div class="stats-countries">
|
|
||||||
<span class="stats-countries-label">Countries visited</span>
|
|
||||||
{{ country_display|join(' · ') }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var GPS_POINTS = {{ gps_points|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
|
|
||||||
function haversine(lat1, lng1, lat2, lng2) {
|
|
||||||
var R = 6371;
|
|
||||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
|
||||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
|
||||||
var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
|
|
||||||
Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*
|
|
||||||
Math.sin(dLng/2)*Math.sin(dLng/2);
|
|
||||||
return R * 2 * Math.asin(Math.sqrt(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
var distEl = document.getElementById('stat-distance');
|
|
||||||
|
|
||||||
if (GPX_URLS.length > 0) {
|
|
||||||
// Mode A: sum haversine between all GPX trackpoints (deterministic order)
|
|
||||||
var pending = GPX_URLS.length;
|
|
||||||
var fileResults = new Array(GPX_URLS.length);
|
|
||||||
GPX_URLS.forEach(function(url, idx) {
|
|
||||||
fetch(url)
|
|
||||||
.then(function(r) { return r.text(); })
|
|
||||||
.then(function(text) {
|
|
||||||
var xml = new DOMParser().parseFromString(text, 'text/xml');
|
|
||||||
var trkpts = xml.querySelectorAll('trkpt');
|
|
||||||
var pts = [];
|
|
||||||
trkpts.forEach(function(pt) {
|
|
||||||
pts.push({
|
|
||||||
lat: parseFloat(pt.getAttribute('lat')),
|
|
||||||
lon: parseFloat(pt.getAttribute('lon'))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
fileResults[idx] = pts;
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeTotalDistance(); }
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.warn('GPX load failed:', url, err);
|
|
||||||
fileResults[idx] = [];
|
|
||||||
pending--;
|
|
||||||
if (pending === 0) { computeTotalDistance(); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function computeTotalDistance() {
|
|
||||||
var total = 0;
|
|
||||||
fileResults.forEach(function(pts) {
|
|
||||||
for (var i = 1; i < pts.length; i++) {
|
|
||||||
total += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
distEl.textContent = total > 0 ? Math.round(total).toLocaleString() : '—';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mode B: sum haversine between consecutive entry lat/lng points
|
|
||||||
var total = 0;
|
|
||||||
for (var i = 1; i < GPS_POINTS.length; i++) {
|
|
||||||
total += haversine(
|
|
||||||
parseFloat(GPS_POINTS[i-1][0]), parseFloat(GPS_POINTS[i-1][1]),
|
|
||||||
parseFloat(GPS_POINTS[i][0]), parseFloat(GPS_POINTS[i][1])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
distEl.textContent = GPS_POINTS.length < 2 ? '—' : '~' + Math.round(total).toLocaleString();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
|
||||||
|
|
||||||
{% block map_assets %}
|
|
||||||
{% do assets.addCss('theme://css-compiled/map.css') %}
|
|
||||||
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% import 'macros/date-range.html.twig' as dr_m %}
|
|
||||||
{% set stories = page.children.published().order('date', 'asc') %}
|
|
||||||
|
|
||||||
{# Collect stories that have coordinates for the mini-map #}
|
|
||||||
{% set map_entries = [] %}
|
|
||||||
{% for story in stories %}
|
|
||||||
{% if story.header.lat is not empty and story.header.lng is not empty %}
|
|
||||||
{% set map_entries = map_entries|merge([{
|
|
||||||
'lat': story.header.lat,
|
|
||||||
'lng': story.header.lng,
|
|
||||||
'title': story.title,
|
|
||||||
'slug': story.slug,
|
|
||||||
'url': story.url,
|
|
||||||
'type': 'story',
|
|
||||||
'force_connect': false,
|
|
||||||
'transport_mode': null
|
|
||||||
}]) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% set trip_page = page.parent() %}
|
|
||||||
|
|
||||||
{% include 'partials/feed-map.html.twig' with {
|
|
||||||
'map_entries': map_entries,
|
|
||||||
'map_id': 'stories-map',
|
|
||||||
'map_var': 'storiesMap',
|
|
||||||
'link_href': null,
|
|
||||||
'card_prefix': 'story-',
|
|
||||||
'trip_page': trip_page,
|
|
||||||
'show_journey': false
|
|
||||||
} only %}
|
|
||||||
|
|
||||||
<div class="stories-listing">
|
|
||||||
<div class="stories-listing__header">
|
|
||||||
<h1 class="stories-listing__heading">Stories</h1>
|
|
||||||
<button class="trip-stats-btn" id="feed-sort-toggle" aria-label="Sort: oldest first">↑ Oldest first</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if stories|length > 0 %}
|
|
||||||
<div class="stories-grid">
|
|
||||||
{% for story in stories %}
|
|
||||||
{% set hero = null %}
|
|
||||||
{% if story.header.hero_image and story.media[story.header.hero_image] is defined %}
|
|
||||||
{% set hero = story.media[story.header.hero_image] %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set date_str = dr_m.format_date_range(story.date, story.header.end_date ?? null) %}
|
|
||||||
|
|
||||||
<a class="story-card" id="story-{{ story.slug }}" href="{{ story.url }}">
|
|
||||||
{% if hero %}
|
|
||||||
<div class="story-card__photo">
|
|
||||||
<img src="{{ hero.cropResize(720, 405).url }}" alt="{{ story.title }}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="story-card__photo story-card__photo--empty"></div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="story-card__body">
|
|
||||||
<time class="story-card__date" datetime="{{ story.date|date('Y-m-d') }}">{{ date_str }}</time>
|
|
||||||
{% if story.header.location_name %}
|
|
||||||
<span class="story-card__location">📍 {{ story.header.location_name }}{% if story.header.location_country %}, {{ story.header.location_country }}{% endif %}</span>
|
|
||||||
{% endif %}
|
|
||||||
<h2 class="story-card__title">{{ story.title }}</h2>
|
|
||||||
<span class="story-card__cta">Read story →</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="stories-empty">No stories yet — check back soon.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<a class="story-escape" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="story-escape" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
|
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
{{ page.content|raw }}
|
{{ page.content|raw }}
|
||||||
|
|
||||||
<footer class="story-footer">
|
<footer class="story-footer">
|
||||||
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
<a class="back-pill" href="{{ page.parent().parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -56,16 +56,18 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
{% include 'partials/entry-map.html.twig' with {
|
||||||
<div class="home-map" id="trip-map">
|
map_id: 'trip-map',
|
||||||
<button class="feed-map-fullscreen-btn" id="trip-map-fullscreen" aria-label="Expand map">
|
map_global: 'tripMap',
|
||||||
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
entries: map_entries,
|
||||||
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
card_prefix: 'entry-',
|
||||||
</svg>
|
story_markers: true,
|
||||||
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
gpx_urls: gpx_urls,
|
||||||
</button>
|
use_gpx: page.header.use_gpx ?? true,
|
||||||
</div>
|
autoconnect: page.header.autoconnect ?? 'on',
|
||||||
</div>
|
gpx_source_prefix: 'gpx',
|
||||||
|
journey_id: 'trip-journey'
|
||||||
|
} only %}
|
||||||
|
|
||||||
{% include 'partials/trip-feed-col.html.twig' with {
|
{% include 'partials/trip-feed-col.html.twig' with {
|
||||||
trip_page: page,
|
trip_page: page,
|
||||||
@@ -80,99 +82,6 @@
|
|||||||
} only %}
|
} only %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
|
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
|
||||||
var USE_GPX = {{ page.header.use_gpx ?? true ? 'true' : 'false' }};
|
|
||||||
var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
|
|
||||||
var tripMap = new maplibregl.Map({
|
|
||||||
container: 'trip-map',
|
|
||||||
style: MapUtils.MAP_STYLE,
|
|
||||||
center: [20, 20],
|
|
||||||
zoom: 2,
|
|
||||||
attributionControl: false
|
|
||||||
});
|
|
||||||
tripMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
|
||||||
|
|
||||||
tripMap.on('load', function () {
|
|
||||||
if (TRIP_ENTRIES.length === 0) {
|
|
||||||
tripMap.jumpTo({ center: [0, 20], zoom: 2 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Markers + bounds ──────────────────────────────────────── */
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
TRIP_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (entry.type !== 'story') && (i === TRIP_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = entry.type === 'story' ? MapUtils.createStoryMarker() : MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(tripMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (!card) return;
|
|
||||||
var mapCol = document.querySelector('.home-map-col');
|
|
||||||
var isFs = mapCol && mapCol.classList.contains('is-fullscreen');
|
|
||||||
function scrollAndHighlight() {
|
|
||||||
window.location.hash = 'entry-' + entry.slug;
|
|
||||||
setTimeout(function () {
|
|
||||||
card.classList.add('is-highlighted');
|
|
||||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
|
||||||
}, 350);
|
|
||||||
}
|
|
||||||
if (isFs) {
|
|
||||||
var fsBtn = document.getElementById('trip-map-fullscreen');
|
|
||||||
if (fsBtn) fsBtn.click();
|
|
||||||
setTimeout(scrollAndHighlight, 450);
|
|
||||||
} else {
|
|
||||||
scrollAndHighlight();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(tripMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Fit bounds ─────────────────────────────────────────────── */
|
|
||||||
if (TRIP_ENTRIES.length === 1) {
|
|
||||||
tripMap.jumpTo({ center: [parseFloat(TRIP_ENTRIES[0].lng), parseFloat(TRIP_ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
tripMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
|
||||||
MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
|
|
||||||
|
|
||||||
// Collapse attribution <details> which MapLibre may open on load
|
|
||||||
var attrib = tripMap.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
|
||||||
if (attrib) attrib.removeAttribute('open');
|
|
||||||
});
|
|
||||||
setTimeout(function () { tripMap.resize(); }, 100);
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var fsBtn = document.getElementById('trip-map-fullscreen');
|
|
||||||
var mapCol = document.querySelector('.home-map-col');
|
|
||||||
if (!fsBtn || !mapCol) return;
|
|
||||||
fsBtn.addEventListener('click', function() {
|
|
||||||
var isFs = mapCol.classList.toggle('is-fullscreen');
|
|
||||||
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
|
||||||
document.body.style.overflow = isFs ? 'hidden' : '';
|
|
||||||
setTimeout(function() { tripMap.resize(); }, 50);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
}); // DOMContentLoaded
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="story-totop" id="trip-totop" aria-label="Back to top">↑ Top</button>
|
<button class="story-totop" id="trip-totop" aria-label="Back to top">↑ Top</button>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user