feat: expand connect markers to 4-mode select
Replaces the boolean toggle with a select field offering:
on — connect all consecutive entries (chronological line)
manual — force_connect entries only (user-controlled connections)
intelligent_gpx — suppress connectors where GPX covers both endpoints;
force_connect overrides (original smart logic, restored)
off — no connectors at all; force_connect also ignored
buildJourneySegments gains an optional trackpointsPerFile param used
only by intelligent_gpx mode. renderGpxJourney extracts trackpoints
only when connectMode is intelligent_gpx. dailies.html.twig falls
back from intelligent_gpx → on (mini-map has no GPX tracks to
suppress against).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user