/* ===================================================================== L2 Physical Topology — display-only interactive map. Groups switches into frames (COBCO outer → workshop/DataCenter/Core), colours links by speed, nodes by latency & alarm, supports dark mode, terminal show / only-offline, live + historical snapshots, MAC/IP search. ===================================================================== */ (function () { 'use strict'; var CFG = window.L2_CFG || {}; var T = window.L2_I18N || {}; var META = window.L2_META || {}; var SNAP = window.L2SNAP || null; var colors = CFG.colors || {100:'#F39C12',1000:'#2ECC71',10000:'#3498DB',20000:'#9B59B6',unknown:'#E74C3C',down:'#C0392B'}; var LATW = (CFG.latency_warn != null) ? +CFG.latency_warn : 3; var LATB = (CFG.latency_bad != null) ? +CFG.latency_bad : 5; var TIER_BG = {core:'#1F4E78', dist:'#2E75B6', access:'#548235'}; var TIER_BG_DARK = {core:'#1B4A72', dist:'#2A5E93', access:'#3E6B2E'}; var SEV_COLORS = ['#97AAB3','#7499FF','#FFC859','#FFA059','#E97659','#E45959']; var NODE_W = 156, NODE_H = 50, GX = 26, GY = 18, PADX = 20, PADY = 42; var DGAP_X = 18, DGAP_Y = 26; var GROUP_GAP_X = 46, GROUP_GAP_Y = 48, MAXW = 2800; // overview ("全景") tiles var TILE_W = 232, TILE_H = 116, TILE_GAP_X = 24, TILE_GAP_Y = 22; var cy = null; var dark = false; var showTerm = false; var onlyOff = false; var paused = false; var countdown = 0; var timer = null; var lastSnap = null; var viewGroup = null; // null = overview, otherwise workshop label var detailNet = null; // null/auto, 'SURVE' or 'OFFICE' inside a workshop var pathParent = {}; // child -> parent chain for hover highlighting var lockedPath = null; // switch id whose path is locked by click function el(id) { return document.getElementById(id); } function t(k, d) { return (T[k] != null) ? T[k] : (d != null ? d : k); } /* ---------------- name parsing / grouping ---------------- */ function parseName(name) { var r = {workshop:'', floor:'', cab:'', u:'', area:'', role:'', ip:''}; if (!name) return r; var toks = String(name).split('-'); for (var i = 0; i < toks.length; i++) { var tk = toks[i]; if (/^\d{1,3}(\.\d{1,3}){3}$/.test(tk)) { r.ip = tk; continue; } if (!r.workshop && /^\d{4}$/.test(tk)) { r.workshop = tk; continue; } if (!r.floor && /^\d[A-Z]\w*$/i.test(tk)) { var m = /^(\d)([A-Z])(\w+)$/i.exec(tk); if (m) { r.floor = m[1]; r.cab = m[2]; r.u = m[3]; continue; } } if (!r.area && /^(OFFICE|SURVE|GRDA|DC|IDC|LAB|PROD)$/i.test(tk)) { r.area = tk.toUpperCase(); continue; } if (!r.role && /^(Dist|Access|Core)$/i.test(tk)) { r.role = tk; } } return r; } function labelFor(n) { var p = parseName(n.name); var l1 = []; if (p.workshop) l1.push(p.workshop); if (p.area) l1.push(p.area); if (!l1.length) { l1.push((n.name || '').split('-')[0] || n.ip); } var l2 = []; if (p.floor) { var loc = p.floor + 'F' + (p.cab || '') + (p.u ? '-' + p.u + 'U' : ''); l1.push(loc); } if (p.role) l2.push(p.role); l2.push(n.ip); return l1.join(' · ') + '\n' + l2.join(' · '); } function detailLabel(n) { var s = labelFor(n); if (viewGroup) { var p = viewGroup + ' · '; if (s.indexOf(p) === 0) s = s.substring(p.length); } return s; } function rowLabel(n) { var s = String((n && n.name) || (n && n.ip) || ''); s = s.replace(/^COBCO-/i, ''); if (viewGroup) { try { s = s.replace(new RegExp('^' + viewGroup + '-', 'i'), ''); } catch (e) {} } return s; } function rootLabel(n) { var s = rowLabel(n); var m = /^(.+?)-(SURVE|OFFICE|OTHER)-(Dist|Access)-(.+)$/i.exec(s); if (m) return m[2].toUpperCase() + ' · ' + m[1] + '\n' + m[3] + ' · ' + m[4]; return s; } function expandLabel(tpl, m) { return String(tpl).replace(/\$(\d)/g, function (_, d) { return m && m[+d] != null ? m[+d] : ''; }); } function groupOf(n) { var name = n.name || n.ip || ''; var rules = CFG.group_rules || []; for (var i = 0; i < rules.length; i++) { var r = rules[i]; if (r.match === 'regex') { var re; try { re = new RegExp(r.pattern); } catch (e) { continue; } var m = re.exec(name); if (m) return {label: expandLabel(r.label, m) || (CFG.other_label || 'Other'), big: !!r.big}; } else { if (name.toLowerCase().indexOf(String(r.pattern).toLowerCase()) >= 0) return {label: r.label || (CFG.other_label || 'Other'), big: !!r.big}; } } return {label: CFG.other_label || 'Other', big: false}; } /* ---------------- overview layout (workshop tiles) ---------------- */ function groupStats(snap) { var map = {}; var nodeById = {}; (snap.nodes || []).forEach(function (n) { nodeById[n.id] = n; var g = groupOf(n); var s = map[g.label] || (map[g.label] = {label: g.label, big: g.big, nodes: [], alarms: 0, offline: 0, maxsev: -1, latmax: 0, up: 0}); s.nodes.push(n); s.offline += (+n.term_offline || 0); if (n.alarms && n.alarms.length) { s.alarms += n.alarms.length; var sev = (n.maxsev != null && +n.maxsev >= 0) ? +n.maxsev : -1; n.alarms.forEach(function (a) { if (a && a.sev != null && +a.sev > sev) sev = +a.sev; }); if (sev > s.maxsev) s.maxsev = sev; } if (n.latency_ms != null && +n.latency_ms > s.latmax) s.latmax = +n.latency_ms; }); (snap.edges || []).forEach(function (e) { var a = nodeById[e.source], b = nodeById[e.target]; if (!a || !b) return; var ga = groupOf(a), gb = groupOf(b); if (ga.label === gb.label) return; // count links that leave this workshop towards a core switch if ((/core/i.test(a.tier || '') || /core/i.test(a.name || '')) && map[gb.label]) map[gb.label].up++; if ((/core/i.test(b.tier || '') || /core/i.test(b.name || '')) && map[ga.label]) map[ga.label].up++; }); return map; } function groupRank(l) { if (/core/i.test(l)) return 0; if (/data\s*center/i.test(l)) return 1; var m = /^(\d{4})([A-Za-z]?)$/.exec(l); if (m) return 1000 + parseInt(m[1], 10) * 40 + (m[2] ? m[2].toUpperCase().charCodeAt(0) - 64 : 0); return 5000; } function computeOverview(snap) { var stats = groupStats(snap); var labels = Object.keys(stats).sort(function (a, b) { return groupRank(a) - groupRank(b); }); var special = labels.filter(function (l) { return groupRank(l) < 1000; }); var others = labels.filter(function (l) { return groupRank(l) >= 1000; }); var COLS = Math.max(4, Math.min(7, Math.ceil(Math.sqrt(Math.max(1, others.length))))); var gridW = COLS * TILE_W + (COLS - 1) * TILE_GAP_X; var pos = {}, tiles = [], y = 0; if (special.length) { var sW = special.length * TILE_W + (special.length - 1) * TILE_GAP_X; var sx = Math.max(0, (gridW - sW) / 2); special.forEach(function (l) { tiles.push({label: l, x: sx, y: y, w: TILE_W, h: TILE_H}); pos[l] = {x: sx + TILE_W / 2, y: y + TILE_H / 2}; sx += TILE_W + TILE_GAP_X; }); y += TILE_H + 46; } var maxX = gridW, maxY = y; for (var i = 0; i < others.length; i += COLS) { var row = others.slice(i, i + COLS); var rW = row.length * TILE_W + (row.length - 1) * TILE_GAP_X; var rx = Math.max(0, (gridW - rW) / 2); row.forEach(function (l, j) { tiles.push({label: l, x: rx + j * (TILE_W + TILE_GAP_X), y: y, w: TILE_W, h: TILE_H}); pos[l] = {x: rx + j * (TILE_W + TILE_GAP_X) + TILE_W / 2, y: y + TILE_H / 2}; }); y += TILE_H + TILE_GAP_Y; maxY = y - TILE_GAP_Y; maxX = Math.max(maxX, rx + rW); } var root = {x: -34, y: -64, w: maxX + 68, h: maxY + 64 + 40}; root.cx = root.x + root.w / 2; root.cy = root.y + root.h / 2; return {stats: stats, labels: labels, pos: pos, tiles: tiles, root: root}; } function aggregateEdges(snap) { var gid = {}; (snap.nodes || []).forEach(function (n) { gid[n.id] = groupOf(n).label; }); var pairs = {}; (snap.edges || []).forEach(function (e) { var a = gid[e.source], b = gid[e.target]; if (!a || !b || a === b) return; var k = (a < b) ? (a + '\u0001' + b) : (b + '\u0001' + a); var p = pairs[k] || (pairs[k] = {a: a, b: b, count: 0, speed: 0, down: false}); p.count++; if ((parseFloat(e.speed) || 0) > p.speed) p.speed = parseFloat(e.speed) || 0; if (e.state && e.state !== 'up') p.down = true; }); return Object.keys(pairs).map(function (k) { return pairs[k]; }); } /* ---------------- detail layout (one workshop, left-to-right tree) ---------------- */ function floorInfo(n) { var toks = String((n && n.name) || '').split('-'); for (var i = 0; i < toks.length; i++) { var tk = toks[i]; var bm = /^B(\d+)$/i.exec(tk); if (bm) { var bn = parseInt(bm[1], 10); return {key: 'B' + bn, label: 'B' + bn, rank: -200 + bn}; } if (/^GRD/i.test(tk)) return {key: 'GRD', label: 'GRD', rank: -100}; var fm = /^(\d+(?:\.\d+)?)F/i.exec(tk); if (fm) { var num = parseFloat(fm[1]); var txt = (num % 1 ? num.toFixed(1) : String(num)) + 'F'; return {key: txt, label: txt, rank: num * 10}; } var cm = /^(\d)([A-Z])(\w+)$/i.exec(tk); if (cm) { var n2 = parseInt(cm[1], 10); return {key: n2 + 'F', label: n2 + 'F', rank: n2 * 10}; } } return {key: 'NA', label: t('l2.floor_na', 'Other'), rank: -500}; } function baseNetworkOf(n) { var s = String((n && n.name) || '').toUpperCase(); if (s.indexOf('SURVE') >= 0) return 'SURVE'; if (s.indexOf('OFFICE') >= 0) return 'OFFICE'; var ip = String((n && n.ip) || ''); if (/^10\.169\./.test(ip)) return 'SURVE'; if (/^10\.160\./.test(ip)) return 'OFFICE'; return 'OTHER'; } function networkOf(n) { return baseNetworkOf(n); } function networkLabel(key) { if (key === 'SURVE') return t('l2.net_surve', 'SURVE 监控网'); if (key === 'OFFICE') return t('l2.net_office', 'OFFICE 办公网'); return t('l2.net_other', '其他网络'); } function computeDetail(snap, label) { var all = snap.nodes || []; var byId = {}, groupNodes = []; all.forEach(function (n) { byId[n.id] = n; if (groupOf(n).label === label) groupNodes.push(n); }); // Split one workshop by physical network first: office and surveillance never mix. var gids = {}, rawAdj = {}; groupNodes.forEach(function (n) { gids[n.id] = true; rawAdj[n.id] = []; }); (snap.edges || []).forEach(function (e) { if (gids[e.source] && gids[e.target]) { rawAdj[e.source].push(e.target); rawAdj[e.target].push(e.source); } }); var netMap = {}; groupNodes.forEach(function (n) { netMap[n.id] = baseNetworkOf(n); }); // Unlabelled access switches inherit the network of the distribution they hang from. for (var pass = 0; pass < 4; pass++) { groupNodes.forEach(function (n) { if (netMap[n.id] !== 'OTHER') return; var votes = {}; rawAdj[n.id].forEach(function (id) { var k = netMap[id]; if (k && k !== 'OTHER') votes[k] = (votes[k] || 0) + 1; }); var best = null, bestN = 0; Object.keys(votes).forEach(function (k) { if (votes[k] > bestN) { best = k; bestN = votes[k]; } }); if (best) netMap[n.id] = best; }); } var counts = {}, nets = []; groupNodes.forEach(function (n) { var k = netMap[n.id]; if (!counts[k]) { counts[k] = 0; nets.push(k); } counts[k]++; }); nets.sort(function (a, b) { if (a === 'SURVE') return -1; if (b === 'SURVE') return 1; if (a === 'OFFICE') return -1; if (b === 'OFFICE') return 1; return counts[b] - counts[a]; }); var activeNet = (detailNet && counts[detailNet]) ? detailNet : (nets.length ? nets[0] : 'OTHER'); var nodes = groupNodes.filter(function (n) { return netMap[n.id] === activeNet; }); var ids = {}; nodes.forEach(function (n) { ids[n.id] = true; }); var internal = [], external = []; (snap.edges || []).forEach(function (e) { var a = ids[e.source], b = ids[e.target]; if (a && b) internal.push(e); else if (a || b) external.push(e); }); var adj = {}; nodes.forEach(function (n) { adj[n.id] = []; }); internal.forEach(function (e) { adj[e.source].push(e.target); adj[e.target].push(e.source); }); // Distribution switches are the left-hand roots of the workshop tree. var dists = nodes.filter(function (n) { return /dist/i.test(n.tier || '') && /dist/i.test(n.name || ''); }) .sort(function (a, b) { return (adj[b.id].length - adj[a.id].length) || String(a.name).localeCompare(String(b.name)); }); if (!dists.length) { var cand = nodes.slice().sort(function (a, b) { return adj[b.id].length - adj[a.id].length; })[0]; if (cand) dists = [cand]; } // One physical root per network tab; peer/redundant distribution switches are listed below it. if (dists.length > 1) dists = [dists[0]]; // Group all non-distribution switches into floor buckets (GRD lowest, then 1F/2F/3F...). var floorMap = {}; nodes.forEach(function (n) { if (dists.indexOf(n) >= 0) return; if (/core/i.test(n.tier || '')) return; var fi = floorInfo(n); var f = floorMap[fi.key] || (floorMap[fi.key] = {key: fi.key, label: fi.label, rank: fi.rank, nodes: []}); f.nodes.push(n); }); var floors = Object.keys(floorMap).map(function (k) { return floorMap[k]; }); floors.forEach(function (f) { f.nodes.sort(function (a, b) { var pa = parseName(a.name), pb = parseName(b.name); var ca = (pa.cab || '') + (pa.u || ''), cb = (pb.cab || '') + (pb.u || ''); if (ca !== cb) return ca.localeCompare(cb); return String(a.name).localeCompare(String(b.name)); }); }); // Highest floor at the top, GRD at the bottom. floors.sort(function (a, b) { return b.rank - a.rank; }); var WORK_L = 300, FLOOR_X = 520, DIST_FRAME_W = 190, TOP_OFF = 34; var ROW_W = 380, ROW_H = 24, ROW_GAP = 8, FLOOR_HEAD = 28, FLOOR_GAP = 12; var floorW = ROW_W + PADX * 2 + 110; floors.forEach(function (f) { f.cols = 1; f.rows = f.nodes.length; f.innerW = ROW_W; f.h = FLOOR_HEAD + f.rows * ROW_H + Math.max(0, f.rows - 1) * ROW_GAP + 10; }); var workRight = FLOOR_X + floorW + 14; var workW = workRight - WORK_L; var pos = {}, top = 44 + TOP_OFF; floors.forEach(function (f) { f.frame = {x: FLOOR_X, y: top, w: floorW, h: f.h}; f.nodes.forEach(function (n, i) { pos[n.id] = { x: FLOOR_X + PADX + ROW_W / 2, y: top + FLOOR_HEAD + i * (ROW_H + ROW_GAP) + ROW_H / 2 }; }); top += f.h + FLOOR_GAP; }); var workH = Math.max(220, top + 6); var distFrame = dists.length ? {x: WORK_L + 8, y: 44 + TOP_OFF, w: DIST_FRAME_W, h: workH - (44 + TOP_OFF) - 14} : null; var distX = WORK_L + 8 + DIST_FRAME_W / 2; if (dists.length) { var childYs = floors.reduce(function (acc, f) { return acc.concat(f.nodes.map(function (n) { return pos[n.id] ? pos[n.id].y : 0; })); }, []); var cy = childYs.length ? (Math.min.apply(null, childYs) + Math.max.apply(null, childYs)) / 2 : distFrame.y + distFrame.h / 2; pos[dists[0].id] = {x: distX, y: cy}; } var missing = nodes.filter(function (n) { return !pos[n.id]; }); missing.forEach(function (n, i) { pos[n.id] = {x: distX, y: (distFrame ? distFrame.y + 30 : 80) + i * (NODE_H + 12)}; }); // One ghost core per external core→distribution uplink, aligned left of its distribution switch. var ghosts = [], ghostSeen = {}; external.forEach(function (e) { var inside = ids[e.source] ? e.source : e.target; var outside = ids[e.source] ? e.target : e.source; if (!pos[inside] || !byId[outside]) return; // Only a real core uplink gets a ghost; other cross-group edges are not drawn as cores. if (!/core/i.test(byId[outside].tier || '') && !/core/i.test(byId[outside].name || '')) return; var key = outside + '|' + inside; if (ghostSeen[key]) return; var g = {id: 'gh_' + outside + '_' + inside, to: inside, node: byId[outside], speed: e.speed, state: e.state}; ghostSeen[key] = g; ghosts.push(g); }); var ghostAt = {}; ghosts.forEach(function (g) { var base = pos[g.to]; var k = ghostAt[g.to] || 0; ghostAt[g.to] = k + 1; g.x = 92; g.y = base.y + (k ? (k % 2 ? 1 : -1) * 46 * Math.ceil(k / 2) : 0); }); // Root each tree at the distribution switch; remember every child's parent for path highlighting. var parentOf = {}, ghostOfDist = {}; ghosts.forEach(function (g) { if (!ghostOfDist[g.to]) ghostOfDist[g.to] = g.id; }); var seen = {}; dists.forEach(function (d) { if (seen[d.id]) return; seen[d.id] = true; parentOf[d.id] = ghostOfDist[d.id] || null; var queue = [d.id]; while (queue.length) { var cur = queue.shift(); adj[cur].forEach(function (nb) { if (seen[nb]) return; seen[nb] = true; parentOf[nb] = cur; queue.push(nb); }); } }); nodes.forEach(function (n) { if (!seen[n.id]) seen[n.id] = true; }); var workshop = {x: WORK_L, y: 0, w: workW, h: workH, label: label + ' · ' + networkLabel(activeNet) + ' · ' + nodes.length + ' ' + t('l2.unit_sw', 'sw')}; var distFrameFull = distFrame ? { x: distFrame.x, y: distFrame.y, w: distFrame.w, h: distFrame.h, label: t('l2.dist', 'Distribution') + ' · ' + dists.length } : null; floors.forEach(function (f) { f.frame.label = f.label + ' · ' + f.nodes.length + ' ' + t('l2.unit_sw', 'sw'); }); var root = {x: -20, y: -48, w: workRight + 40, h: workH + 80}; root.cx = root.x + root.w / 2; root.cy = root.y + root.h / 2; return { nodes: nodes, byId: byId, pos: pos, ghosts: ghosts, parentOf: parentOf, dists: dists, floors: floors, frames: {workshop: workshop, dist: distFrameFull}, root: root, totalW: workRight, ids: ids, internal: internal, nets: nets.map(function (k) { return {key: k, count: counts[k]}; }), activeNet: activeNet, totalNodes: groupNodes.length }; } /* ---------------- speed colour ---------------- */ function speedColor(speed, state) { if (state && state !== 'up') return colors.down; var s = parseFloat(speed); if (!isFinite(s) || s <= 0) return colors.unknown; if (s <= 100) return colors['100']; if (s <= 1000) return colors['1000']; if (s <= 10000) return colors['10000']; return colors['20000']; } function speedWidth(speed) { var s = parseFloat(speed) || 0; if (s >= 20000) return 4.5; if (s >= 10000) return 3.6; if (s >= 1000) return 2.6; return 1.8; } function speedText(speed) { var s = parseFloat(speed) || 0; if (!s) return '?'; if (s >= 1000) return (s / 1000) + 'G'; return s + 'M'; } /* ---------------- style ---------------- */ function styleFor(d) { var bg = d ? TIER_BG_DARK : TIER_BG; return [ {selector: 'node', style: { 'width': 'data(w)', 'height': 'data(h)', 'shape': 'round-rectangle', 'background-color': 'data(bg)', 'border-width': 2, 'border-color': 'data(bd)', 'label': 'data(label)', 'color': d ? '#eaf1fb' : '#ffffff', 'font-size': 11.5, 'font-weight': 600, 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'wrap', 'text-max-width': NODE_W - 16, 'padding': '2px', 'overlay-opacity': 0, 'z-index-compare': 'manual', 'z-index': 20 }}, {selector: 'node[type="frame"]', style: { 'background-opacity': d ? 0.05 : 0.035, 'background-color': d ? '#5b7bb0' : '#8fa6c4', 'border-width': 1.6, 'border-style': 'dashed', 'border-color': d ? '#3f5377' : '#c2ccdb', 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'font-size': 'data(fs)', 'font-weight': 800, 'color': d ? '#aebfd8' : '#526178', 'text-margin-y': 8, 'padding': '0px', 'events': 'no', 'z-index-compare': 'manual', 'z-index': 2 }}, {selector: 'node[type="wsframe"]', style: { 'background-opacity': d ? 0.025 : 0.018, 'background-color': d ? '#6f8fc4' : '#8fa6c4', 'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#4f6790' : '#9fb0c6', 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none', 'font-size': 19, 'font-weight': 900, 'color': d ? '#d7e6ff' : '#2f4560', 'text-margin-x': 0, 'text-margin-y': 8, 'padding': '0px', 'events': 'no', 'z-index-compare': 'manual', 'z-index': 1 }}, {selector: 'node[type="floorframe"]', style: { 'background-opacity': d ? 0.055 : 0.035, 'background-color': d ? '#5b7bb0' : '#8fa6c4', 'border-width': 1.2, 'border-style': 'dashed', 'border-color': d ? '#3f5377' : '#b9c6d8', 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'text-wrap': 'none', 'font-size': 13, 'font-weight': 800, 'color': d ? '#a9c1e3' : '#425a78', 'text-margin-x': 0, 'text-margin-y': 7, 'padding': '0px', 'events': 'no', 'z-index-compare': 'manual', 'z-index': 2 }}, {selector: 'node[type="root"]', style: { 'background-opacity': d ? 0.02 : 0.012, 'background-color': d ? '#7aa0d8' : '#8ea6c2', 'border-width': 2.2, 'border-style': 'solid', 'border-color': d ? '#476492' : '#9fb0c6', 'label': 'data(label)', 'text-valign': 'top', 'text-halign': 'center', 'font-size': 28, 'font-weight': 900, 'color': d ? '#cfe0ff' : '#33465f', 'text-margin-y': 10, 'events': 'no', 'z-index-compare': 'manual', 'z-index': 0 }}, {selector: 'node[type="tile"]', style: { 'width': 'data(w)', 'height': 'data(h)', 'shape': 'round-rectangle', 'background-color': 'data(bg)', 'background-opacity': 0.97, 'border-width': 2.5, 'border-color': 'data(bd)', 'label': 'data(label)', 'color': 'data(fg)', 'font-size': 16, 'font-weight': 700, 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'wrap', 'text-max-width': TILE_W - 26, 'line-height': 1.45, 'padding': '6px', 'overlay-opacity': 0, 'z-index-compare': 'manual', 'z-index': 20 }}, {selector: 'node[type="tile"].alarm', style: {'border-width': 5}}, {selector: 'node[type="tile"].core', style: {'border-width': 3.5}}, {selector: 'node[type="ghost"]', style: { 'width': 190, 'height': 40, 'shape': 'round-rectangle', 'background-color': d ? '#52667f' : '#c8d3e2', 'background-opacity': 0.55, 'border-width': 1.2, 'border-style': 'dashed', 'border-color': d ? '#7d93ad' : '#8fa3bc', 'label': 'data(label)', 'color': d ? '#dce8fa' : '#3b4f68', 'font-size': 11, 'font-weight': 600, 'text-wrap': 'wrap', 'text-max-width': 176, 'z-index-compare': 'manual', 'z-index': 15 }}, {selector: 'node[type="edgepoint"]', style: { 'width': 1, 'height': 1, 'opacity': 0, 'events': 'no' }}, {selector: 'node[type="term"]', style: { 'width': 12, 'height': 12, 'shape': 'ellipse', 'background-color': 'data(bg)', 'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '', 'z-index-compare': 'manual', 'z-index': 30 }}, {selector: 'edge[type="tilelink"]', style: { 'width': 'data(w)', 'line-color': 'data(color)', 'opacity': 0.55, 'curve-style': 'bezier', 'target-arrow-shape': 'none', 'z-index-compare': 'manual', 'z-index': 5 }}, {selector: 'edge[type="ghost"]', style: { 'width': 1.2, 'line-color': d ? '#6b809b' : '#a9b6c8', 'line-style': 'dashed', 'opacity': 0.7, 'curve-style': 'bezier', 'target-arrow-shape': 'none', 'z-index-compare': 'manual', 'z-index': 8 }}, {selector: 'edge[type="uplink"]', style: { 'width': 'data(w)', 'line-color': 'data(color)', 'line-style': 'data(ls)', 'opacity': 0.95, 'curve-style': 'bezier', 'target-arrow-shape': 'none', 'label': 'data(lbl)', 'font-size': 10, 'font-weight': 700, 'color': d ? '#dce8fa' : '#25405f', 'text-background-color': d ? '#0d1b2a' : '#ffffff', 'text-background-opacity': 0.88, 'text-background-padding': '2px', 'text-rotation': 'autorotate', 'z-index-compare': 'manual', 'z-index': 40 }}, {selector: 'edge', style: { 'width': 'data(w)', 'line-color': 'data(color)', 'line-style': 'data(ls)', 'curve-style': 'bezier', 'opacity': 0.92, 'target-arrow-shape': 'none', 'z-index-compare': 'manual', 'z-index': 12 }}, {selector: 'edge[type="term"]', style: { '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}}, {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}}, {selector: 'edge.detail', style: {'opacity': 0.72, 'curve-style': 'straight', 'source-endpoint': '0% 50%', 'target-endpoint': '0% 50%'}}, {selector: 'node[type="switch"]', style: { 'background-color': function (e) { return e.data('bg'); }, 'text-max-width': 'data(tw)', 'text-wrap': 'none', 'text-halign': 'center', 'font-size': 10.5 }}, {selector: 'node[type="switch"].root', style: {'text-wrap': 'wrap'}} ]; } /* ---------------- render ---------------- */ function applyElements(els) { if (!cy) { cy = cytoscape({ container: el('l2-cy'), elements: els, style: styleFor(dark), layout: {name: 'preset', fit: true, padding: 40}, wheelSensitivity: 0.2, minZoom: 0.15, maxZoom: 4 }); bindCy(); } else { cy.elements().remove(); cy.add(els); cy.style(styleFor(dark)); cy.layout({name: 'preset', fit: true, padding: 40}).run(); } } function render(snap) { if (!snap || !snap.nodes) { el('l2-cy').innerHTML = '