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:
@@ -68,16 +68,15 @@ form:
|
|||||||
type: bool
|
type: bool
|
||||||
|
|
||||||
header.autoconnect:
|
header.autoconnect:
|
||||||
type: toggle
|
type: select
|
||||||
label: Connect markers
|
label: Connect markers
|
||||||
help: 'Draw connector lines between all location markers in chronological order'
|
help: 'Controls connector lines between location markers'
|
||||||
highlight: 1
|
default: 'on'
|
||||||
default: 1
|
|
||||||
options:
|
options:
|
||||||
1: 'Yes'
|
'on': 'On — always connect all'
|
||||||
0: 'No'
|
'manual': 'Manual — force connect only'
|
||||||
validate:
|
'intelligent_gpx': 'Intelligent GPX — suppress where route is covered'
|
||||||
type: bool
|
'off': 'Off — no connections'
|
||||||
|
|
||||||
publishing:
|
publishing:
|
||||||
type: tab
|
type: tab
|
||||||
|
|||||||
@@ -152,21 +152,65 @@
|
|||||||
return el;
|
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.
|
* Build journey line segments from entries.
|
||||||
*
|
*
|
||||||
* entries: [{lat, lng, force_connect?}, ...] in chronological order
|
* entries: [{lat, lng, force_connect?}, ...] in chronological order
|
||||||
* opts.autoconnect (bool, default true):
|
* opts.connectMode: 'on' | 'off' | 'manual' | 'intelligent_gpx' (default: 'on')
|
||||||
* true → connect every consecutive pair (simple chronological line)
|
* 'on' → connect every consecutive pair (force_connect irrelevant)
|
||||||
* false → connect only entries where force_connect:true
|
* '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.
|
* Returns array of segments [[lng, lat], ...] in MapLibre coordinate order.
|
||||||
* Segments with < 2 points are omitted.
|
* Segments with < 2 points are omitted.
|
||||||
*/
|
*/
|
||||||
function buildJourneySegments(entries, opts) {
|
function buildJourneySegments(entries, opts, trackpointsPerFile) {
|
||||||
var autoconnect = !opts || opts.autoconnect !== false;
|
var mode = (opts && opts.connectMode) || 'on';
|
||||||
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];
|
||||||
@@ -174,7 +218,30 @@
|
|||||||
|
|
||||||
if (i === 0) { current.push(lngLat); continue; }
|
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) {
|
if (connect) {
|
||||||
current.push(lngLat);
|
current.push(lngLat);
|
||||||
@@ -202,15 +269,16 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Fetch GPX files and render their raw tracks, then draw journey connector
|
* 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)
|
* 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)
|
* opts: forwarded to buildJourneySegments (opts.connectMode)
|
||||||
*/
|
*/
|
||||||
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
|
function renderGpxJourney(map, gpxUrls, entries, gpxSourcePrefix, journeySourceId, opts) {
|
||||||
|
var needsTrackpoints = opts && opts.connectMode === 'intelligent_gpx';
|
||||||
Promise.all(gpxUrls.map(function (url, idx) {
|
Promise.all(gpxUrls.map(function (url, idx) {
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then(function (r) { return r.text(); })
|
.then(function (r) { return r.text(); })
|
||||||
@@ -224,10 +292,12 @@
|
|||||||
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 needsTrackpoints ? extractTrackpoints(geojson) : [];
|
||||||
})
|
})
|
||||||
.catch(function (err) { console.warn('GPX load failed:', url, err); });
|
.catch(function (err) { console.warn('GPX load failed:', url, err); return []; });
|
||||||
})).then(function () {
|
})).then(function (allTrackpoints) {
|
||||||
var segments = buildJourneySegments(entries, opts);
|
var valid = needsTrackpoints ? allTrackpoints.filter(function (tp) { return tp.length > 0; }) : [];
|
||||||
|
var segments = buildJourneySegments(entries, opts, valid);
|
||||||
addJourneySegments(map, segments, journeySourceId);
|
addJourneySegments(map, segments, journeySourceId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
<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 AUTOCONNECT = {{ trip_page.header.autoconnect ?? true ? 'true' : 'false' }};
|
{% set _ac = trip_page.header.autoconnect ?? 'on' %}
|
||||||
|
var AUTOCONNECT = "{{ _ac == 'intelligent_gpx' ? 'on' : _ac }}";
|
||||||
|
|
||||||
var feedMap = new maplibregl.Map({
|
var feedMap = new maplibregl.Map({
|
||||||
container: 'feed-map',
|
container: 'feed-map',
|
||||||
@@ -79,7 +80,7 @@ feedMap.on('load', function () {
|
|||||||
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
feedMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
||||||
}
|
}
|
||||||
|
|
||||||
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { autoconnect: AUTOCONNECT });
|
var segments = MapUtils.buildJourneySegments(FEED_ENTRIES, { connectMode: AUTOCONNECT });
|
||||||
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
MapUtils.addJourneySegments(feedMap, segments, 'feed-journey');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
|
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
|
||||||
var HOME_GPX_URLS = {{ home_gpx_urls|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 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({
|
var homeMap = new maplibregl.Map({
|
||||||
container: 'home-map',
|
container: 'home-map',
|
||||||
@@ -194,7 +194,7 @@ homeMap.on('load', function () {
|
|||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
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 });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
var ENTRIES = {{ map_entries|json_encode|raw }};
|
var ENTRIES = {{ map_entries|json_encode|raw }};
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
||||||
var USE_GPX = {{ trip_page.header.use_gpx ?? true ? 'true' : 'false' }};
|
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({
|
var map = new maplibregl.Map({
|
||||||
container: 'trip-map',
|
container: 'trip-map',
|
||||||
@@ -94,7 +94,7 @@ map.on('load', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
/* ── 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 });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@
|
|||||||
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
|
var TRIP_ENTRIES = {{ map_entries|json_encode|raw }};
|
||||||
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
var GPX_URLS = {{ gpx_urls|json_encode|raw }};
|
||||||
var USE_GPX = {{ page.header.use_gpx ?? true ? 'true' : 'false' }};
|
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({
|
var tripMap = new maplibregl.Map({
|
||||||
container: 'trip-map',
|
container: 'trip-map',
|
||||||
@@ -351,7 +351,7 @@ tripMap.on('load', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
/* ── 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);
|
setTimeout(function () { tripMap.resize(); }, 100);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user