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:
|
header.autoconnect:
|
||||||
type: toggle
|
type: toggle
|
||||||
label: Connect markers
|
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
|
highlight: 1
|
||||||
default: 1
|
default: 1
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -152,127 +152,35 @@
|
|||||||
return el;
|
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.
|
* Build journey line segments from entries.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* entries: [{lat, lng, force_connect}, ...] in chronological order
|
* entries: [{lat, lng, force_connect?}, ...] in chronological order
|
||||||
* allTrackpoints: [ [[lat,lng],...], ... ] — one sub-array per GPX file
|
* opts.autoconnect (bool, default true):
|
||||||
* thresholdKm: proximity radius (default 10)
|
* 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
|
* Returns array of segments [[lng, lat], ...] in MapLibre coordinate order.
|
||||||
* coordinate order. A segment with < 2 points is omitted.
|
* Segments with < 2 points are 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
|
|
||||||
*/
|
*/
|
||||||
/*
|
function buildJourneySegments(entries, opts) {
|
||||||
* 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;
|
|
||||||
var autoconnect = !opts || opts.autoconnect !== false;
|
var autoconnect = !opts || opts.autoconnect !== false;
|
||||||
var hasGpx = allTrackpoints && allTrackpoints.length > 0;
|
|
||||||
var segments = [];
|
var segments = [];
|
||||||
var current = [];
|
var current = [];
|
||||||
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
for (var i = 0; i < entries.length; i++) {
|
||||||
var e = entries[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) {
|
if (i === 0) { current.push(lngLat); continue; }
|
||||||
current.push(lngLat);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prev = entries[i - 1];
|
var connect = e.force_connect || autoconnect;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connect) {
|
if (connect) {
|
||||||
current.push(lngLat);
|
current.push(lngLat);
|
||||||
} else {
|
} else {
|
||||||
if (current.length >= 2) segments.push(current);
|
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
|
* Fetch GPX files and render their raw tracks, then draw journey connector
|
||||||
* only between entries not covered by any GPX file (connector suppression).
|
* 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
|
* entries: [{lat, lng, force_connect?}, ...] in chronological order
|
||||||
* gpxSourcePrefix: source/layer ID prefix for raw GPX tracks (e.g. 'gpx', 'home-gpx')
|
* 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')
|
* journeySourceId: base source ID for connector segments (e.g. 'journey', 'home-journey')
|
||||||
*
|
* opts: forwarded to buildJourneySegments (opts.autoconnect)
|
||||||
* When gpxUrls is empty, Promise.all resolves immediately → no GPX layers,
|
|
||||||
* buildJourneySegments draws a full connector line between all entries.
|
|
||||||
*/
|
*/
|
||||||
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
|
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
|
||||||
Promise.all(gpxUrls.map(function (url, idx) {
|
Promise.all(gpxUrls.map(function (url, idx) {
|
||||||
@@ -318,12 +224,10 @@
|
|||||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||||
paint: { 'line-color': ACCENT, 'line-width': 2, 'line-opacity': 0.7 }
|
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 []; });
|
.catch(function (err) { console.warn('GPX load failed:', url, err); });
|
||||||
})).then(function (allTrackpoints) {
|
})).then(function () {
|
||||||
var valid = allTrackpoints.filter(function (tp) { return tp.length > 0; });
|
var segments = buildJourneySegments(entries, opts);
|
||||||
var segments = buildJourneySegments(entries, valid, 10, opts);
|
|
||||||
addJourneySegments(map, segments, journeySourceId);
|
addJourneySegments(map, segments, journeySourceId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -334,7 +238,6 @@
|
|||||||
addJourneyLine: addJourneyLine,
|
addJourneyLine: addJourneyLine,
|
||||||
addJourneySegments: addJourneySegments,
|
addJourneySegments: addJourneySegments,
|
||||||
buildJourneySegments: buildJourneySegments,
|
buildJourneySegments: buildJourneySegments,
|
||||||
extractTrackpoints: extractTrackpoints,
|
|
||||||
renderGpxJourney: renderGpxJourney,
|
renderGpxJourney: renderGpxJourney,
|
||||||
createDotMarker: createDotMarker,
|
createDotMarker: createDotMarker,
|
||||||
createStoryMarker: createStoryMarker
|
createStoryMarker: createStoryMarker
|
||||||
|
|||||||
@@ -31,14 +31,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{# Collect GPX URLs from parent trip page for connector algorithm #}
|
|
||||||
{% set trip_page = page.parent() %}
|
{% 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 %}
|
{% if map_entries|length > 0 %}
|
||||||
<div class="feed-map-wrap">
|
<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">
|
<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/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 src="{{ url('theme://js/maplibre-utils.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
var FEED_ENTRIES = {{ map_entries|json_encode|raw }};
|
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 AUTOCONNECT = {{ trip_page.header.autoconnect ?? true ? 'true' : 'false' }};
|
||||||
|
|
||||||
var feedMap = new maplibregl.Map({
|
var feedMap = new maplibregl.Map({
|
||||||
@@ -89,23 +79,8 @@ feedMap.on('load', function () {
|
|||||||
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all((USE_GPX ? GPX_URLS : []).map(function (url) {
|
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { autoconnect: AUTOCONNECT });
|
||||||
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');
|
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user