feat: add GPX proximity algorithm to MapUtils (buildJourneySegments, extractTrackpoints)
Adds haversineKm, extractTrackpoints, isNearTrack, buildJourneySegments, and addJourneySegments to the shared MapLibre GL IIFE. Updates MapUtils export to expose the new functions. ES5-only; no arrow functions, const/let, or modules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
This commit is contained in:
@@ -135,5 +135,141 @@
|
||||
return el;
|
||||
}
|
||||
|
||||
global.MapUtils = { MAP_STYLE: MAP_STYLE, ACCENT: ACCENT, addJourneyLine: addJourneyLine, createDotMarker: createDotMarker };
|
||||
/* ── 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;
|
||||
}
|
||||
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
|
||||
* allTrackpoints: [ [[lat,lng],...], ... ] — one sub-array per GPX file
|
||||
* thresholdKm: proximity radius (default 10)
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
function buildJourneySegments(entries, allTrackpoints, thresholdKm) {
|
||||
thresholdKm = thresholdKm || 10;
|
||||
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]
|
||||
|
||||
if (i === 0) {
|
||||
current.push(lngLat);
|
||||
continue;
|
||||
}
|
||||
|
||||
var prev = entries[i - 1];
|
||||
var connect;
|
||||
|
||||
if (!hasGpx || e.force_connect) {
|
||||
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) {
|
||||
current.push(lngLat);
|
||||
} else {
|
||||
if (current.length >= 2) segments.push(current);
|
||||
current = [lngLat]; // start new segment from this point
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length >= 2) segments.push(current);
|
||||
return segments;
|
||||
}
|
||||
|
||||
/*
|
||||
* Draw journey segments — calls addJourneyLine once per segment.
|
||||
* baseSourceId: e.g. 'journey' → sources become 'journey-0', 'journey-1', ...
|
||||
* (single segment gets plain 'journey' for backwards compatibility).
|
||||
*/
|
||||
function addJourneySegments(map, segments, baseSourceId) {
|
||||
segments.forEach(function (coords, i) {
|
||||
var sid = segments.length === 1 ? baseSourceId : baseSourceId + '-' + i;
|
||||
addJourneyLine(map, coords, sid);
|
||||
});
|
||||
}
|
||||
|
||||
global.MapUtils = {
|
||||
MAP_STYLE: MAP_STYLE,
|
||||
ACCENT: ACCENT,
|
||||
addJourneyLine: addJourneyLine,
|
||||
addJourneySegments: addJourneySegments,
|
||||
buildJourneySegments: buildJourneySegments,
|
||||
extractTrackpoints: extractTrackpoints,
|
||||
createDotMarker: createDotMarker
|
||||
};
|
||||
})(window);
|
||||
|
||||
Reference in New Issue
Block a user