feat: expand stats page to 6 stats — cities, temp range, distance mode detection
This commit is contained in:
@@ -467,11 +467,15 @@ body::after {
|
|||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
margin-bottom: var(--space-8);
|
margin-bottom: var(--space-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
.stat-block {
|
.stat-block {
|
||||||
background: var(--color-canvas);
|
background: var(--color-canvas);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
{% extends 'partials/base.html.twig' %}
|
{% extends 'partials/base.html.twig' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set tracker_page = grav.pages.find(page.parent().route ~ '/dailies') %}
|
{% set trip_page = page.parent() %}
|
||||||
|
{% set tracker_page = grav.pages.find(trip_page.route ~ '/dailies') %}
|
||||||
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
{% set all_entries = tracker_page ? tracker_page.children.published() : [] %}
|
||||||
|
|
||||||
{# Basic counts #}
|
{# Basic counts #}
|
||||||
{% set entry_count = all_entries|length %}
|
{% set entry_count = all_entries|length %}
|
||||||
|
|
||||||
{# Days on the road — find earliest entry timestamp by iterating #}
|
{# Days on road — past trip uses declared date_end; active trip uses first entry to now #}
|
||||||
{% set days_on_road = 0 %}
|
{% set days_on_road = 0 %}
|
||||||
{% set first_ts = null %}
|
{% if trip_page.header.date_end is not empty %}
|
||||||
{% for entry in all_entries %}
|
{# 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') %}
|
{% set ts = entry.date|date('U') %}
|
||||||
{% if first_ts is null or ts < first_ts %}
|
{% if first_ts is null or ts < first_ts %}{% set first_ts = ts %}{% endif %}
|
||||||
{% set first_ts = ts %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% if first_ts is not null %}
|
||||||
{% endfor %}
|
{% set diff_seconds = "now"|date('U') - first_ts %}
|
||||||
{% 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_raw = (diff_seconds / 86400)|round(0, 'floor') %}
|
||||||
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
{% set days_on_road = days_raw < 1 ? 1 : days_raw %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Countries — unique, case-insensitive dedup, preserve original casing #}
|
{# Countries — unique, case-insensitive dedup, preserve original casing #}
|
||||||
@@ -36,6 +42,30 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% 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 #}
|
{# GPS points for distance — collect as JSON for JS computation #}
|
||||||
{% set gps_points = [] %}
|
{% set gps_points = [] %}
|
||||||
{% for entry in all_entries %}
|
{% for entry in all_entries %}
|
||||||
@@ -44,6 +74,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% 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">
|
<div class="stats-page">
|
||||||
<h1 class="stats-heading">Trip Statistics</h1>
|
<h1 class="stats-heading">Trip Statistics</h1>
|
||||||
|
|
||||||
@@ -60,9 +99,21 @@
|
|||||||
<span class="stat-value">{{ country_display|length }}</span>
|
<span class="stat-value">{{ country_display|length }}</span>
|
||||||
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
<span class="stat-label">{{ country_display|length == 1 ? 'country' : 'countries' }} visited</span>
|
||||||
</div>
|
</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">
|
<div class="stat-block">
|
||||||
<span class="stat-value" id="stat-distance">—</span>
|
<span class="stat-value" id="stat-distance">—</span>
|
||||||
<span class="stat-label">km traveled</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,35 +124,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="stats-note">Distance is approximate — straight lines between entry locations.</p>
|
<p class="stats-note">{{ has_gpx ? 'Distance based on GPS track data.' : 'Distance is approximate — straight lines between entry locations.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var GPS_POINTS = {{ gps_points|json_encode|raw }};
|
var GPS_POINTS = {{ gps_points|json_encode|raw }};
|
||||||
|
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
||||||
|
|
||||||
function haversine(lat1, lng1, lat2, lng2) {
|
function haversine(lat1, lng1, lat2, lng2) {
|
||||||
var R = 6371;
|
var R = 6371;
|
||||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
var dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
var dLng = (lng2 - lng1) * Math.PI / 180;
|
||||||
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
var a = Math.sin(dLat/2)*Math.sin(dLat/2) +
|
||||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*
|
||||||
Math.sin(dLng/2) * Math.sin(dLng/2);
|
Math.sin(dLng/2)*Math.sin(dLng/2);
|
||||||
return R * 2 * Math.asin(Math.sqrt(a));
|
return R * 2 * Math.asin(Math.sqrt(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
var total = 0;
|
var distEl = document.getElementById('stat-distance');
|
||||||
for (var i = 1; i < GPS_POINTS.length; i++) {
|
|
||||||
|
if (GPX_URLS.length > 0) {
|
||||||
|
// Mode A: sum haversine between all GPX trackpoints
|
||||||
|
var pending = GPX_URLS.length;
|
||||||
|
var masterPts = [];
|
||||||
|
GPX_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) {
|
||||||
|
masterPts.push({
|
||||||
|
lat: parseFloat(pt.getAttribute('lat')),
|
||||||
|
lon: parseFloat(pt.getAttribute('lon'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pending--;
|
||||||
|
if (pending === 0) {
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 1; i < masterPts.length; i++) {
|
||||||
|
total += haversine(masterPts[i-1].lat, masterPts[i-1].lon,
|
||||||
|
masterPts[i].lat, masterPts[i].lon);
|
||||||
|
}
|
||||||
|
distEl.textContent = masterPts.length < 2 ? '—' : Math.round(total).toLocaleString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) { console.warn('GPX load failed:', url, err); pending--; });
|
||||||
|
});
|
||||||
|
} 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(
|
total += haversine(
|
||||||
parseFloat(GPS_POINTS[i-1][0]), parseFloat(GPS_POINTS[i-1][1]),
|
parseFloat(GPS_POINTS[i-1][0]), parseFloat(GPS_POINTS[i-1][1]),
|
||||||
parseFloat(GPS_POINTS[i][0]), parseFloat(GPS_POINTS[i][1])
|
parseFloat(GPS_POINTS[i][0]), parseFloat(GPS_POINTS[i][1])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
distEl.textContent = GPS_POINTS.length < 2 ? '—' : '~' + Math.round(total).toLocaleString();
|
||||||
var el = document.getElementById('stat-distance');
|
|
||||||
if (GPS_POINTS.length < 2) {
|
|
||||||
el.textContent = '—';
|
|
||||||
} else {
|
|
||||||
el.textContent = '~' + Math.round(total).toLocaleString();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user