mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-28 07:42:47 +00:00
## Summary Pure refactor extracting three pure helpers out of the `public/route-view.js` IIFE into a sibling `public/route-view-utils.js`, per the triage fix path on #1424. - `escapeHtml` - `buildPacketContextBlock` - `buildSnrSparkline` All three are exposed via `window.MC_ROUTE_UTILS`, and the IIFE in `route-view.js` unpacks the namespace into locals at the top so every existing call site stays textually unchanged. `spiderFanFor` was deliberately **not** extracted: it consumes Leaflet types (`mapRef.latLngToLayerPoint`, `mk.getLatLng` / `setLatLng`, `L.point`) and mutates marker state. A one-line comment was added at its definition explaining the reason (matches the dijkstra caveat from the triage comment). ## Changes - `public/route-view-utils.js` — new file, 151 LoC. Single IIFE exporting `window.MC_ROUTE_UTILS = { escapeHtml, buildPacketContextBlock, buildSnrSparkline }`. Body is byte-equivalent to the originals. - `public/route-view.js` — three function definitions removed, replaced with an 8-line namespace unpack stanza. `spiderFanFor` keeps a NOT-extracted comment. Net: `-126/+12`, file now 1473 LoC (was 1588). - `public/index.html` — adds `<script src="route-view-utils.js?v=__BUST__">` immediately before the existing `route-view.js` script tag. Repo-wide grep confirmed `index.html` is the only HTML loader for `route-view.js`. ## TDD exemption justification Pure refactor: no test files modified; existing CI suite green without test edits. Test files diff vs `origin/master`: **none**. Local full-suite (`sh test-all.sh`) is identical between this branch and `origin/master@9b36b7c4` — same single pre-existing `channels.js sidebar links to #/analytics` failure on both, **zero new regressions** introduced by this PR. Route-view-specific guards all green: ``` test-issue-1418-polish-review.js passed: 22 failed: 0 test-issue-1418-spider-fan.js passed: 25 failed: 0 test-issue-1418-edge-weights.js passed: 18 failed: 0 test-issue-1418-cb-preset-ramp.js passed: 19 failed: 0 test-issue-1418-raw-hex-extraction.js passed: 39 failed: 0 test-issue-1418-deeplink-hops-channels.js passed: 27 failed: 0 ``` ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → **clean** (all gates and warnings pass). ## Out of scope - No bundler / build step (no-build is a project constraint, per triage) - DOM-touching helpers stay inside the IIFE (they rely on closure state) - `spiderFanFor` stays (Leaflet types — not pure) Closes #1424 Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
This commit is contained in:
@@ -154,6 +154,7 @@
|
||||
<script src="geo-filter-overlay.js?v=__BUST__"></script>
|
||||
<script src="map.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="route-render.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="route-view-utils.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="route-view.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=__BUST__" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="table-sort.js?v=__BUST__"></script>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/* route-view-utils.js — pure helpers extracted from route-view.js (#1424).
|
||||
*
|
||||
* Pure refactor: zero behavior change. Loaded BEFORE route-view.js in any
|
||||
* HTML that needs the route view. The IIFE in route-view.js unpacks
|
||||
* window.MC_ROUTE_UTILS at the top.
|
||||
*
|
||||
* Helpers exported:
|
||||
* escapeHtml(s)
|
||||
* buildPacketContextBlock(pktCtx)
|
||||
* buildSnrSparkline(snrTrend)
|
||||
*
|
||||
* `spiderFanFor` is NOT extracted — it consumes Leaflet LatLng / map types
|
||||
* (mapRef.latLngToLayerPoint, mk.getLatLng, mk.setLatLng, L.point) and is
|
||||
* not pure. Left in place inside route-view.js (see comment there).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];
|
||||
});
|
||||
}
|
||||
|
||||
// packet-context block (one stable layout, type chip + 3-5
|
||||
// facts). pktCtx shape:
|
||||
// { type: 'ADVERT'|'TXT_MSG'|'GRP_TXT'|'TRACE'|other,
|
||||
// decoded: <parsed JSON of decoded_json>,
|
||||
// payloadType: <byte>,
|
||||
// srcResolvedName, destResolvedName, observedHops, observationCount }
|
||||
function buildPacketContextBlock(pktCtx) {
|
||||
if (!pktCtx || !pktCtx.type) return '';
|
||||
var t = pktCtx.type;
|
||||
var d = pktCtx.decoded || {};
|
||||
var glyph, label, factsHtml = '';
|
||||
switch (t) {
|
||||
case 'ADVERT':
|
||||
glyph = '📡'; label = 'ADVERT';
|
||||
var name = d.adName || d.name || (d.pubKey ? d.pubKey.slice(0, 8) : '?');
|
||||
var role = (d.flags && (d.flags.repeater ? 'repeater' : d.flags.room ? 'room' : d.flags.sensor ? 'sensor' : d.flags.chat ? 'companion' : 'unknown')) || 'unknown';
|
||||
// no fabricated fields. Battery isn't decoded into the advert
|
||||
// JSON — adverts carry lat/lon + name + flags, not battery. If a
|
||||
// future advert version exposes it, re-add then.
|
||||
var sig = (d.signatureValid === true) ? '✓' : (d.signatureValid === false ? '✗' : null);
|
||||
var line1 = '<b>' + escapeHtml(name) + '</b> · ' + escapeHtml(role);
|
||||
if (sig) line1 += ' · sig ' + sig;
|
||||
// Self-reported GPS if present
|
||||
if (d.lat != null && d.lon != null) {
|
||||
line1 += ' · ' + d.lat.toFixed(3) + ', ' + d.lon.toFixed(3);
|
||||
}
|
||||
var pkPrefix = d.pubKey ? d.pubKey.slice(0, 12) + '…' : '';
|
||||
factsHtml = '<div class="mc-rt-ctx-line">' + line1 + '</div>';
|
||||
if (pkPrefix) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-mono">' + escapeHtml(pkPrefix) + '</div>';
|
||||
break;
|
||||
case 'PATH':
|
||||
glyph = '🔀'; label = 'PATH';
|
||||
var psrc = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : '?');
|
||||
var pdst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(psrc) + '</b> <span class="mc-rt-ctx-arrow">→</span> <b>' + escapeHtml(pdst) + '</b></div>';
|
||||
break;
|
||||
case 'TXT_MSG':
|
||||
case 'REQ':
|
||||
case 'RESPONSE':
|
||||
case 'ANON_REQ':
|
||||
var typeGlyphs = { 'TXT_MSG': '✉', 'REQ': '🔒', 'RESPONSE': '🔓', 'ANON_REQ': '🔒' };
|
||||
var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'REQUEST', 'RESPONSE': 'RESPONSE', 'ANON_REQ': 'ANON REQ' };
|
||||
glyph = typeGlyphs[t] || '·';
|
||||
label = typeLabels[t] || t;
|
||||
var src = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : (t === 'ANON_REQ' ? 'anon' : '?'));
|
||||
var dst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(src) + '</b> <span class="mc-rt-ctx-arrow">→</span> <b>' + escapeHtml(dst) + '</b></div>';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">🔒 encrypted</div>';
|
||||
break;
|
||||
case 'GRP_TXT':
|
||||
case 'CHAN':
|
||||
glyph = '#'; label = 'CHANNEL MSG';
|
||||
var chName = pktCtx.channelName || d.channel || (d.channelHashHex ? 'channel 0x' + d.channelHashHex : 'channel ?');
|
||||
var contentText = pktCtx.decryptedText || d.text || d.plainText || null;
|
||||
var encStatus = contentText ? '🔓 decrypted' : (d.decryptionStatus === 'decrypted' ? '🔓 decrypted' : '🔒 no key');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(chName) + '</b></div>';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">' + encStatus + '</div>';
|
||||
if (contentText) {
|
||||
var preview = contentText.slice(0, 80);
|
||||
if (contentText.length > 80) preview += '…';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-quote">"' + escapeHtml(preview) + '"</div>';
|
||||
}
|
||||
var senderName = pktCtx.srcResolvedName || d.sender || (d.srcHash ? 'sender 0x' + d.srcHash : null);
|
||||
if (senderName) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">from <b>' + escapeHtml(senderName) + '</b></div>';
|
||||
break;
|
||||
case 'TRACE':
|
||||
glyph = '⌖'; label = 'TRACE';
|
||||
var officialHops = (d.routeTaken && d.routeTaken.length) || (d.route && d.route.length) || null;
|
||||
var observed = (pktCtx.observedHops != null) ? pktCtx.observedHops : null;
|
||||
if (officialHops != null && observed != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line">Official: <b>' + officialHops + '</b> hops · Observed: <b>' + observed + '</b></div>';
|
||||
} else if (officialHops != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line">Official route: <b>' + officialHops + '</b> hops</div>';
|
||||
}
|
||||
if (pktCtx.issuedBy) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">issued by <b>' + escapeHtml(pktCtx.issuedBy) + '</b></div>';
|
||||
break;
|
||||
default:
|
||||
glyph = '·'; label = (t || 'OTHER').toUpperCase();
|
||||
if (pktCtx.payloadSize != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line mc-rt-ctx-meta">' + pktCtx.payloadSize + ' bytes</div>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
return '<div class="mc-rt-ctx" data-type="' + escapeHtml(t) + '">' +
|
||||
'<div class="mc-rt-ctx-chip"><span class="mc-rt-ctx-glyph">' + glyph + '</span> ' + escapeHtml(label) + '</div>' +
|
||||
'<div class="mc-rt-ctx-facts">' + factsHtml + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildSnrSparkline(snrTrend) {
|
||||
if (!snrTrend || !snrTrend.length) return '<span class="mc-rt-detail-na">no SNR data</span>';
|
||||
var pts = snrTrend.filter(function (p) { return p && p.snr != null; });
|
||||
if (!pts.length) return '<span class="mc-rt-detail-na">no SNR data</span>';
|
||||
var W = 200, H = 28;
|
||||
var snrs = pts.map(function (p) { return p.snr; });
|
||||
var minS = Math.min.apply(null, snrs), maxS = Math.max.apply(null, snrs);
|
||||
if (maxS === minS) { minS -= 1; maxS += 1; }
|
||||
// n<3 is not a sparkline — a 2-point polyline implies a
|
||||
// trend across time it can't represent. Show DOTS only (no connecting line)
|
||||
// when there are fewer than 3 observations.
|
||||
var showLine = pts.length >= 3;
|
||||
var poly = pts.map(function (p, i) {
|
||||
var x = (i / (pts.length - 1 || 1)) * W;
|
||||
var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
|
||||
return x.toFixed(1) + ',' + y.toFixed(1);
|
||||
}).join(' ');
|
||||
var svg = '<svg class="mc-rt-detail-spark" width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" aria-label="SNR across route observations">';
|
||||
if (showLine) {
|
||||
svg += '<polyline points="' + poly + '" fill="none" stroke="currentColor" stroke-width="1.2"/>';
|
||||
}
|
||||
// Dots always (data points themselves are the truth)
|
||||
pts.forEach(function (p, i) {
|
||||
var x = (i / (pts.length - 1 || 1)) * W;
|
||||
var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
|
||||
svg += '<circle cx="' + x.toFixed(1) + '" cy="' + y.toFixed(1) + '" r="2" fill="currentColor"/>';
|
||||
});
|
||||
svg += '</svg>';
|
||||
return svg +
|
||||
'<span class="mc-rt-detail-spark-meta">' + pts.length + ' obs · ' + minS.toFixed(1) + '..' + maxS.toFixed(1) + ' dB</span>';
|
||||
}
|
||||
|
||||
window.MC_ROUTE_UTILS = {
|
||||
escapeHtml: escapeHtml,
|
||||
buildPacketContextBlock: buildPacketContextBlock,
|
||||
buildSnrSparkline: buildSnrSparkline
|
||||
};
|
||||
})();
|
||||
+11
-126
@@ -72,132 +72,14 @@
|
||||
return Math.round(d/86400000) + 'd ago';
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];
|
||||
});
|
||||
}
|
||||
|
||||
// packet-context block (one stable layout, type chip + 3-5
|
||||
// facts). pktCtx shape:
|
||||
// { type: 'ADVERT'|'TXT_MSG'|'GRP_TXT'|'TRACE'|other,
|
||||
// decoded: <parsed JSON of decoded_json>,
|
||||
// payloadType: <byte>,
|
||||
// srcResolvedName, destResolvedName, observedHops, observationCount }
|
||||
function buildPacketContextBlock(pktCtx) {
|
||||
if (!pktCtx || !pktCtx.type) return '';
|
||||
var t = pktCtx.type;
|
||||
var d = pktCtx.decoded || {};
|
||||
var glyph, label, factsHtml = '';
|
||||
switch (t) {
|
||||
case 'ADVERT':
|
||||
glyph = '📡'; label = 'ADVERT';
|
||||
var name = d.adName || d.name || (d.pubKey ? d.pubKey.slice(0, 8) : '?');
|
||||
var role = (d.flags && (d.flags.repeater ? 'repeater' : d.flags.room ? 'room' : d.flags.sensor ? 'sensor' : d.flags.chat ? 'companion' : 'unknown')) || 'unknown';
|
||||
// no fabricated fields. Battery isn't decoded into the advert
|
||||
// JSON — adverts carry lat/lon + name + flags, not battery. If a
|
||||
// future advert version exposes it, re-add then.
|
||||
var sig = (d.signatureValid === true) ? '✓' : (d.signatureValid === false ? '✗' : null);
|
||||
var line1 = '<b>' + escapeHtml(name) + '</b> · ' + escapeHtml(role);
|
||||
if (sig) line1 += ' · sig ' + sig;
|
||||
// Self-reported GPS if present
|
||||
if (d.lat != null && d.lon != null) {
|
||||
line1 += ' · ' + d.lat.toFixed(3) + ', ' + d.lon.toFixed(3);
|
||||
}
|
||||
var pkPrefix = d.pubKey ? d.pubKey.slice(0, 12) + '…' : '';
|
||||
factsHtml = '<div class="mc-rt-ctx-line">' + line1 + '</div>';
|
||||
if (pkPrefix) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-mono">' + escapeHtml(pkPrefix) + '</div>';
|
||||
break;
|
||||
case 'PATH':
|
||||
glyph = '🔀'; label = 'PATH';
|
||||
var psrc = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : '?');
|
||||
var pdst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(psrc) + '</b> <span class="mc-rt-ctx-arrow">→</span> <b>' + escapeHtml(pdst) + '</b></div>';
|
||||
break;
|
||||
case 'TXT_MSG':
|
||||
case 'REQ':
|
||||
case 'RESPONSE':
|
||||
case 'ANON_REQ':
|
||||
var typeGlyphs = { 'TXT_MSG': '✉', 'REQ': '🔒', 'RESPONSE': '🔓', 'ANON_REQ': '🔒' };
|
||||
var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'REQUEST', 'RESPONSE': 'RESPONSE', 'ANON_REQ': 'ANON REQ' };
|
||||
glyph = typeGlyphs[t] || '·';
|
||||
label = typeLabels[t] || t;
|
||||
var src = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : (t === 'ANON_REQ' ? 'anon' : '?'));
|
||||
var dst = pktCtx.destResolvedName || (d.destHash ? 'unknown (hash ' + d.destHash + ')' : '?');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(src) + '</b> <span class="mc-rt-ctx-arrow">→</span> <b>' + escapeHtml(dst) + '</b></div>';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">🔒 encrypted</div>';
|
||||
break;
|
||||
case 'GRP_TXT':
|
||||
case 'CHAN':
|
||||
glyph = '#'; label = 'CHANNEL MSG';
|
||||
var chName = pktCtx.channelName || d.channel || (d.channelHashHex ? 'channel 0x' + d.channelHashHex : 'channel ?');
|
||||
var contentText = pktCtx.decryptedText || d.text || d.plainText || null;
|
||||
var encStatus = contentText ? '🔓 decrypted' : (d.decryptionStatus === 'decrypted' ? '🔓 decrypted' : '🔒 no key');
|
||||
factsHtml = '<div class="mc-rt-ctx-line"><b>' + escapeHtml(chName) + '</b></div>';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">' + encStatus + '</div>';
|
||||
if (contentText) {
|
||||
var preview = contentText.slice(0, 80);
|
||||
if (contentText.length > 80) preview += '…';
|
||||
factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-quote">"' + escapeHtml(preview) + '"</div>';
|
||||
}
|
||||
var senderName = pktCtx.srcResolvedName || d.sender || (d.srcHash ? 'sender 0x' + d.srcHash : null);
|
||||
if (senderName) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">from <b>' + escapeHtml(senderName) + '</b></div>';
|
||||
break;
|
||||
case 'TRACE':
|
||||
glyph = '⌖'; label = 'TRACE';
|
||||
var officialHops = (d.routeTaken && d.routeTaken.length) || (d.route && d.route.length) || null;
|
||||
var observed = (pktCtx.observedHops != null) ? pktCtx.observedHops : null;
|
||||
if (officialHops != null && observed != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line">Official: <b>' + officialHops + '</b> hops · Observed: <b>' + observed + '</b></div>';
|
||||
} else if (officialHops != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line">Official route: <b>' + officialHops + '</b> hops</div>';
|
||||
}
|
||||
if (pktCtx.issuedBy) factsHtml += '<div class="mc-rt-ctx-line mc-rt-ctx-meta">issued by <b>' + escapeHtml(pktCtx.issuedBy) + '</b></div>';
|
||||
break;
|
||||
default:
|
||||
glyph = '·'; label = (t || 'OTHER').toUpperCase();
|
||||
if (pktCtx.payloadSize != null) {
|
||||
factsHtml = '<div class="mc-rt-ctx-line mc-rt-ctx-meta">' + pktCtx.payloadSize + ' bytes</div>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
return '<div class="mc-rt-ctx" data-type="' + escapeHtml(t) + '">' +
|
||||
'<div class="mc-rt-ctx-chip"><span class="mc-rt-ctx-glyph">' + glyph + '</span> ' + escapeHtml(label) + '</div>' +
|
||||
'<div class="mc-rt-ctx-facts">' + factsHtml + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildSnrSparkline(snrTrend) {
|
||||
if (!snrTrend || !snrTrend.length) return '<span class="mc-rt-detail-na">no SNR data</span>';
|
||||
var pts = snrTrend.filter(function (p) { return p && p.snr != null; });
|
||||
if (!pts.length) return '<span class="mc-rt-detail-na">no SNR data</span>';
|
||||
var W = 200, H = 28;
|
||||
var snrs = pts.map(function (p) { return p.snr; });
|
||||
var minS = Math.min.apply(null, snrs), maxS = Math.max.apply(null, snrs);
|
||||
if (maxS === minS) { minS -= 1; maxS += 1; }
|
||||
// n<3 is not a sparkline — a 2-point polyline implies a
|
||||
// trend across time it can't represent. Show DOTS only (no connecting line)
|
||||
// when there are fewer than 3 observations.
|
||||
var showLine = pts.length >= 3;
|
||||
var poly = pts.map(function (p, i) {
|
||||
var x = (i / (pts.length - 1 || 1)) * W;
|
||||
var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
|
||||
return x.toFixed(1) + ',' + y.toFixed(1);
|
||||
}).join(' ');
|
||||
var svg = '<svg class="mc-rt-detail-spark" width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" aria-label="SNR across route observations">';
|
||||
if (showLine) {
|
||||
svg += '<polyline points="' + poly + '" fill="none" stroke="currentColor" stroke-width="1.2"/>';
|
||||
}
|
||||
// Dots always (data points themselves are the truth)
|
||||
pts.forEach(function (p, i) {
|
||||
var x = (i / (pts.length - 1 || 1)) * W;
|
||||
var y = H - 2 - ((p.snr - minS) / (maxS - minS)) * (H - 4);
|
||||
svg += '<circle cx="' + x.toFixed(1) + '" cy="' + y.toFixed(1) + '" r="2" fill="currentColor"/>';
|
||||
});
|
||||
svg += '</svg>';
|
||||
return svg +
|
||||
'<span class="mc-rt-detail-spark-meta">' + pts.length + ' obs · ' + minS.toFixed(1) + '..' + maxS.toFixed(1) + ' dB</span>';
|
||||
}
|
||||
// #1424 — pure helpers (escapeHtml, buildPacketContextBlock,
|
||||
// buildSnrSparkline) extracted into public/route-view-utils.js. Loader
|
||||
// for that file MUST run before route-view.js (see index.html). Local
|
||||
// refs keep the call sites in this file unchanged.
|
||||
var _MC_RT_U = window.MC_ROUTE_UTILS || {};
|
||||
var escapeHtml = _MC_RT_U.escapeHtml;
|
||||
var buildPacketContextBlock = _MC_RT_U.buildPacketContextBlock;
|
||||
var buildSnrSparkline = _MC_RT_U.buildSnrSparkline;
|
||||
|
||||
// Polish review (carmack #1423): bound _detailCache (was unbounded plain
|
||||
// object; every distinct pubkey ever clicked was retained for the tab's
|
||||
@@ -1369,6 +1251,9 @@
|
||||
// Spider-fan: after Leaflet projects, group any markers within
|
||||
// 25px of each other and offset them on an arc around their centroid.
|
||||
// Draw a hairline from each offset marker back to the centroid.
|
||||
// #1424: NOT extracted into route-view-utils.js — this consumes Leaflet
|
||||
// types (mapRef.latLngToLayerPoint, mk.getLatLng / setLatLng, L.point)
|
||||
// and mutates marker objects, so it isn't pure.
|
||||
function spiderFanFor(markerArray, positionArray) {
|
||||
if (!mapRef || !mapRef.latLngToLayerPoint) return;
|
||||
var pts = markerArray.map(function (mk, i) {
|
||||
|
||||
Reference in New Issue
Block a user