How many of each area\'s nodes have an actual reported GPS position vs. how many were only placeable via a neighbor-based estimate (same technique used for View Path\'s approximate markers). Click a column header to sort by it.
Failed to load area analytics: ' + esc(String(e)) + '
';
}
diff --git a/public/map.js b/public/map.js
index 5590fa2b..8434245b 100644
--- a/public/map.js
+++ b/public/map.js
@@ -679,19 +679,15 @@
} catch {}
return;
}
- // Check for an estimated-nodes view from the Areas tab (via
- // sessionStorage) — see drawEstimatedNodes. Checked before
- // map-route-hops since both use similar payload shapes but mean
- // different things (a resolved hop route vs. a flat point list).
- const estimatedNodesJson = sessionStorage.getItem('map-estimated-nodes');
- if (estimatedNodesJson) {
- sessionStorage.removeItem('map-estimated-nodes');
- try {
- const parsed = JSON.parse(estimatedNodesJson);
- if (parsed && Array.isArray(parsed.points)) {
- drawEstimatedNodes(parsed.points);
- }
- } catch {}
+ // Estimated-nodes deep link: #/map?estimatedNodes=1 -- the Areas
+ // tab's "View Estimated Nodes on Map" button links straight here
+ // instead of stashing state in sessionStorage (like the old
+ // map-estimated-nodes key did), so dborup can copy/share/bookmark
+ // the URL and it still works after a reload -- same shareable-URL
+ // convention as loadRouteFromDeepLink below (#1418/#1419).
+ const hashQs = (location.hash || '').split('?')[1];
+ if (new URLSearchParams(hashQs || '').get('estimatedNodes')) {
+ loadEstimatedNodesFromDeepLink();
return;
}
// Check for route from packet detail (via sessionStorage)
@@ -1097,7 +1093,15 @@
closeBtn.addTo(map);
const valid = (points || []).filter(function (p) { return p && p.lat != null && p.lon != null; });
- if (valid.length === 0) return;
+ if (valid.length === 0) {
+ const container = map.getContainer();
+ const label = document.createElement('div');
+ label.className = 'mc-estimated-nodes-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 = 'No nodes currently have an estimated position.';
+ container.appendChild(label);
+ return;
+ }
const color = '#f59e0b';
valid.forEach(function (p) {
@@ -1552,6 +1556,28 @@
}
}
+ // Deep link for #/map?estimatedNodes=1 -- fetches the current
+ // network-wide estimated-position list fresh from the same endpoint
+ // the Areas tab uses (/api/analytics/areas) and draws it, rather than
+ // relying on a sessionStorage snapshot from whenever the link was
+ // clicked. Since this list isn't per-user/per-request state (it's the
+ // deployment's current estimate), the URL alone is enough to
+ // reproduce the view -- share/bookmark/reload all just work.
+ async function loadEstimatedNodesFromDeepLink() {
+ try {
+ const resp = await fetch('/api/analytics/areas');
+ if (!resp.ok) {
+ console.warn('[deep-link] /api/analytics/areas returned ' + resp.status);
+ return;
+ }
+ const data = await resp.json();
+ const points = (data && Array.isArray(data.estimatedNodes)) ? data.estimatedNodes : [];
+ drawEstimatedNodes(points);
+ } catch (e) {
+ console.warn('[deep-link] estimated-nodes load failed', e);
+ }
+ }
+
async function loadNodes() {
try {
// Load regions from config + observed IATAs
diff --git a/test-analytics-areas-tab.js b/test-analytics-areas-tab.js
index 5acf1d9a..4b3c5679 100644
--- a/test-analytics-areas-tab.js
+++ b/test-analytics-areas-tab.js
@@ -331,19 +331,24 @@ function makeApiStub(resp) {
assert.ok(!el.innerHTML.includes('Show all'), 'no toggle should render with only 1 area in positionGaps');
});
- await testAsync('renders a "View Estimated Nodes on Map" button with the estimated-node count when estimatedNodes is non-empty', async () => {
+ await testAsync('renders a "View Estimated Nodes on Map" LINK (not a JS-click button) with the estimated-node count, pointing at a shareable/bookmarkable URL', async () => {
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse()));
const el = fakeEl();
await ctx.window._analyticsRenderAreasTab(el);
- assert.ok(el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the View Estimated Nodes button should render');
- assert.ok(el.innerHTML.includes('View Estimated Nodes on Map (1)'), 'the button label should show the estimated-node count');
+ assert.ok(el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the View Estimated Nodes link should render');
+ assert.ok(el.innerHTML.includes('View Estimated Nodes on Map (1)'), 'the link label should show the estimated-node count');
+ // Must be a real so right-click "Copy Link Address" works and
+ // the URL survives a reload -- dborup asked for this after the first
+ // version (sessionStorage + a plain button) only ever led to a bare
+ // "#/map" with no shareable state.
+ assert.ok(el.innerHTML.includes(' {
+ await testAsync('does not render the "View Estimated Nodes on Map" link when estimatedNodes is empty', async () => {
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse({ estimatedNodes: [] })));
const el = fakeEl();
await ctx.window._analyticsRenderAreasTab(el);
- assert.ok(!el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the button should not render when there are no estimated nodes to show');
+ assert.ok(!el.innerHTML.includes('id="areasViewEstimatedNodes"'), 'the link should not render when there are no estimated nodes to show');
});
await testAsync('renders the unpositioned-nodes summary note including the no-neighbor-fix subset', async () => {