fix: map labels show short hash ID (e.g. 5B, BEEF), better deconfliction with spiral offsets

This commit is contained in:
you
2026-03-21 00:30:25 +00:00
parent 8a6923c3b3
commit e38d1fa8f8
3 changed files with 65 additions and 29 deletions
+1 -1
View File
@@ -83,7 +83,7 @@
<script src="app.js?v=1774052279"></script>
<script src="home.js?v=1774042199"></script>
<script src="packets.js?v=1774052279"></script>
<script src="map.js?v=1774052755" onerror="console.error('Failed to load:', this.src)"></script>
<script src="map.js?v=1774053025" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774050030" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774050030" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774048777" onerror="console.error('Failed to load:', this.src)"></script>
+35 -24
View File
@@ -62,9 +62,12 @@
function makeRepeaterLabelIcon(node) {
var s = ROLE_STYLE['repeater'] || ROLE_STYLE.companion;
var label = node.hash_size ? node.hash_size + 'B' : '?';
var bgColor = node.hash_size ? s.color : '#999';
var html = '<div style="background:' + bgColor + ';color:#fff;font-weight:bold;font-size:12px;padding:3px 7px;border-radius:4px;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,0.4);text-align:center;line-height:1;">' + label + '</div>';
var hs = node.hash_size || 1;
// Show the short mesh hash ID (first N bytes of pubkey, uppercased)
var shortHash = node.public_key ? node.public_key.slice(0, hs * 2).toUpperCase() : '??';
var bgColor = node.hash_size ? s.color : '#888';
var html = '<div style="background:' + bgColor + ';color:#fff;font-weight:bold;font-size:11px;padding:2px 5px;border-radius:3px;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,0.4);text-align:center;line-height:1.2;white-space:nowrap;">' +
shortHash + '</div>';
return L.divIcon({
html: html,
className: 'meshcore-marker meshcore-label-marker',
@@ -374,31 +377,39 @@
var _renderingMarkers = false;
var _lastDeconflictZoom = null;
function deconflictLabels(labelMarkers, map) {
function deconflictLabels(labelMarkers, mapRef) {
const placed = [];
const LABEL_W = 32;
const LABEL_H = 22;
const PAD = 4;
const LABEL_W = 38;
const LABEL_H = 24;
const PAD = 6;
const overlaps = (b) => placed.some(p =>
b.x < p.x + p.w + PAD && b.x + b.w + PAD > p.x &&
b.y < p.y + p.h + PAD && b.y + b.h + PAD > p.y
);
const overlaps = function(b) {
return placed.some(function(p) {
return b.x < p.x + p.w + PAD && b.x + b.w + PAD > p.x &&
b.y < p.y + p.h + PAD && b.y + b.h + PAD > p.y;
});
};
for (const m of labelMarkers) {
const pt = map.latLngToLayerPoint(m.latLng);
let bestPt = pt;
let box = { x: pt.x - LABEL_W / 2, y: pt.y - LABEL_H / 2, w: LABEL_W, h: LABEL_H };
// Generate spiral offsets — 5 rings, 8 directions each, up to 120px out
var offsets = [];
for (var ring = 1; ring <= 6; ring++) {
var dist = ring * 28;
for (var angle = 0; angle < 360; angle += 45) {
var rad = angle * Math.PI / 180;
offsets.push([Math.round(Math.cos(rad) * dist), Math.round(Math.sin(rad) * dist)]);
}
}
for (var i = 0; i < labelMarkers.length; i++) {
var m = labelMarkers[i];
var pt = mapRef.latLngToLayerPoint(m.latLng);
var bestPt = pt;
var box = { x: pt.x - LABEL_W / 2, y: pt.y - LABEL_H / 2, w: LABEL_W, h: LABEL_H };
if (overlaps(box)) {
const offsets = [
[0, -25], [0, 25], [-30, 0], [30, 0],
[-25, -20], [25, -20], [-25, 20], [25, 20],
[0, -45], [0, 45], [-50, 0], [50, 0]
];
for (const [dx, dy] of offsets) {
const tryPt = L.point(pt.x + dx, pt.y + dy);
const tryBox = { x: tryPt.x - LABEL_W / 2, y: tryPt.y - LABEL_H / 2, w: LABEL_W, h: LABEL_H };
for (var j = 0; j < offsets.length; j++) {
var tryPt = L.point(pt.x + offsets[j][0], pt.y + offsets[j][1]);
var tryBox = { x: tryPt.x - LABEL_W / 2, y: tryPt.y - LABEL_H / 2, w: LABEL_W, h: LABEL_H };
if (!overlaps(tryBox)) {
bestPt = tryPt;
box = tryBox;
@@ -408,7 +419,7 @@
}
placed.push(box);
m.adjustedLatLng = map.layerPointToLatLng(bestPt);
m.adjustedLatLng = mapRef.layerPointToLatLng(bestPt);
m.offset = Math.sqrt(Math.pow(bestPt.x - pt.x, 2) + Math.pow(bestPt.y - pt.y, 2));
}
}
+29 -4
View File
@@ -973,20 +973,45 @@ app.get('/api/nodes', (req, res) => {
counts[r + 's'] = db.db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE role = ?`).get(r).count;
}
// Compute hash_size for each node from latest ADVERT packets
// Compute hash_size for each node from ADVERT path byte or path hop lengths
const hashSizeMap = new Map();
// Pass 1: from ADVERT packets (most authoritative — path byte bits 7-6)
for (const p of pktStore.packets) {
if (p.payload_type === 4 && p.decoded_json) {
if (p.payload_type === 4 && p.raw_hex) {
try {
const d = JSON.parse(p.decoded_json);
const d = JSON.parse(p.decoded_json || '{}');
const pk = d.pubKey || d.public_key;
if (pk && p.raw_hex && !hashSizeMap.has(pk)) {
if (pk && !hashSizeMap.has(pk)) {
const pathByte = parseInt(p.raw_hex.slice(2, 4), 16);
hashSizeMap.set(pk, ((pathByte >> 6) & 0x3) + 1);
}
} catch {}
}
}
// Pass 2: for nodes without ADVERTs, derive from path hop lengths in any packet
// Path hops are hex strings; their length / 2 = hash_size in bytes
for (const p of pktStore.packets) {
if (p.path_json) {
try {
const hops = JSON.parse(p.path_json);
if (hops.length > 0) {
const hopLen = hops[0].length / 2; // hex chars / 2 = bytes
if (hopLen >= 1 && hopLen <= 4) {
// The path byte tells us hash_size for ALL nodes in this packet's mesh
const pathByte = p.raw_hex ? parseInt(p.raw_hex.slice(2, 4), 16) : -1;
const hs = pathByte >= 0 ? ((pathByte >> 6) & 0x3) + 1 : hopLen;
// We can't map hops to pubkeys directly (hops are truncated hashes)
// but we know the packet's source node uses this hash_size
if (p.decoded_json) {
const d = JSON.parse(p.decoded_json);
const pk = d.pubKey || d.public_key;
if (pk && !hashSizeMap.has(pk)) hashSizeMap.set(pk, hs);
}
}
}
} catch {}
}
}
for (const node of nodes) {
node.hash_size = hashSizeMap.get(node.public_key) || null;
}