refactor: simplify connector logic — remove GPX proximity suppression

autoconnect:true now connects every consecutive entry pair in
chronological order; the old proximity check (suppress where GPX
covers the route) is removed entirely.

- buildJourneySegments: drops allTrackpoints/thresholdKm params;
  logic is now force_connect || autoconnect (binary, no GPX math)
- renderGpxJourney: no longer extracts trackpoints; just renders
  visual GPX layers then calls buildJourneySegments
- dailies.html.twig: removes GPX URL collection, toGeoJSON CDN load,
  and the Promise.all — connectors are now synchronous
- extractTrackpoints/isNearTrack/haversineKm removed (dead code)
- blueprint help text updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
2026-06-21 11:38:50 +02:00
parent 21b572677e
commit 9809950347
3 changed files with 25 additions and 147 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ form:
header.autoconnect:
type: toggle
label: Connect markers
help: 'Draw connector lines between location markers (suppressed where GPX covers the route)'
help: 'Draw connector lines between all location markers in chronological order'
highlight: 1
default: 1
options:
+19 -116
View File
@@ -152,127 +152,35 @@
return el;
}
/* ── GPX connector algorithm ────────────────────────────────────────── */
/* 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 trackpoints from a toGeoJSON output.
* Returns [[lat, lng], ...] — latitude first (internal convention).
* GeoJSON coordinates are [lng, lat]; we flip them here.
*/
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]]); }); // [lng,lat] → [lat,lng]
});
return points;
}
/*
* Check whether a marker is within thresholdKm of any trackpoint in the array.
* trackpoints: [[lat, lng], ...] (internal convention, latitude first).
* Samples every 10th point for performance; always checks the last point.
*/
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;
}
// Always check the last point (may be skipped by stride=10).
// Note: per-point degree pre-filter in the loop is functionally equivalent
// to a per-file bounding-box skip at this data scale.
var last = trackpoints[trackpoints.length - 1];
return haversineKm(markerLat, markerLng, last[0], last[1]) <= thresholdKm;
}
/*
* Build journey line segments from entries and GPX trackpoints.
* Build journey line segments from entries.
*
* entries: [{lat, lng, force_connect}, ...] in chronological order
* allTrackpoints: [ [[lat,lng],...], ... ] — one sub-array per GPX file
* thresholdKm: proximity radius (default 10)
* 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
*
* Returns array of segments, each segment being [[lng, lat], ...] in MapLibre
* coordinate order. A segment with < 2 points is omitted.
*
* Rules:
* - No GPX files → all adjacent pairs connected (one segment)
* - GPX present, pair covered by same file → connector suppressed
* - GPX present, pair NOT covered by any single file → connector drawn
* - force_connect on arriving entry → always draw connector
* Returns array of segments [[lng, lat], ...] in MapLibre coordinate order.
* Segments with < 2 points are omitted.
*/
/*
* opts.autoconnect (bool, default true): when false, only entries with
* force_connect:true are connected — all other pairs are left unconnected.
*/
function buildJourneySegments(entries, allTrackpoints, thresholdKm, opts) {
thresholdKm = thresholdKm || 10;
function buildJourneySegments(entries, opts) {
var autoconnect = !opts || opts.autoconnect !== false;
var hasGpx = allTrackpoints && allTrackpoints.length > 0;
var segments = [];
var current = [];
for (var i = 0; i < entries.length; i++) {
var e = entries[i];
var lngLat = [parseFloat(e.lng), parseFloat(e.lat)]; // MapLibre: [lng, lat]
var lngLat = [parseFloat(e.lng), parseFloat(e.lat)];
if (i === 0) {
current.push(lngLat);
continue;
}
if (i === 0) { current.push(lngLat); continue; }
var prev = entries[i - 1];
var connect;
if (e.force_connect) {
connect = true;
} else if (!autoconnect) {
connect = false;
} else if (!hasGpx) {
connect = true;
} else {
var pLat = parseFloat(prev.lat);
var pLng = parseFloat(prev.lng);
var cLat = parseFloat(e.lat);
var cLng = parseFloat(e.lng);
var covered = false;
for (var f = 0; f < allTrackpoints.length; f++) {
if (isNearTrack(pLat, pLng, allTrackpoints[f], thresholdKm) &&
isNearTrack(cLat, cLng, allTrackpoints[f], thresholdKm)) {
covered = true;
break;
}
}
connect = !covered;
}
var connect = e.force_connect || autoconnect;
if (connect) {
current.push(lngLat);
} else {
if (current.length >= 2) segments.push(current);
current = [lngLat]; // start new segment from this point
current = [lngLat];
}
}
@@ -293,16 +201,14 @@
}
/*
* Fetch GPX files, render their raw tracks, then draw journey connector lines
* only between entries not covered by any GPX file (connector suppression).
* Fetch GPX files and render their raw tracks, then draw journey connector
* lines between entries per opts.autoconnect / force_connect.
*
* gpxUrls: array of GPX file URLs to fetch
* 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')
*
* When gpxUrls is empty, Promise.all resolves immediately → no GPX layers,
* buildJourneySegments draws a full connector line between all entries.
* opts: forwarded to buildJourneySegments (opts.autoconnect)
*/
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
Promise.all(gpxUrls.map(function (url, idx) {
@@ -318,12 +224,10 @@
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': ACCENT, 'line-width': 2, 'line-opacity': 0.7 }
});
return extractTrackpoints(geojson);
})
.catch(function (err) { console.warn('GPX load failed:', url, err); return []; });
})).then(function (allTrackpoints) {
var valid = allTrackpoints.filter(function (tp) { return tp.length > 0; });
var segments = buildJourneySegments(entries, valid, 10, opts);
.catch(function (err) { console.warn('GPX load failed:', url, err); });
})).then(function () {
var segments = buildJourneySegments(entries, opts);
addJourneySegments(map, segments, journeySourceId);
});
}
@@ -334,7 +238,6 @@
addJourneyLine: addJourneyLine,
addJourneySegments: addJourneySegments,
buildJourneySegments: buildJourneySegments,
extractTrackpoints: extractTrackpoints,
renderGpxJourney: renderGpxJourney,
createDotMarker: createDotMarker,
createStoryMarker: createStoryMarker
+1 -26
View File
@@ -31,14 +31,7 @@
{% endif %}
{% endfor %}
{# Collect GPX URLs from parent trip page for connector algorithm #}
{% set trip_page = page.parent() %}
{% 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 %}
{% if map_entries|length > 0 %}
<div class="feed-map-wrap">
@@ -48,12 +41,9 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var FEED_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 feedMap = new maplibregl.Map({
@@ -89,24 +79,9 @@ feedMap.on('load', function () {
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
}
Promise.all((USE_GPX ? GPX_URLS : []).map(function (url) {
return fetch(url)
.then(function (r) { return r.text(); })
.then(function (text) {
var xml = new DOMParser().parseFromString(text, 'text/xml');
var geojson = toGeoJSON.gpx(xml);
return MapUtils.extractTrackpoints(geojson);
})
.catch(function (err) {
console.warn('GPX load failed (feed-map):', url, err);
return [];
});
})).then(function (allTrackpoints) {
var validTrackpoints = allTrackpoints.filter(function (tp) { return tp.length > 0; });
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, validTrackpoints, 10, { autoconnect: AUTOCONNECT });
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { autoconnect: AUTOCONNECT });
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
});
});
</script>
{% endif %}