|
|
@@ -39,6 +39,7 @@
|
|
|
var detailNet = null; // null/auto, 'SURVE' or 'OFFICE' inside a workshop
|
|
|
var focusGroupLabel = null; // ?g= target, zoomed after the full chart is built
|
|
|
var pathParent = {}; // child -> parent chain for hover highlighting
|
|
|
+ var lastRoleMap = {}; // switch id -> computed display role
|
|
|
var lockedPath = null; // switch id whose path is locked by click
|
|
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
|
@@ -453,11 +454,12 @@
|
|
|
var CORE_X = 130, MAIN_X = 430, DIST_X = 790, ACCESS_X = 1300;
|
|
|
var CORE_W = 200, MAIN_W = 300, NODE_W3 = 300;
|
|
|
var WORK_LEFT = 610, WORK_RIGHT = 1530;
|
|
|
- var FLOOR_LEFT = 1130, FLOOR_RIGHT = 1525;
|
|
|
+ var FLOOR_LEFT = 1130;
|
|
|
var CHART_W = 1560;
|
|
|
+ var ACCESS_STEP = 380, DIST_STEP = 86;
|
|
|
var SEC_HEAD = 46, CORE_ROW = 92, WORK_TOP = 148;
|
|
|
- var ROW_H = 24, ROW_GAP = 8, WS_GAP = 20, SEC_GAP = 40;
|
|
|
- var FLOOR_GAP = 18;
|
|
|
+ var ROW_H = 34, ROW_GAP = 14, WS_GAP = 36, SEC_GAP = 52;
|
|
|
+ var FLOOR_GAP = 26;
|
|
|
|
|
|
function makeNetworks() {
|
|
|
var raw = CFG.networks;
|
|
|
@@ -584,16 +586,23 @@
|
|
|
// config). An explicitly empty list means the administrator wants no
|
|
|
// configured core column.
|
|
|
if (!(CFG.core_rules instanceof Array) && (/^core(\s|-)/i.test(String(n.name || '')) || groupOf(n).label === 'Core')) return 'core';
|
|
|
- // STP topology is the truth for Dist vs Access: a switch with downstream
|
|
|
- // children is a distribution point even when its hostname says Access.
|
|
|
- // This fixes the 3301/4504 cases where the old snapshot tier was a root
|
|
|
- // tier but the device is really an aggregation switch.
|
|
|
+ // Hostname role is the display truth for Access switches. An access
|
|
|
+ // switch may itself have access children (cascade/stack); it still stays
|
|
|
+ // an access node and the children simply take a further-right column.
|
|
|
+ // A Dist switch stays a distribution point even when it has only one child.
|
|
|
+ if (r === 'access') return 'access';
|
|
|
+ if (r === 'dist') {
|
|
|
+ // Old snapshots sometimes call a leaf "Dist"; if it has no children and
|
|
|
+ // the collector already marked it as access, keep it an access leaf.
|
|
|
+ if (tier === 'access' && childCount <= 0) return 'access';
|
|
|
+ return 'dist';
|
|
|
+ }
|
|
|
+ if (r === 'core') return childCount > 0 ? 'dist' : 'main';
|
|
|
if (tier === 'core') {
|
|
|
- if (childCount > 0 || r === 'dist') return 'dist';
|
|
|
+ if (childCount > 0) return 'dist';
|
|
|
return 'main';
|
|
|
}
|
|
|
if (childCount > 0) return 'dist';
|
|
|
- if (r === 'dist' && tier !== 'access') return 'dist';
|
|
|
if (tier === 'distribution') return 'dist';
|
|
|
return 'access';
|
|
|
}
|
|
|
@@ -629,15 +638,37 @@
|
|
|
(snap.edges || []).forEach(function (e) {
|
|
|
if (adj[e.source] && adj[e.target]) { adj[e.source].push(e.target); adj[e.target].push(e.source); }
|
|
|
});
|
|
|
+ function depthOf(n) {
|
|
|
+ var d = parseInt(n && n.depth, 10);
|
|
|
+ return (isFinite(d) && d >= 0 && d < 99) ? d : 999;
|
|
|
+ }
|
|
|
+ // Walk upstream to the highest non-core node. This is the real workshop
|
|
|
+ // anchor. It keeps cascaded access (2.59 -> 2.75) in the 4401 frame while
|
|
|
+ // still allowing an access switch that is itself an anchor (4504 -> .118/
|
|
|
+ // 6109 access) to own one frame. No protocol edge is changed here.
|
|
|
+ function rootOf(n) {
|
|
|
+ var cur = n, guard = 0;
|
|
|
+ while (cur && guard++ < 80) {
|
|
|
+ var cd = depthOf(cur), best = null;
|
|
|
+ (adj[cur.id] || []).forEach(function (id) {
|
|
|
+ var up = byId[id];
|
|
|
+ if (!up || up.id === cur.id) return;
|
|
|
+ var ud = depthOf(up);
|
|
|
+ if (ud < cd) {
|
|
|
+ if (!best || ud < depthOf(best)) best = up;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (!best) return cur;
|
|
|
+ if (roleOf(best) === 'core') return cur;
|
|
|
+ cur = best;
|
|
|
+ }
|
|
|
+ return cur || n;
|
|
|
+ }
|
|
|
(snap.nodes || []).forEach(function (n) {
|
|
|
var r = roleOf(n);
|
|
|
if (r === 'core' || r === 'main') return;
|
|
|
- if (r === 'dist') { keys[n.id] = groupOf(n).label; return; }
|
|
|
- var ds = (adj[n.id] || []).map(function (id) { return byId[id]; })
|
|
|
- .filter(function (d) { return d && roleOf(d) === 'dist'; });
|
|
|
- var same = ds.filter(function (d) { return groupOf(d).label === groupOf(n).label; });
|
|
|
- var pick = same[0] || ds[0];
|
|
|
- keys[n.id] = pick ? groupOf(pick).label : groupOf(n).label;
|
|
|
+ var root = rootOf(n);
|
|
|
+ keys[n.id] = groupOf(root || n).label;
|
|
|
});
|
|
|
return keys;
|
|
|
}
|
|
|
@@ -664,48 +695,119 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- function layoutWorkshop(w, top) {
|
|
|
+ function layoutWorkshop(w, top, snap) {
|
|
|
var rowH = ROW_H + ROW_GAP + (showTerm ? 24 : 0);
|
|
|
- var floorMap = {}, floors = [];
|
|
|
+ var floorMap = {}, floors = [], accessFloor = {};
|
|
|
w.access.forEach(function (n) {
|
|
|
var fi = floorInfo(n);
|
|
|
var k = fi.key;
|
|
|
if (!floorMap[k]) { floorMap[k] = {key: k, label: fi.label, rank: (fi.key === 'NA' ? 999 : fi.rank), nodes: [], rows: []}; floors.push(floorMap[k]); }
|
|
|
floorMap[k].nodes.push(n);
|
|
|
+ accessFloor[n.id] = k;
|
|
|
});
|
|
|
floors.sort(function (a, b) { return a.rank - b.rank || String(a.label).localeCompare(String(b.label)); });
|
|
|
- var accessH = 0;
|
|
|
+
|
|
|
+ // Build the access sub-columns from the real graph: direct children of a
|
|
|
+ // dist are column 0, their access children column 1, and so on. No
|
|
|
+ // logical parent/child is replaced by this; it only decides x positions.
|
|
|
+ var byId = {}, adj = {}, member = {};
|
|
|
+ w.all.forEach(function (n) { byId[n.id] = n; adj[n.id] = []; member[n.id] = true; });
|
|
|
+ (snap.edges || []).forEach(function (e) {
|
|
|
+ if (member[e.source] && member[e.target]) { adj[e.source].push(e.target); adj[e.target].push(e.source); }
|
|
|
+ });
|
|
|
+ function numDepth(n) {
|
|
|
+ var d = parseInt(n && n.depth, 10);
|
|
|
+ return (isFinite(d) && d >= 0 && d < 99) ? d : 99;
|
|
|
+ }
|
|
|
+ var starts = w.dists.map(function (n) { return n.id; });
|
|
|
+ if (!starts.length) {
|
|
|
+ // A frame with no Dist (e.g. 4504 when its hostname says Access) still
|
|
|
+ // has a top access node. Find nodes with no upstream member.
|
|
|
+ w.access.forEach(function (n) {
|
|
|
+ var up = false;
|
|
|
+ (adj[n.id] || []).forEach(function (id) {
|
|
|
+ var other = byId[id];
|
|
|
+ if (other && numDepth(other) < numDepth(n)) up = true;
|
|
|
+ });
|
|
|
+ if (!up) starts.push(n.id);
|
|
|
+ });
|
|
|
+ if (!starts.length && w.access.length) starts.push(w.access[0].id);
|
|
|
+ }
|
|
|
+ var level = {}, queue = [];
|
|
|
+ starts.forEach(function (id) { if (byId[id]) { level[id] = 0; queue.push(id); } });
|
|
|
+ while (queue.length) {
|
|
|
+ var cur = queue.shift();
|
|
|
+ (adj[cur] || []).forEach(function (id) {
|
|
|
+ if (!byId[id] || level[id] != null) return;
|
|
|
+ level[id] = level[cur] + 1;
|
|
|
+ queue.push(id);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ var base = w.dists.length ? 1 : 0;
|
|
|
+ w.access.forEach(function (n) {
|
|
|
+ var lv = level[n.id];
|
|
|
+ n._col = (lv == null) ? 0 : Math.max(0, lv - base);
|
|
|
+ if (n._col > 12) n._col = 12;
|
|
|
+ });
|
|
|
+
|
|
|
+ var maxCol = 0;
|
|
|
floors.forEach(function (f) {
|
|
|
- f.nodes.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)); });
|
|
|
- f.h = 24 + f.nodes.length * rowH + 8;
|
|
|
- accessH += f.h;
|
|
|
+ // A child fed from another floor (e.g. 1FA-27 2.59 -> GRD 2.75) is put
|
|
|
+ // at the top of its own floor frame. Its straight line then stays out
|
|
|
+ // of the labels of the floor's normal first column.
|
|
|
+ function parentFloorKey(n) {
|
|
|
+ var best = null, bestD = 999;
|
|
|
+ (adj[n.id] || []).forEach(function (id) {
|
|
|
+ var up = byId[id];
|
|
|
+ if (!up || !accessFloor[id]) return;
|
|
|
+ var ud = numDepth(up);
|
|
|
+ if (ud < numDepth(n) && ud < bestD) { best = up; bestD = ud; }
|
|
|
+ });
|
|
|
+ return best ? accessFloor[best.id] : f.key;
|
|
|
+ }
|
|
|
+ f.nodes.sort(function (a, b) {
|
|
|
+ var ca = (parentFloorKey(a) === f.key ? 1 : 0) - (parentFloorKey(b) === f.key ? 1 : 0);
|
|
|
+ if (ca) return ca;
|
|
|
+ var cc = (a._col || 0) - (b._col || 0);
|
|
|
+ if (cc) return cc;
|
|
|
+ return String(a.name).localeCompare(String(b.name));
|
|
|
+ });
|
|
|
+ f.nodes.forEach(function (n) { maxCol = Math.max(maxCol, n._col || 0); f.maxCol = Math.max(f.maxCol || 0, n._col || 0); });
|
|
|
+ f.crossFloor = f.nodes.some(function (n) { return parentFloorKey(n) !== f.key; });
|
|
|
+ f.h = 28 + f.nodes.length * rowH + 10;
|
|
|
});
|
|
|
+ var accessH = 0;
|
|
|
+ floors.forEach(function (f) { accessH += f.h; });
|
|
|
if (floors.length > 1) accessH += (floors.length - 1) * FLOOR_GAP;
|
|
|
- var distH = w.dists.length ? (w.dists.length * 64 + (w.dists.length - 1) * 8) : 0;
|
|
|
+ var distH = w.dists.length ? (w.dists.length * DIST_STEP + (w.dists.length - 1) * 10) : 0;
|
|
|
var inner = Math.max(accessH, distH, 54);
|
|
|
- var h = 38 + inner + 10;
|
|
|
+ var h = 40 + inner + 12;
|
|
|
w.top = top; w.h = h; w.rowH = rowH;
|
|
|
+ w.right = Math.max(WORK_RIGHT, ACCESS_X + maxCol * ACCESS_STEP + NODE_W3 / 2 + 26);
|
|
|
+
|
|
|
// Floors are stacked bottom-up: GRD lowest, then 1F → 3F → 4F → 7F.
|
|
|
- var cursor = top + h - 8;
|
|
|
+ var cursor = top + h - 10;
|
|
|
floors.forEach(function (f) {
|
|
|
cursor -= f.h;
|
|
|
- f.frame = {x: FLOOR_LEFT, y: cursor, w: FLOOR_RIGHT - FLOOR_LEFT, h: f.h};
|
|
|
+ var floorW = Math.max(340, ACCESS_X + (f.maxCol || 0) * ACCESS_STEP + NODE_W3 / 2 + 26 - FLOOR_LEFT);
|
|
|
+ f.frame = {x: FLOOR_LEFT, y: cursor, w: floorW, h: f.h};
|
|
|
+ if (f.crossFloor) f.labelX = FLOOR_LEFT + 72;
|
|
|
f.nodes.forEach(function (n, i) {
|
|
|
- f.rows.push({id: n.id, x: ACCESS_X, y: cursor + 22 + rowH / 2 + i * rowH});
|
|
|
+ f.rows.push({id: n.id, x: ACCESS_X + (n._col || 0) * ACCESS_STEP, y: cursor + 28 + rowH / 2 + i * rowH});
|
|
|
});
|
|
|
cursor -= FLOOR_GAP;
|
|
|
});
|
|
|
- var dstart = top + 38 + (inner - distH) / 2;
|
|
|
+ var dstart = top + 40 + (inner - distH) / 2;
|
|
|
w.distPos = w.dists.map(function (n, i) {
|
|
|
- return {id: n.id, x: DIST_X, y: dstart + 32 + i * 72};
|
|
|
+ return {id: n.id, x: DIST_X, y: dstart + DIST_STEP / 2 + i * (DIST_STEP + 10)};
|
|
|
});
|
|
|
w.floors = floors;
|
|
|
w.title = w.label + ' · ' + netLabel(w.net) + ' · ' + w.all.length + ' ' + t('l2.unit_sw', '台');
|
|
|
// A workshop without a distribution switch only needs a box around its
|
|
|
- // access column; keeping the full dist-column width would leave a large
|
|
|
+ // access columns; keeping the full dist-column width would leave a large
|
|
|
// empty rectangle and make the chart look sparse.
|
|
|
var frameLeft = w.dists.length ? WORK_LEFT : FLOOR_LEFT - 40;
|
|
|
- w.frame = {x: frameLeft, y: top, w: WORK_RIGHT - frameLeft, h: h};
|
|
|
+ w.frame = {x: frameLeft, y: top, w: w.right - frameLeft, h: h};
|
|
|
return h;
|
|
|
}
|
|
|
|
|
|
@@ -714,7 +816,7 @@
|
|
|
var netMap = assignNetworks(snap);
|
|
|
var sections = [], placements = [], frames = [], roleMap = {};
|
|
|
var wsKeys = workshopKeys(snap);
|
|
|
- var y = 100;
|
|
|
+ var y = 100, maxRight = CHART_W;
|
|
|
NETWORKS.forEach(function (net) {
|
|
|
var secNodes = nodes.filter(function (n) { return netMap[n.id] === net.key; });
|
|
|
if (!secNodes.length) return;
|
|
|
@@ -732,13 +834,14 @@
|
|
|
};
|
|
|
var cur = y + WORK_TOP;
|
|
|
ws.forEach(function (w) {
|
|
|
- layoutWorkshop(w, cur);
|
|
|
+ layoutWorkshop(w, cur, snap);
|
|
|
+ maxRight = Math.max(maxRight, w.right || WORK_RIGHT);
|
|
|
cur += w.h + WS_GAP;
|
|
|
frames.push({id: 'ws_' + net.key + '_' + w.key, type: 'wsframe', x: w.frame.x, y: w.frame.y, w: w.frame.w, h: w.frame.h,
|
|
|
label: w.title, group: w.key, net: net.key, color: net.color});
|
|
|
w.floors.forEach(function (f, fi) {
|
|
|
frames.push({id: 'ff_' + net.key + '_' + w.key + '_' + fi, type: 'floorframe', x: f.frame.x, y: f.frame.y,
|
|
|
- w: f.frame.w, h: f.frame.h, label: f.label + ' · ' + f.nodes.length + ' ' + t('l2.unit_sw', '台'), net: net.key, color: net.color});
|
|
|
+ w: f.frame.w, h: f.frame.h, label: f.label + ' · ' + f.nodes.length + ' ' + t('l2.unit_sw', '台'), labelX: f.labelX, net: net.key, color: net.color});
|
|
|
});
|
|
|
w.distPos.forEach(function (p) {
|
|
|
var n = nodes.filter(function (x) { return x.id === p.id; })[0];
|
|
|
@@ -768,7 +871,9 @@
|
|
|
sections.push(sec);
|
|
|
y += sec.h + SEC_GAP;
|
|
|
});
|
|
|
- var root = {x: 0, y: 0, w: CHART_W, h: y + 6};
|
|
|
+ var chartW = Math.max(CHART_W, maxRight + 40);
|
|
|
+ sections.forEach(function (sec) { sec.w = chartW - 20; });
|
|
|
+ var root = {x: 0, y: 0, w: chartW, h: y + 6};
|
|
|
return {sections: sections, placements: placements, frames: frames, netMap: netMap, roleMap: roleMap, root: root};
|
|
|
}
|
|
|
|
|
|
@@ -978,13 +1083,15 @@
|
|
|
'width': 0.7, 'line-color': d ? '#3b4a5e' : '#c5cfdb', 'opacity': 0.55,
|
|
|
'curve-style': 'bezier', 'z-index-compare': 'manual', 'z-index': 11
|
|
|
}},
|
|
|
- {selector: 'node.alarm', style: {'border-width': 6}},
|
|
|
+ // Latency is a secondary label signal; alarm and offline must win over
|
|
|
+ // latency so an alarmed/offline switch is never shown as merely orange.
|
|
|
+ {selector: 'node.lat-warn', style: {'border-color': '#F39C12', 'border-width': 4, 'border-style': 'solid'}},
|
|
|
+ {selector: 'node.lat-bad', style: {'border-color': '#E74C3C', 'border-width': 6}},
|
|
|
+ {selector: 'node.alarm', style: {'border-color': '#E45959', 'border-width': 6}},
|
|
|
{selector: 'node.offline', style: {'border-color': '#C0392B', 'border-width': 6, 'border-style': 'dashed', 'opacity': 0.9}},
|
|
|
{selector: 'node.dim', style: {'opacity': 0.12}},
|
|
|
{selector: 'edge.dim', style: {'opacity': 0.08}},
|
|
|
{selector: 'edge.hl', style: {'width': 5, 'line-color': '#FFD54A', 'opacity': 1, 'z-index-compare': 'manual', 'z-index': 45}},
|
|
|
- {selector: 'node.lat-warn', style: {'border-color': '#F39C12'}},
|
|
|
- {selector: 'node.lat-bad', style: {'border-color': '#E74C3C', 'border-width': 6}},
|
|
|
{selector: 'node.hl', style: {'border-color': '#ff0', 'border-width': 7, 'overlay-color': '#ff0', 'overlay-opacity': 0.25, 'overlay-padding': 6}},
|
|
|
{selector: 'node.pathhl', style: {'border-width': 5, 'border-color': '#FFD54A', 'overlay-color': '#FFD54A', 'overlay-opacity': 0.2, 'overlay-padding': 4}},
|
|
|
{selector: 'edge.pathhl', style: {'width': 5.5, 'line-color': '#FFD54A', 'opacity': 1}},
|
|
|
@@ -1068,6 +1175,7 @@
|
|
|
function renderFull(snap) {
|
|
|
var L = computeFull(snap), els = [];
|
|
|
if (!L.placements.length) { applyElements([], false); return; }
|
|
|
+ lastRoleMap = L.roleMap || {};
|
|
|
var byId = {};
|
|
|
L.placements.forEach(function (p) { byId[p.node.id] = p; });
|
|
|
|
|
|
@@ -1089,7 +1197,7 @@
|
|
|
var tt = f.type === 'floorframe' ? 'floorlabel' : 'wslabel';
|
|
|
var ty = f.type === 'floorframe' ? 11 : 18;
|
|
|
els.push({group: 'nodes', data: {id: 'label_' + f.id, type: tt, label: f.label, w: f.w, h: 16},
|
|
|
- position: {x: f.x + f.w / 2, y: f.y + ty}, selectable: false, grabbable: false});
|
|
|
+ position: {x: (f.labelX != null ? f.labelX : f.x + f.w / 2), y: f.y + ty}, selectable: false, grabbable: false});
|
|
|
});
|
|
|
|
|
|
L.placements.forEach(function (p) {
|
|
|
@@ -1522,6 +1630,38 @@
|
|
|
setTimeout(function () { n.removeClass('hl'); }, 2600);
|
|
|
}
|
|
|
|
|
|
+ function exportPng() {
|
|
|
+ if (!cy) return;
|
|
|
+ var btn = el('l2-export'), oldText = btn ? btn.textContent : '';
|
|
|
+ if (btn) { btn.disabled = true; btn.textContent = t('l2.exporting', '正在导出…'); }
|
|
|
+ var wasDark = dark;
|
|
|
+ try {
|
|
|
+ // The exported file must always be the day palette, even when the
|
|
|
+ // operator is currently using Zabbix dark mode.
|
|
|
+ cy.elements().removeClass('hl dim pathhl');
|
|
|
+ cy.style(styleFor(false));
|
|
|
+ if (typeof cy.forceRender === 'function') cy.forceRender();
|
|
|
+ var data = cy.png({full: true, scale: 1, maxWidth: 2600, maxHeight: 18000, bg: '#ffffff'});
|
|
|
+ var now = new Date();
|
|
|
+ function p2(v) { return (v < 10 ? '0' : '') + v; }
|
|
|
+ var suffix = META.usedTs ? ('_' + META.usedTs) : '_live';
|
|
|
+ var name = 'L2_Topology_' + now.getFullYear() + p2(now.getMonth() + 1) + p2(now.getDate()) +
|
|
|
+ '_' + p2(now.getHours()) + p2(now.getMinutes()) + suffix + '.png';
|
|
|
+ var a = document.createElement('a');
|
|
|
+ a.href = data;
|
|
|
+ a.download = name;
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ } catch (e) {
|
|
|
+ window.alert(t('l2.export_fail', '导出失败,请重试。'));
|
|
|
+ } finally {
|
|
|
+ cy.style(styleFor(wasDark));
|
|
|
+ if (typeof cy.forceRender === 'function') cy.forceRender();
|
|
|
+ if (btn) { btn.disabled = false; btn.textContent = oldText; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
function groupOfNodeId(id) {
|
|
|
var hit = (lastSnap && lastSnap.nodes || []).filter(function (n) { return n.id === id; })[0];
|
|
|
return hit ? groupOf(hit).label : null;
|
|
|
@@ -1559,22 +1699,63 @@
|
|
|
lockedPath = null;
|
|
|
}
|
|
|
|
|
|
+ function upstreamGraph(nodeId) {
|
|
|
+ var snap = lastSnap || {};
|
|
|
+ var nodes = snap.nodes || [], edges = snap.edges || [];
|
|
|
+ var byId = {}, adj = {};
|
|
|
+ nodes.forEach(function (n) { byId[n.id] = n; adj[n.id] = []; });
|
|
|
+ edges.forEach(function (e) {
|
|
|
+ if (adj[e.source] && adj[e.target]) { adj[e.source].push(e.target); adj[e.target].push(e.source); }
|
|
|
+ });
|
|
|
+ var coreIds = nodes.filter(function (n) {
|
|
|
+ return (lastRoleMap[n.id] || roleOf(n)) === 'core';
|
|
|
+ }).map(function (n) { return n.id; });
|
|
|
+ // If the configured core list is empty, fall back to the collector's root
|
|
|
+ // tier so the upstream chain still starts at the physical core.
|
|
|
+ if (!coreIds.length) {
|
|
|
+ nodes.forEach(function (n) {
|
|
|
+ if (String(n.tier || '').toLowerCase() === 'core' || /^core(\s|-)/i.test(String(n.name || ''))) coreIds.push(n.id);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ var level = {}, queue = [];
|
|
|
+ coreIds.forEach(function (id) { level[id] = 0; queue.push(id); });
|
|
|
+ while (queue.length) {
|
|
|
+ var cur = queue.shift();
|
|
|
+ (adj[cur] || []).forEach(function (nb) {
|
|
|
+ if (level[nb] == null) { level[nb] = level[cur] + 1; queue.push(nb); }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ var nodeSet = {}, edgeSet = {}, stack = [nodeId];
|
|
|
+ nodeSet[nodeId] = true;
|
|
|
+ while (stack.length) {
|
|
|
+ var cur = stack.pop();
|
|
|
+ var curLv = level[cur];
|
|
|
+ if (curLv == null || curLv <= 0) continue;
|
|
|
+ (adj[cur] || []).forEach(function (nb) {
|
|
|
+ if (level[nb] !== curLv - 1) return;
|
|
|
+ // Cytoscape edges are anchored to hidden edgepoint nodes; the real
|
|
|
+ // switch pair is stored in p/c, so identify both orientations.
|
|
|
+ edgeSet[cur + '|' + nb] = true;
|
|
|
+ edgeSet[nb + '|' + cur] = true;
|
|
|
+ if (!nodeSet[nb]) { nodeSet[nb] = true; stack.push(nb); }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return {nodes: nodeSet, edges: edgeSet};
|
|
|
+ }
|
|
|
+
|
|
|
function highlightPath(nodeId) {
|
|
|
if (!cy) return;
|
|
|
- var chain = [], id = nodeId, guard = 0;
|
|
|
- while (id && guard++ < 40) {
|
|
|
- chain.unshift(id);
|
|
|
- id = pathParent[id] || null;
|
|
|
- }
|
|
|
+ var up = upstreamGraph(nodeId);
|
|
|
cy.elements('node[type="switch"], node[type="ghost"], edge').addClass('dim').removeClass('pathhl');
|
|
|
var nodes = cy.collection();
|
|
|
- chain.forEach(function (cid) { var n = cy.getElementById(cid); if (n && n.length) nodes = nodes.union(n); });
|
|
|
+ Object.keys(up.nodes).forEach(function (id) {
|
|
|
+ var n = cy.getElementById(id);
|
|
|
+ if (n && n.length) nodes = nodes.union(n);
|
|
|
+ });
|
|
|
nodes.removeClass('dim').addClass('pathhl');
|
|
|
var edges = cy.edges().filter(function (e) {
|
|
|
var p = e.data('p'), c = e.data('c');
|
|
|
- if (p && c) return chain.indexOf(p) >= 0 && chain.indexOf(c) >= 0;
|
|
|
- var s = e.data('source'), t = e.data('target');
|
|
|
- return chain.indexOf(s) >= 0 && chain.indexOf(t) >= 0;
|
|
|
+ return p && c && (up.edges[p + '|' + c] || up.edges[c + '|' + p]);
|
|
|
});
|
|
|
edges.removeClass('dim').addClass('pathhl');
|
|
|
}
|
|
|
@@ -1736,6 +1917,7 @@
|
|
|
el('l2-zin').addEventListener('click', function () { if (cy) cy.zoom({level: cy.zoom() * 1.25, renderedPosition: {x: cy.width() / 2, y: cy.height() / 2}}); });
|
|
|
el('l2-zout').addEventListener('click', function () { if (cy) cy.zoom({level: cy.zoom() / 1.25, renderedPosition: {x: cy.width() / 2, y: cy.height() / 2}}); });
|
|
|
el('l2-fit').addEventListener('click', function () { if (cy) cy.fit(cy.elements(), 50); });
|
|
|
+ if (el('l2-export')) el('l2-export').addEventListener('click', exportPng);
|
|
|
el('l2-refresh-btn').addEventListener('click', function () { reload(true); });
|
|
|
window.addEventListener('resize', fitViewport);
|
|
|
|