mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 00:47:52 +00:00
feat: compare a GPS-sharing sender's real position against path[0]
For messages where a sender shared their real position AND have a
resolvable entry-point repeater, pairs them up: haversine distance
shown inline in the message table ("2.3 km from DK-XXX"), plus a new
"Compare shared position vs. entry point" drill-down button.
On the map (drawGPSTrail's new 'compare' kind), draws both trails —
real position (blue/red/green) and entry-point repeater (orange) —
with a thin dashed line connecting each matched pair, so you can see
at a glance how good the entry-point proxy is when ground truth is
available.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
23b650337f
commit
9d77d2ea96
+60
-6
@@ -5707,12 +5707,30 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
.filter(Boolean)
|
||||
.reverse(); // chronological — messages come back most-recent-first
|
||||
|
||||
// Comparison: for messages where the sender ALSO shared a real
|
||||
// position, pair it with that same message's resolved entry-point
|
||||
// repeater — lets us see how far the nearest-repeater proxy was
|
||||
// from where the sender actually said they were.
|
||||
var comparisonHits = messages
|
||||
.filter(function(m) { return m.lat != null && m.lon != null && m.pathPrefixes && m.pathPrefixes.length > 0; })
|
||||
.map(function(m) {
|
||||
var r = resolved[m.pathPrefixes[0]];
|
||||
if (!r || r.confidence !== 'unique_prefix' || !r.pubkey) return null;
|
||||
return { transmissionId: m.transmissionId, pubkey: r.pubkey, name: r.name, timestamp: m.timestamp, gpsLat: m.lat, gpsLon: m.lon };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reverse();
|
||||
|
||||
var entryTrailPoints = [];
|
||||
if (entryHits.length >= 2) {
|
||||
var comparisonGpsPoints = [];
|
||||
var comparisonEntryPoints = [];
|
||||
var distanceByTxId = {};
|
||||
if (entryHits.length >= 2 || comparisonHits.length >= 1) {
|
||||
try {
|
||||
var nodesResp = await fetchAllNodes('', { ttl: CLIENT_TTL.nodeList });
|
||||
var nodesByKey = {};
|
||||
(nodesResp.nodes || []).forEach(function(n) { nodesByKey[n.public_key] = n; });
|
||||
|
||||
var lastKey = null;
|
||||
entryHits.forEach(function(hit) {
|
||||
if (hit.pubkey === lastKey) return; // collapse consecutive hits on the same repeater
|
||||
@@ -5722,7 +5740,17 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
lastKey = hit.pubkey;
|
||||
}
|
||||
});
|
||||
} catch (e) { /* leave entryTrailPoints empty — button just won't show */ }
|
||||
|
||||
comparisonHits.forEach(function(hit) {
|
||||
var node = nodesByKey[hit.pubkey];
|
||||
if (!node || node.lat == null || node.lon == null) return;
|
||||
var km = (window.HopResolver && typeof window.HopResolver.haversineKm === 'function')
|
||||
? window.HopResolver.haversineKm(hit.gpsLat, hit.gpsLon, node.lat, node.lon) : null;
|
||||
distanceByTxId[hit.transmissionId] = { km: km, name: hit.name };
|
||||
comparisonGpsPoints.push({ lat: hit.gpsLat, lon: hit.gpsLon, timestamp: hit.timestamp, label: km != null ? (km.toFixed(1) + ' km from ' + hit.name) : 'Shared position' });
|
||||
comparisonEntryPoints.push({ lat: node.lat, lon: node.lon, timestamp: hit.timestamp, label: hit.name });
|
||||
});
|
||||
} catch (e) { /* leave arrays empty — buttons just won't show */ }
|
||||
}
|
||||
|
||||
var rows = messages.map(function(m) {
|
||||
@@ -5734,7 +5762,14 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
return esc(o.observerName) + ' (' + o.snr.toFixed(1) + 'dB / ' + o.rssi.toFixed(0) + 'dBm)';
|
||||
}).join(', ')
|
||||
: '<span class="text-muted">—</span>';
|
||||
var posStr = (m.lat != null && m.lon != null) ? mapLinkHtml(m.lat, m.lon) : '<span class="text-muted">—</span>';
|
||||
var posStr = '<span class="text-muted">—</span>';
|
||||
if (m.lat != null && m.lon != null) {
|
||||
posStr = mapLinkHtml(m.lat, m.lon);
|
||||
var dist = distanceByTxId[m.transmissionId];
|
||||
if (dist && dist.km != null) {
|
||||
posStr += ' <span class="text-muted" style="font-size:0.9em">(' + dist.km.toFixed(1) + ' km from ' + esc(dist.name) + ')</span>';
|
||||
}
|
||||
}
|
||||
return '<tr><td>' + (typeof timeAgo === 'function' ? timeAgo(m.timestamp) : m.timestamp) + '</td>' +
|
||||
'<td>' + pathStr + '</td>' +
|
||||
'<td>' + obsStr + '</td>' +
|
||||
@@ -5751,10 +5786,19 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
var entryBtnHtml = entryTrailPoints.length >= 2
|
||||
? '<button type="button" data-wd-view-entry-path style="' + btnStyle + '">View approximate path via entry points (' + entryTrailPoints.length + ' repeaters)</button>'
|
||||
: '';
|
||||
var btnRow = (pathBtnHtml || entryBtnHtml) ? '<div>' + pathBtnHtml + entryBtnHtml + '</div>' : '';
|
||||
return { html: btnRow + tableHtml, gpsPoints: gpsPoints, entryTrailPoints: entryTrailPoints };
|
||||
var compareBtnHtml = comparisonGpsPoints.length >= 1
|
||||
? '<button type="button" data-wd-view-compare style="' + btnStyle + '">Compare shared position vs. entry point (' + comparisonGpsPoints.length + ')</button>'
|
||||
: '';
|
||||
var btnRow = (pathBtnHtml || entryBtnHtml || compareBtnHtml) ? '<div>' + pathBtnHtml + entryBtnHtml + compareBtnHtml + '</div>' : '';
|
||||
return {
|
||||
html: btnRow + tableHtml, gpsPoints: gpsPoints, entryTrailPoints: entryTrailPoints,
|
||||
comparisonGpsPoints: comparisonGpsPoints, comparisonEntryPoints: comparisonEntryPoints
|
||||
};
|
||||
} catch (err) {
|
||||
return { html: '<p style="color:var(--status-red);font-size:0.85em">Failed to load messages: ' + esc(String(err)) + '</p>', gpsPoints: [], entryTrailPoints: [] };
|
||||
return {
|
||||
html: '<p style="color:var(--status-red);font-size:0.85em">Failed to load messages: ' + esc(String(err)) + '</p>',
|
||||
gpsPoints: [], entryTrailPoints: [], comparisonGpsPoints: [], comparisonEntryPoints: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5803,6 +5847,16 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
window.location.hash = '#/map';
|
||||
});
|
||||
}
|
||||
var compareBtn = cell.querySelector('[data-wd-view-compare]');
|
||||
if (compareBtn) {
|
||||
compareBtn.addEventListener('click', function() {
|
||||
sessionStorage.setItem('map-gps-trail', JSON.stringify({
|
||||
points: result.comparisonGpsPoints, comparisonPoints: result.comparisonEntryPoints,
|
||||
sender: senderName, kind: 'compare'
|
||||
}));
|
||||
window.location.hash = '#/map';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+42
-10
@@ -605,7 +605,7 @@
|
||||
try {
|
||||
const parsed = JSON.parse(gpsTrailJson);
|
||||
if (parsed && Array.isArray(parsed.points)) {
|
||||
drawGPSTrail(parsed.points, { sender: parsed.sender, kind: parsed.kind });
|
||||
drawGPSTrail(parsed.points, { sender: parsed.sender, kind: parsed.kind, comparisonPoints: parsed.comparisonPoints });
|
||||
}
|
||||
} catch {}
|
||||
return;
|
||||
@@ -873,13 +873,19 @@
|
||||
|
||||
// Wardriving trail — draws a polyline through a sequence of points in
|
||||
// chronological order (see "View path on map" / "View approximate path
|
||||
// via entry points" in the Wardriving analytics tab). Two flavors, both
|
||||
// pre-resolved to plain lat/lon by the caller so no node resolution is
|
||||
// needed here:
|
||||
// via entry points" / "Compare shared position vs. entry point" in the
|
||||
// Wardriving analytics tab). All pre-resolved to plain lat/lon by the
|
||||
// caller so no node resolution is needed here:
|
||||
// - opts.kind 'gps' (default): a sender's own literal shared positions.
|
||||
// - opts.kind 'entry-point': for senders who don't share GPS — each
|
||||
// point is the KNOWN position of the entry-point repeater that first
|
||||
// relayed one of their messages, not the sender's real position.
|
||||
// - opts.kind 'compare': `points` is the sender's real shared positions
|
||||
// for messages that ALSO have a resolvable entry-point repeater;
|
||||
// opts.comparisonPoints (same length, same order) is that repeater's
|
||||
// position for each corresponding message — drawn as a second trail
|
||||
// with a thin connecting line per pair, to visualize how far the
|
||||
// nearest-repeater proxy was from the sender's real position.
|
||||
function drawGPSTrail(points, opts) {
|
||||
opts = opts || {};
|
||||
if (markerLayer) map.removeLayer(markerLayer);
|
||||
@@ -906,6 +912,7 @@
|
||||
|
||||
const valid = (points || []).filter(function (p) { return p && p.lat != null && p.lon != null; });
|
||||
if (valid.length === 0) return;
|
||||
const compValid = (opts.comparisonPoints || []).filter(function (p) { return p && p.lat != null && p.lon != null; });
|
||||
|
||||
const coords = valid.map(function (p) { return [p.lat, p.lon]; });
|
||||
if (coords.length >= 2) {
|
||||
@@ -922,10 +929,31 @@
|
||||
marker.bindPopup(label);
|
||||
});
|
||||
|
||||
if (coords.length >= 2) {
|
||||
map.fitBounds(L.latLngBounds(coords).pad(0.2));
|
||||
// Comparison overlay: the entry-point repeater for each of the SAME
|
||||
// messages, plus a thin dashed line per pair showing the offset.
|
||||
const compCoords = compValid.map(function (p) { return [p.lat, p.lon]; });
|
||||
if (compCoords.length >= 2) {
|
||||
L.polyline(compCoords, { color: '#f97316', weight: 2, opacity: 0.7, dashArray: '4 4' }).addTo(routeLayer);
|
||||
}
|
||||
compValid.forEach(function (p, i) {
|
||||
const marker = L.circleMarker([p.lat, p.lon], {
|
||||
radius: 5, color: '#f97316', fillColor: '#f97316', fillOpacity: 0.75, weight: 1
|
||||
}).addTo(routeLayer);
|
||||
const label = safeEsc(p.label || 'Entry point ' + (i + 1)) +
|
||||
(p.timestamp ? ' — ' + safeEsc(new Date(p.timestamp).toLocaleString()) : '');
|
||||
marker.bindPopup(label);
|
||||
if (valid[i]) {
|
||||
L.polyline([[valid[i].lat, valid[i].lon], [p.lat, p.lon]], {
|
||||
color: '#94a3b8', weight: 1, opacity: 0.6, dashArray: '2 5'
|
||||
}).addTo(routeLayer);
|
||||
}
|
||||
});
|
||||
|
||||
const allCoords = coords.concat(compCoords);
|
||||
if (allCoords.length >= 2) {
|
||||
map.fitBounds(L.latLngBounds(allCoords).pad(0.2));
|
||||
} else {
|
||||
map.setView(coords[0], 15);
|
||||
map.setView(allCoords[0], 15);
|
||||
}
|
||||
|
||||
if (opts.sender) {
|
||||
@@ -933,9 +961,13 @@
|
||||
const label = document.createElement('div');
|
||||
label.className = 'mc-gps-trail-label';
|
||||
label.style.cssText = 'position:absolute;top:10px;left:50px;z-index:1000;background:var(--input-bg,#1e293b);color:var(--text,#e2e8f0);padding:4px 10px;border-radius:4px;font-size:12px';
|
||||
label.textContent = opts.kind === 'entry-point'
|
||||
? opts.sender + ' — approximate path via ' + valid.length + ' entry-point repeater' + (valid.length === 1 ? '' : 's') + ' (not their real position)'
|
||||
: opts.sender + ' — ' + valid.length + ' shared position' + (valid.length === 1 ? '' : 's');
|
||||
if (opts.kind === 'entry-point') {
|
||||
label.textContent = opts.sender + ' — approximate path via ' + valid.length + ' entry-point repeater' + (valid.length === 1 ? '' : 's') + ' (not their real position)';
|
||||
} else if (opts.kind === 'compare') {
|
||||
label.textContent = opts.sender + ' — shared position (blue/red/green) vs. entry-point repeater (orange), ' + valid.length + ' message' + (valid.length === 1 ? '' : 's');
|
||||
} else {
|
||||
label.textContent = opts.sender + ' — ' + valid.length + ' shared position' + (valid.length === 1 ? '' : 's');
|
||||
}
|
||||
container.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user