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);