@@ -303,7 +325,7 @@ let altitudeRows = [];
let altitudeExportRows = [];
function rankAltitudeNodes(nodes) {
return nodes
- .filter(node => Number.isFinite(node.last_altitude) && node.node_id != null
+ .filter(node => Number.isFinite(node.last_altitude) && node.last_altitude > 25 * 0.3048 && node.node_id != null
&& ["ROUTER", "ROUTER_CLIENT", "ROUTER_LATE"].includes((node.role || "").toUpperCase()))
.sort((a, b) => b.last_altitude - a.last_altitude || a.node_id - b.node_id);
}
@@ -319,12 +341,14 @@ async function loadAltitudeNodes(refreshNodes = false) {
const el = document.getElementById("chart_altitude");
const chart = echarts.getInstanceByDom(el) || initMeshChart(el);
refresh.disabled = true;
+ document.getElementById("altitude-map-button").disabled = true;
status.textContent = "Loading…";
chart.off("click");
chart.on("click", openAltitudeNode);
try {
altitudeExportRows = rankAltitudeNodes(await fetchStatsNodes(refreshNodes)).slice(0, 50);
altitudeRows = altitudeExportRows.slice(0, 10);
+ document.getElementById("altitude-map-button").disabled = altitudeExportRows.length === 0;
chart.setOption({
tooltip: {trigger:"axis", renderMode:"richText", valueFormatter:value => `${value} m`},
grid: {left:10, right:20, top:55, bottom:10, containLabel:true},
@@ -338,7 +362,7 @@ async function loadAltitudeNodes(refreshNodes = false) {
}, true);
status.textContent = altitudeRows.length
? `Showing ${altitudeRows.length} nodes · highest first`
- : "No routers have reported altitude yet.";
+ : "No routers have reported altitude above 25 ft (7.62 m).";
} catch (error) {
altitudeRows = [];
altitudeExportRows = [];
@@ -352,6 +376,73 @@ async function loadAltitudeNodes(refreshNodes = false) {
document.getElementById("altitude-refresh").addEventListener("click", () => loadAltitudeNodes(true));
loadAltitudeNodes();
+// Map the same ranked sample used by the top-50 CSV export.
+let altitudeMap;
+let altitudeMarkers;
+const altitudeMapModal = document.getElementById("altitude-map-modal");
+const altitudeMapButton = document.getElementById("altitude-map-button");
+const altitudeMapClose = document.getElementById("altitude-map-close");
+function closeAltitudeMap() {
+ altitudeMapModal.hidden = true;
+ altitudeMapButton.focus();
+}
+altitudeMapClose.addEventListener("click", closeAltitudeMap);
+altitudeMapModal.addEventListener("click", event => {
+ if (event.target === altitudeMapModal) closeAltitudeMap();
+});
+altitudeMapModal.addEventListener("keydown", event => {
+ if (event.key === "Escape") { event.preventDefault(); closeAltitudeMap(); }
+ if (event.key === "Tab") {
+ const controls = [...altitudeMapModal.querySelectorAll('button, a[href], [tabindex="0"]')]
+ .filter(el => el.getClientRects().length);
+ const first = controls[0], last = controls[controls.length - 1];
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
+ }
+});
+altitudeMapButton.addEventListener("click", () => {
+ altitudeMapModal.hidden = false;
+ altitudeMapClose.focus();
+ if (!altitudeMap) {
+ altitudeMap = L.map("altitude-router-map").setView([0,0], 2);
+ L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
+ attribution:'Map data: ©
OpenStreetMap contributors, SRTM | Map style: ©
OpenTopoMap (CC-BY-SA)',
+ maxZoom:17
+ }).addTo(altitudeMap);
+ altitudeMarkers = L.layerGroup().addTo(altitudeMap);
+ }
+ altitudeMarkers.clearLayers();
+ const located = altitudeExportRows.filter(node =>
+ Number.isFinite(node.last_lat) && Number.isFinite(node.last_long)
+ && Math.abs(node.last_lat) <= 900000000 && Math.abs(node.last_long) <= 1800000000
+ && (Math.abs(node.last_lat) > 10000000 || Math.abs(node.last_long) > 10000000));
+ const icon = L.divIcon({className:"altitude-router-icon", iconSize:[32,32], iconAnchor:[16,16],
+ html:'
'});
+ located.forEach(node => {
+ const marker = L.marker([node.last_lat / 1e7,node.last_long / 1e7], {icon}).addTo(altitudeMarkers);
+ const label = document.createElement("span");
+ label.textContent = `${node.short_name || node.long_name || node.id} · ${node.last_altitude.toLocaleString()} m`;
+ marker.bindTooltip(label, {permanent:true, direction:"top", offset:[0,-16], className:"altitude-router-label"});
+ const details = document.createElement("div");
+ const link = document.createElement("a");
+ link.href = `/node/${encodeURIComponent(node.node_id)}`;
+ link.textContent = node.long_name || node.short_name || node.id;
+ const altitude = document.createElement("p");
+ altitude.textContent = `${node.last_altitude.toLocaleString()} m above sea level · ${node.role}`;
+ details.append(link,altitude);
+ marker.bindPopup(details);
+ });
+ const missing = altitudeExportRows.length - located.length;
+ document.getElementById("altitude-map-status").textContent =
+ `Showing ${located.length} of the top ${altitudeExportRows.length} routers · altitude in meters above sea level` +
+ (missing ? ` · ${missing} without a valid location` : '');
+ requestAnimationFrame(() => {
+ altitudeMap.invalidateSize();
+ if (located.length) altitudeMap.fitBounds(located.map(node => [node.last_lat / 1e7,node.last_long / 1e7]), {padding:[60,60], maxZoom:12});
+ else altitudeMap.setView([0,0],2);
+ });
+});
+
// --- Fetch & Processing ---
async function fetchStats(period_type,length,portnum=null,channel=null){
try{