diff --git a/themes/intotheeast/css/style.css b/themes/intotheeast/css/style.css
index 3defae1..9917153 100644
--- a/themes/intotheeast/css/style.css
+++ b/themes/intotheeast/css/style.css
@@ -990,7 +990,7 @@ body::after {
.trip-stats-grid {
display: grid;
- grid-template-columns: repeat(4, 1fr);
+ grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
margin-bottom: var(--space-4);
}
@@ -1010,6 +1010,43 @@ body::after {
color: var(--color-ink-muted);
}
+/* ── Trip page cycling panel ─────────────────────────────────────────────────── */
+
+.trip-cycling-block {
+ background: var(--color-canvas);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-6);
+ margin-bottom: var(--space-6);
+}
+
+.trip-cycling-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-bottom: var(--space-4);
+}
+
+.trip-cycling-icon {
+ font-size: var(--text-xl);
+}
+
+.trip-cycling-title {
+ font-family: var(--font-display);
+ font-size: var(--text-lg);
+ color: var(--color-ink);
+}
+
+.trip-cycling-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: var(--space-4);
+}
+
+@media (max-width: 600px) {
+ .trip-cycling-grid { grid-template-columns: repeat(2, 1fr); }
+}
+
/* ── Story pages ─────────────────────────────────────────────────────────── */
/* Override site-main constraints for story pages */
diff --git a/themes/intotheeast/templates/trip.html.twig b/themes/intotheeast/templates/trip.html.twig
index 8c66e21..4fa58a0 100644
--- a/themes/intotheeast/templates/trip.html.twig
+++ b/themes/intotheeast/templates/trip.html.twig
@@ -20,18 +20,21 @@
{# Stats computation #}
{% set days_on_road = 0 %}
-{% set first_ts = null %}
-{% for entry in journal_entries %}
- {% set ts = entry.date|date('U') %}
- {% if first_ts is null or ts < first_ts %}
- {% set first_ts = ts %}
+{% if page.header.date_end is not empty %}
+ {% set start_ts = page.header.date_start|date('U') %}
+ {% set end_ts = page.header.date_end|date('U') %}
+ {% set days_on_road = ((end_ts - start_ts) / 86400)|round(0, 'ceil') %}
+{% else %}
+ {% set first_ts = null %}
+ {% for entry in journal_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 %}
-{% endfor %}
-{% if first_ts is not null %}
- {% set now_ts = "now"|date('U') %}
- {% set diff_seconds = now_ts - first_ts %}
- {% set days_raw = (diff_seconds / 86400)|round(0, 'floor') %}
- {% set days_on_road = days_raw < 1 ? 1 : days_raw %}
{% endif %}
{% set seen_lower = [] %}
@@ -46,6 +49,28 @@
{% endif %}
{% endfor %}
+{% set seen_city_lower = [] %}
+{% set city_display = [] %}
+{% for entry in journal_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 %}
+
+{% set temp_min = null %}
+{% set temp_max = null %}
+{% for entry in journal_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 %}
+
{% set gps_points = [] %}
{% for entry in journal_entries %}
{% if entry.header.lat is not empty and entry.header.lng is not empty %}
@@ -59,6 +84,7 @@
{% set gpx_urls = gpx_urls|merge([page.url ~ '/' ~ name]) %}
{% endif %}
{% endfor %}
+{% set has_gpx = gpx_urls|length > 0 %}
{% set map_entries = [] %}
{% for item in all_items %}
@@ -97,7 +123,12 @@
-
+
+
+ {% if has_gpx %}
+
+ {% endif %}
+
@@ -115,17 +146,68 @@
{{ country_display|length }}
{{ country_display|length == 1 ? 'country' : 'countries' }} visited
+
+ {{ city_display|length }}
+ {{ city_display|length == 1 ? 'city' : 'cities' }} visited
+
—
- km traveled
+ {{ has_gpx ? '🚴 km cycled' : '🧭 km roamed' }}
+
+
+ {% if temp_min is not null %}
+ {{ temp_min == temp_max ? temp_min : temp_min ~ ' → ' ~ temp_max }}
+ {% else %}
+ —
+ {% endif %}
+ °C range
{% if country_display|length > 0 %}
{{ country_display|join(' · ') }}
{% endif %}
- Distance is approximate — straight lines between entry locations.
+ {{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}
+ {% if has_gpx %}
+
+
+
+
+ —
+ km distance
+
+
+ —
+ m ↑ gain
+
+
+ —
+ m ↓ loss
+
+
+ —
+ m highest
+
+
+ —
+ m lowest
+
+
+ —
+ moving time
+
+
+ —
+ km/h avg speed
+
+
+
+ {% endif %}
+
{% if all_items|length > 0 %}
{% for item in all_items %}
@@ -309,32 +391,132 @@ setTimeout(function () { tripMap.resize(); }, 100);
})();
var STATS_GPS = {{ gps_points|json_encode|raw }};
+var HAS_GPX = {{ has_gpx ? 'true' : 'false' }};
+
+function haversineKm(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));
+}
+
+function parseGpxFiles(urls, callback) {
+ var pending = urls.length;
+ var masterPts = [];
+ if (pending === 0) { callback({ error: 'no files' }); return; }
+ urls.forEach(function(url) {
+ fetch(url)
+ .then(function(r) { return r.text(); })
+ .then(function(text) {
+ var xml = new DOMParser().parseFromString(text, 'text/xml');
+ var trkpts = xml.querySelectorAll('trkpt');
+ trkpts.forEach(function(pt) {
+ var eleEl = pt.querySelector('ele');
+ var timeEl = pt.querySelector('time');
+ masterPts.push({
+ lat: parseFloat(pt.getAttribute('lat')),
+ lon: parseFloat(pt.getAttribute('lon')),
+ ele: eleEl ? parseFloat(eleEl.textContent) : NaN,
+ time: timeEl ? timeEl.textContent : null
+ });
+ });
+ pending--;
+ if (pending === 0) { computeAndCallback(); }
+ })
+ .catch(function(err) {
+ console.warn('GPX load failed:', url, err);
+ pending--;
+ if (pending === 0) { computeAndCallback(); }
+ });
+ });
+
+ function computeAndCallback() {
+ var n = masterPts.length;
+ if (n < 2) { callback({ distance: 0 }); return; }
+ var distance = 0, eleGain = 0, eleLoss = 0;
+ var highest = NaN, lowest = NaN, movingTime = 0;
+ for (var i = 1; i < n; i++) {
+ var p0 = masterPts[i-1], p1 = masterPts[i];
+ var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
+ distance += d;
+ if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
+ var dEle = p1.ele - p0.ele;
+ if (dEle > 1) eleGain += dEle - 1;
+ else if (dEle < -1) eleLoss += (-dEle) - 1;
+ if (isNaN(highest) || p1.ele > highest) highest = p1.ele;
+ if (isNaN(lowest) || p1.ele < lowest) lowest = p1.ele;
+ }
+ if (p0.time && p1.time) {
+ var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
+ if (dtHrs > 0) {
+ var speed = d / dtHrs;
+ if (speed >= 1) movingTime += dtHrs;
+ }
+ }
+ }
+ // include first point in elevation range
+ if (!isNaN(masterPts[0].ele)) {
+ if (isNaN(highest) || masterPts[0].ele > highest) highest = masterPts[0].ele;
+ if (isNaN(lowest) || masterPts[0].ele < lowest) lowest = masterPts[0].ele;
+ }
+ var avgSpeed = movingTime > 0 ? distance / movingTime : 0;
+ var movHours = Math.floor(movingTime);
+ var movMins = Math.round((movingTime - movHours) * 60);
+ if (movMins === 60) { movHours++; movMins = 0; }
+ callback({
+ distance: distance,
+ eleGain: eleGain,
+ eleLoss: eleLoss,
+ highest: highest,
+ lowest: lowest,
+ movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
+ avgSpeed: avgSpeed
+ });
+ }
+}
(function() {
- 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 totalKm = 0;
- for (var i = 1; i < STATS_GPS.length; i++) {
- totalKm += haversine(
- parseFloat(STATS_GPS[i - 1][0]), parseFloat(STATS_GPS[i - 1][1]),
- parseFloat(STATS_GPS[i][0]), parseFloat(STATS_GPS[i][1])
- );
- }
var distEl = document.getElementById('stat-distance');
- if (distEl) {
- distEl.textContent = STATS_GPS.length < 2 ? '—' : '~' + Math.round(totalKm).toLocaleString();
+
+ if (HAS_GPX) {
+ parseGpxFiles(GPX_URLS, function(result) {
+ // Mode A: update distance stat
+ if (distEl) {
+ distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
+ }
+ // Populate cycling panel
+ function setText(id, val) {
+ var el = document.getElementById(id);
+ if (el) el.textContent = val;
+ }
+ setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—');
+ setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—');
+ setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—');
+ setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—');
+ setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—');
+ setText('cyc-moving-time', result.movingTime || '—');
+ setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
+ });
+ } else {
+ // Mode B: haversine between entry points
+ var total = 0;
+ for (var i = 1; i < STATS_GPS.length; i++) {
+ total += haversineKm(
+ parseFloat(STATS_GPS[i-1][0]), parseFloat(STATS_GPS[i-1][1]),
+ parseFloat(STATS_GPS[i][0]), parseFloat(STATS_GPS[i][1])
+ );
+ }
+ if (distEl) {
+ distEl.textContent = STATS_GPS.length < 2 ? '—' : '~' + Math.round(total).toLocaleString();
+ }
}
+ // Stats toggle
var statsToggle = document.getElementById('trip-stats-toggle');
- var statsBlock = document.getElementById('trip-stats-block');
+ var statsBlock = document.getElementById('trip-stats-block');
if (statsToggle && statsBlock) {
statsToggle.addEventListener('click', function() {
var isOpen = statsBlock.style.display !== 'none';
@@ -342,6 +524,17 @@ var STATS_GPS = {{ gps_points|json_encode|raw }};
statsToggle.classList.toggle('is-active', !isOpen);
});
}
+
+ // Cycling toggle (only present when has_gpx)
+ var cycToggle = document.getElementById('trip-cycling-toggle');
+ var cycBlock = document.getElementById('trip-cycling-block');
+ if (cycToggle && cycBlock) {
+ cycToggle.addEventListener('click', function() {
+ var isOpen = cycBlock.style.display !== 'none';
+ cycBlock.style.display = isOpen ? 'none' : '';
+ cycToggle.classList.toggle('is-active', !isOpen);
+ });
+ }
})();
{% endblock %}