diff --git a/themes/intotheeast/blueprints/trip.yaml b/themes/intotheeast/blueprints/trip.yaml
index 1ff7576..37c8e00 100644
--- a/themes/intotheeast/blueprints/trip.yaml
+++ b/themes/intotheeast/blueprints/trip.yaml
@@ -68,16 +68,15 @@ form:
type: bool
header.autoconnect:
- type: toggle
+ type: select
label: Connect markers
- help: 'Draw connector lines between all location markers in chronological order'
- highlight: 1
- default: 1
+ help: 'Controls connector lines between location markers'
+ default: 'on'
options:
- 1: 'Yes'
- 0: 'No'
- validate:
- type: bool
+ 'on': 'On — always connect all'
+ 'manual': 'Manual — force connect only'
+ 'intelligent_gpx': 'Intelligent GPX — suppress where route is covered'
+ 'off': 'Off — no connections'
publishing:
type: tab
diff --git a/themes/intotheeast/js/maplibre-utils.js b/themes/intotheeast/js/maplibre-utils.js
index 4db9c57..fe53b40 100644
--- a/themes/intotheeast/js/maplibre-utils.js
+++ b/themes/intotheeast/js/maplibre-utils.js
@@ -152,21 +152,65 @@
return el;
}
+ /* Haversine distance in km between two [lat, lng] points */
+ 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.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ }
+
+ /* Extract [[lat, lng], ...] from a toGeoJSON output (flips GeoJSON [lng,lat] order). */
+ function extractTrackpoints(geojson) {
+ var points = [];
+ (geojson.features || []).forEach(function (feat) {
+ var coords = [];
+ if (feat.geometry.type === 'LineString') {
+ coords = feat.geometry.coordinates;
+ } else if (feat.geometry.type === 'MultiLineString') {
+ feat.geometry.coordinates.forEach(function (line) { coords = coords.concat(line); });
+ }
+ coords.forEach(function (c) { points.push([c[1], c[0]]); });
+ });
+ return points;
+ }
+
+ /* True if [markerLat, markerLng] is within thresholdKm of any point in trackpoints [[lat,lng],...]. */
+ function isNearTrack(markerLat, markerLng, trackpoints, thresholdKm) {
+ if (!trackpoints || trackpoints.length === 0) return false;
+ var degLat = thresholdKm / 111;
+ var degLng = thresholdKm / (111 * Math.cos(markerLat * Math.PI / 180));
+ for (var i = 0; i < trackpoints.length; i += 10) {
+ var pt = trackpoints[i];
+ if (Math.abs(pt[0] - markerLat) > degLat || Math.abs(pt[1] - markerLng) > degLng) continue;
+ if (haversineKm(markerLat, markerLng, pt[0], pt[1]) <= thresholdKm) return true;
+ }
+ var last = trackpoints[trackpoints.length - 1];
+ return haversineKm(markerLat, markerLng, last[0], last[1]) <= thresholdKm;
+ }
+
/*
* Build journey line segments from entries.
*
- * entries: [{lat, lng, force_connect?}, ...] in chronological order
- * opts.autoconnect (bool, default true):
- * true → connect every consecutive pair (simple chronological line)
- * false → connect only entries where force_connect:true
+ * entries: [{lat, lng, force_connect?}, ...] in chronological order
+ * opts.connectMode: 'on' | 'off' | 'manual' | 'intelligent_gpx' (default: 'on')
+ * 'on' → connect every consecutive pair (force_connect irrelevant)
+ * 'off' → no connectors, force_connect is also ignored
+ * 'manual' → only entries with force_connect:true draw a connector
+ * 'intelligent_gpx'→ suppress connector where GPX covers both endpoints;
+ * force_connect always overrides
+ * trackpointsPerFile: [ [[lat,lng],...], ... ] — required only for intelligent_gpx
*
* Returns array of segments [[lng, lat], ...] in MapLibre coordinate order.
* Segments with < 2 points are omitted.
*/
- function buildJourneySegments(entries, opts) {
- var autoconnect = !opts || opts.autoconnect !== false;
- var segments = [];
- var current = [];
+ function buildJourneySegments(entries, opts, trackpointsPerFile) {
+ var mode = (opts && opts.connectMode) || 'on';
+ var segments = [];
+ var current = [];
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
@@ -174,7 +218,30 @@
if (i === 0) { current.push(lngLat); continue; }
- var connect = e.force_connect || autoconnect;
+ var connect;
+ if (mode === 'off') {
+ connect = false;
+ } else if (mode === 'on') {
+ connect = true;
+ } else if (mode === 'manual') {
+ connect = !!e.force_connect;
+ } else { /* intelligent_gpx */
+ if (e.force_connect) {
+ connect = true;
+ } else if (!trackpointsPerFile || trackpointsPerFile.length === 0) {
+ connect = true; /* no GPX present → connect all */
+ } else {
+ var prev = entries[i - 1];
+ var covered = false;
+ for (var f = 0; f < trackpointsPerFile.length; f++) {
+ if (isNearTrack(parseFloat(prev.lat), parseFloat(prev.lng), trackpointsPerFile[f], 10) &&
+ isNearTrack(parseFloat(e.lat), parseFloat(e.lng), trackpointsPerFile[f], 10)) {
+ covered = true; break;
+ }
+ }
+ connect = !covered;
+ }
+ }
if (connect) {
current.push(lngLat);
@@ -202,15 +269,16 @@
/*
* Fetch GPX files and render their raw tracks, then draw journey connector
- * lines between entries per opts.autoconnect / force_connect.
+ * lines between entries per opts.connectMode / force_connect.
*
* gpxUrls: array of GPX file URLs to fetch (empty → no tracks, connectors only)
* entries: [{lat, lng, force_connect?}, ...] in chronological order
* gpxSourcePrefix: source/layer ID prefix for raw GPX tracks (e.g. 'gpx', 'home-gpx')
* journeySourceId: base source ID for connector segments (e.g. 'journey', 'home-journey')
- * opts: forwarded to buildJourneySegments (opts.autoconnect)
+ * opts: forwarded to buildJourneySegments (opts.connectMode)
*/
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
+ var needsTrackpoints = opts && opts.connectMode === 'intelligent_gpx';
Promise.all(gpxUrls.map(function (url, idx) {
return fetch(url)
.then(function (r) { return r.text(); })
@@ -224,10 +292,12 @@
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': ACCENT, 'line-width': 2, 'line-opacity': 0.7 }
});
+ return needsTrackpoints ? extractTrackpoints(geojson) : [];
})
- .catch(function (err) { console.warn('GPX load failed:', url, err); });
- })).then(function () {
- var segments = buildJourneySegments(entries, opts);
+ .catch(function (err) { console.warn('GPX load failed:', url, err); return []; });
+ })).then(function (allTrackpoints) {
+ var valid = needsTrackpoints ? allTrackpoints.filter(function (tp) { return tp.length > 0; }) : [];
+ var segments = buildJourneySegments(entries, opts, valid);
addJourneySegments(map, segments, journeySourceId);
});
}
diff --git a/themes/intotheeast/templates/dailies.html.twig b/themes/intotheeast/templates/dailies.html.twig
index 5948e02..83c3943 100644
--- a/themes/intotheeast/templates/dailies.html.twig
+++ b/themes/intotheeast/templates/dailies.html.twig
@@ -44,7 +44,8 @@
diff --git a/themes/intotheeast/templates/home.html.twig b/themes/intotheeast/templates/home.html.twig
index 42d2ec1..f24f343 100644
--- a/themes/intotheeast/templates/home.html.twig
+++ b/themes/intotheeast/templates/home.html.twig
@@ -152,7 +152,7 @@
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 and trip.header.autoconnect is not null ? (trip.header.autoconnect ? 'true' : 'false') : 'true' }};
+var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
var homeMap = new maplibregl.Map({
container: 'home-map',
@@ -194,7 +194,7 @@ homeMap.on('load', function () {
setTimeout(function () { homeMap.resize(); }, 100);
- MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { autoconnect: AUTOCONNECT });
+ MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { connectMode: AUTOCONNECT });
});
{% endif %}
diff --git a/themes/intotheeast/templates/map.html.twig b/themes/intotheeast/templates/map.html.twig
index cecd355..7a8a9f7 100644
--- a/themes/intotheeast/templates/map.html.twig
+++ b/themes/intotheeast/templates/map.html.twig
@@ -45,7 +45,7 @@
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 ?? true ? 'true' : 'false' }};
+var AUTOCONNECT = "{{ trip_page.header.autoconnect ?? 'on' }}";
var map = new maplibregl.Map({
container: 'trip-map',
@@ -94,7 +94,7 @@ map.on('load', function () {
}
/* ── GPX tracks + journey segments ─────────────────────────── */
- MapUtils.renderGpxJourney(map, USE_GPX ? GPX_URLS : [], ENTRIES, 'gpx', 'journey', { autoconnect: AUTOCONNECT });
+ MapUtils.renderGpxJourney(map, USE_GPX ? GPX_URLS : [], ENTRIES, 'gpx', 'journey', { connectMode: AUTOCONNECT });
});
{% endblock %}
diff --git a/themes/intotheeast/templates/trip.html.twig b/themes/intotheeast/templates/trip.html.twig
index c1ec930..06b3342 100644
--- a/themes/intotheeast/templates/trip.html.twig
+++ b/themes/intotheeast/templates/trip.html.twig
@@ -300,7 +300,7 @@
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 ?? true ? 'true' : 'false' }};
+var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
var tripMap = new maplibregl.Map({
container: 'trip-map',
@@ -351,7 +351,7 @@ tripMap.on('load', function () {
}
/* ── GPX tracks + journey segments ─────────────────────────── */
- MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { autoconnect: AUTOCONNECT });
+ MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
});
setTimeout(function () { tripMap.resize(); }, 100);