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:
@@ -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:
|
||||
|
||||
@@ -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 = [];
|
||||
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
|
||||
* entries: [{lat, lng, force_connect?}, ...] in chronological order
|
||||
* 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
|
||||
|
||||
@@ -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,23 +79,8 @@ 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 });
|
||||
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
||||
});
|
||||
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { autoconnect: AUTOCONNECT });
|
||||
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user