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 %} + + {% 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 %}