GUI -- Migrating the add/update device functionality to the Topology View. (WIP)
- still a lot of work to do.
Change-Id: I0453b7e2ec20a8a8149fd9d6440a13a3d43fbfd6
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.css b/web/gui/src/main/webapp/app/fw/svg/icon.css
index 20d8440..ff1bb5a 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.css
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.css
@@ -70,3 +70,7 @@
.dark svg.embeddedIcon .icon rect {
stroke: #ccc;
}
+
+svg .svgIcon {
+ fill-rule: evenodd;
+}
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.js b/web/gui/src/main/webapp/app/fw/svg/icon.js
index d8c2584..21f9b9d 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -26,15 +26,17 @@
cornerSize = vboxSize / 10,
viewBox = '0 0 ' + vboxSize + ' ' + vboxSize;
- // maps icon id to the glyph id it uses.
- // note: icon id maps to a CSS class for styling that icon
+ // Maps icon ID to the glyph ID it uses.
+ // NOTE: icon ID maps to a CSS class for styling that icon
var glyphMapping = {
- deviceOnline: 'checkMark',
- deviceOffline: 'xMark',
- tableColSortAsc: 'triangleUp',
- tableColSortDesc: 'triangleDown',
- tableColSortNone: '-'
- };
+ deviceOnline: 'checkMark',
+ deviceOffline: 'xMark',
+ tableColSortAsc: 'triangleUp',
+ tableColSortDesc: 'triangleDown',
+ tableColSortNone: '-'
+ };
+
+
function ensureIconLibDefs() {
var body = d3.select('body'),
@@ -48,6 +50,108 @@
return svg.select('defs');
}
+ // div is a D3 selection of the <DIV> element into which icon should load
+ // iconCls is the CSS class used to identify the icon
+ // size is dimension of icon in pixels. Defaults to 20.
+ // installGlyph, if truthy, will cause the glyph to be added to
+ // well-known defs element. Defaults to false.
+ // svgClass is the CSS class used to identify the SVG layer.
+ // Defaults to 'embeddedIcon'.
+ function loadIcon(div, iconCls, size, installGlyph, svgClass) {
+ var dim = size || 20,
+ svgCls = svgClass || 'embeddedIcon',
+ gid = glyphMapping[iconCls] || 'unknown',
+ svg, g;
+
+ if (installGlyph) {
+ gs.loadDefs(ensureIconLibDefs(), [gid], true);
+ }
+
+ svg = div.append('svg').attr({
+ 'class': svgCls,
+ width: dim,
+ height: dim,
+ viewBox: viewBox
+ });
+
+ g = svg.append('g').attr({
+ 'class': 'icon ' + iconCls
+ });
+
+ g.append('rect').attr({
+ width: vboxSize,
+ height: vboxSize,
+ rx: cornerSize
+ });
+
+ if (gid !== '-') {
+ g.append('use').attr({
+ width: vboxSize,
+ height: vboxSize,
+ 'class': 'glyph',
+ 'xlink:href': '#' + gid
+ });
+ }
+ }
+
+ function loadEmbeddedIcon(div, iconCls, size) {
+ loadIcon(div, iconCls, size, true);
+ }
+
+
+ // configuration for device and host icons in the topology view
+ var config = {
+ device: {
+ dim: 36,
+ rx: 4
+ },
+ host: {
+ radius: {
+ noGlyph: 9,
+ withGlyph: 14
+ },
+ glyphed: {
+ endstation: 1,
+ bgpSpeaker: 1,
+ router: 1
+ }
+ }
+ };
+
+
+ // Adds a device icon to the specified element, using the given glyph.
+ // Returns the D3 selection of the icon.
+ function addDeviceIcon(elem, glyphId) {
+ var cfg = config.device,
+ g = elem.append('g')
+ .attr('class', 'svgIcon deviceIcon');
+
+ g.append('rect').attr({
+ x: 0,
+ y: 0,
+ rx: cfg.rx,
+ width: cfg.dim,
+ height: cfg.dim
+ });
+
+ g.append('use').attr({
+ 'xlink:href': '#' + glyphId,
+ width: cfg.dim,
+ height: cfg.dim
+ });
+
+ g.dim = cfg.dim;
+ return g;
+ }
+
+ function addHostIcon(elem, glyphId) {
+ // TODO:
+ }
+
+
+ // =========================
+ // === DEFINE THE MODULE
+
angular.module('onosSvg')
.factory('IconService', ['$log', 'FnService', 'GlyphService',
function (_$log_, _fs_, _gs_) {
@@ -55,57 +159,12 @@
fs = _fs_;
gs = _gs_;
- // div is a D3 selection of the <DIV> element into which icon should load
- // iconCls is the CSS class used to identify the icon
- // size is dimension of icon in pixels. Defaults to 20.
- // installGlyph, if truthy, will cause the glyph to be added to
- // well-known defs element. Defaults to false.
- // svgClass is the CSS class used to identify the SVG layer.
- // Defaults to 'embeddedIcon'.
- function loadIcon(div, iconCls, size, installGlyph, svgClass) {
- var dim = size || 20,
- svgCls = svgClass || 'embeddedIcon',
- gid = glyphMapping[iconCls] || 'unknown',
- svg, g;
-
- if (installGlyph) {
- gs.loadDefs(ensureIconLibDefs(), [gid], true);
- }
-
- svg = div.append('svg').attr({
- 'class': svgCls,
- width: dim,
- height: dim,
- viewBox: viewBox
- });
-
- g = svg.append('g').attr({
- 'class': 'icon ' + iconCls
- });
-
- g.append('rect').attr({
- width: vboxSize,
- height: vboxSize,
- rx: cornerSize
- });
-
- if (gid !== '-') {
- g.append('use').attr({
- width: vboxSize,
- height: vboxSize,
- 'class': 'glyph',
- 'xlink:href': '#' + gid
- });
- }
- }
-
- function loadEmbeddedIcon(div, iconCls, size) {
- loadIcon(div, iconCls, size, true);
- }
-
return {
loadIcon: loadIcon,
- loadEmbeddedIcon: loadEmbeddedIcon
+ loadEmbeddedIcon: loadEmbeddedIcon,
+ addDeviceIcon: addDeviceIcon,
+ addHostIcon: addHostIcon,
+ iconConfig: function () { return config; }
};
}]);
diff --git a/web/gui/src/main/webapp/app/fw/svg/map.js b/web/gui/src/main/webapp/app/fw/svg/map.js
index f44ffe0..1faf6e2 100644
--- a/web/gui/src/main/webapp/app/fw/svg/map.js
+++ b/web/gui/src/main/webapp/app/fw/svg/map.js
@@ -22,45 +22,53 @@
The Map Service provides a simple API for loading geographical maps into
an SVG layer. For example, as a background to the Topology View.
- e.g. var ok = MapService.loadMapInto(svgLayer, '*continental-us');
+ e.g. var promise = MapService.loadMapInto(svgLayer, '*continental-us');
The Map Service makes use of the GeoDataService to load the required data
from the server and to create the appropriate geographical projection.
+ A promise is returned to the caller, which is resolved with the
+ map projection once created.
*/
(function () {
'use strict';
// injected references
- var $log, fs, gds;
+ var $log, $q, fs, gds;
+
+ function loadMapInto(mapLayer, id, opts) {
+ var promise = gds.fetchTopoData(id),
+ deferredProjection = $q.defer();
+
+ if (!promise) {
+ $log.warn('Failed to load map: ' + id);
+ return false;
+ }
+
+ promise.then(function () {
+ var gen = gds.createPathGenerator(promise.topodata, opts);
+
+ deferredProjection.resolve(gen.settings.projection);
+
+ mapLayer.selectAll('path')
+ .data(gen.geodata.features)
+ .enter()
+ .append('path')
+ .attr('d', gen.pathgen);
+ });
+ return deferredProjection.promise;
+ }
+
angular.module('onosSvg')
- .factory('MapService', ['$log', 'FnService', 'GeoDataService',
- function (_$log_, _fs_, _gds_) {
+ .factory('MapService', ['$log', '$q', 'FnService', 'GeoDataService',
+ function (_$log_, _$q_, _fs_, _gds_) {
$log = _$log_;
+ $q = _$q_;
fs = _fs_;
gds = _gds_;
- function loadMapInto(mapLayer, id, opts) {
- var promise = gds.fetchTopoData(id);
- if (!promise) {
- $log.warn('Failed to load map: ' + id);
- return false;
- }
-
- promise.then(function () {
- var gen = gds.createPathGenerator(promise.topodata, opts);
-
- mapLayer.selectAll('path')
- .data(gen.geodata.features)
- .enter()
- .append('path')
- .attr('d', gen.pathgen);
- });
- return true;
- }
-
return {
loadMapInto: loadMapInto
};
diff --git a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
index bef8248..3a35e9f 100644
--- a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
+++ b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
@@ -240,13 +240,18 @@
el.style('visibility', (b ? 'visible' : 'hidden'));
}
+ function safeId(s) {
+ return s.replace(/[^a-z0-9]/gi, '-');
+ }
+
return {
createDragBehavior: createDragBehavior,
loadGlow: loadGlow,
cat7: cat7,
translate: translate,
stripPx: stripPx,
- makeVisible: makeVisible
+ makeVisible: makeVisible,
+ safeId: safeId
};
}]);
}());
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index c3d4ee5..7333a8d 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -245,3 +245,94 @@
/* TODO: add blue glow */
/*filter: url(#blue-glow);*/
}
+
+
+/* --- Topo Nodes --- */
+
+#ov-topo svg .node {
+ cursor: pointer;
+}
+
+#ov-topo svg .node.selected rect,
+#ov-topo svg .node.selected circle {
+ fill: #f90;
+ /* TODO: add blue glow filter */
+ /*filter: url(#blue-glow);*/
+}
+
+#ov-topo svg .node text {
+ pointer-events: none;
+}
+
+/* Device Nodes */
+
+#ov-topo svg .node.device {
+}
+
+#ov-topo svg .node.device rect {
+ stroke-width: 1.5;
+}
+
+#ov-topo svg .node.device.fixed rect {
+ stroke-width: 1.5;
+ stroke: #ccc;
+}
+
+/* note: device is offline without the 'online' class */
+#ov-topo svg .node.device {
+ fill: #777;
+}
+
+#ov-topo svg .node.device.online {
+ fill: #6e7fa3;
+}
+
+/* note: device is offline without the 'online' class */
+#ov-topo svg .node.device text {
+ fill: #bbb;
+ font: 10pt sans-serif;
+}
+
+#ov-topo svg .node.device.online text {
+ fill: white;
+}
+
+#ov-topo svg .node.device .svgIcon rect {
+ fill: #aaa;
+}
+#ov-topo svg .node.device .svgIcon use {
+ fill: #777;
+}
+#ov-topo svg .node.device.selected .svgIcon rect {
+ fill: #f90;
+}
+#ov-topo svg .node.device.online .svgIcon rect {
+ fill: #ccc;
+}
+#ov-topo svg .node.device.online .svgIcon use {
+ fill: #000;
+}
+#ov-topo svg .node.device.online.selected .svgIcon rect {
+ fill: #f90;
+}
+
+
+/* Host Nodes */
+
+#ov-topo svg .node.host {
+ stroke: #000;
+}
+
+#ov-topo svg .node.host text {
+ fill: #846;
+ stroke: none;
+ font: 9pt sans-serif;
+}
+
+svg .node.host circle {
+ stroke: #000;
+ fill: #edb;
+}
+
+
+
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 28b79ac..7c30f87 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -28,7 +28,7 @@
];
// references to injected services etc.
- var $log, fs, ks, zs, gs, ms, sus, tfs;
+ var $log, fs, ks, zs, gs, ms, sus, tfs, tis;
// DOM elements
var ovtopo, svg, defs, zoomLayer, mapG, forceG, noDevsLayer;
@@ -41,20 +41,61 @@
// --- Short Cut Keys ------------------------------------------------
var keyBindings = {
- W: [logWarning, '(temp) log a warning'],
- E: [logError, '(temp) log an error'],
- R: [resetZoom, 'Reset pan / zoom']
+ //O: [toggleSummary, 'Toggle ONOS summary pane'],
+ I: [toggleInstances, 'Toggle ONOS instances pane'],
+ //D: [toggleDetails, 'Disable / enable details pane'],
+
+ //H: [toggleHosts, 'Toggle host visibility'],
+ //M: [toggleOffline, 'Toggle offline visibility'],
+ //B: [toggleBg, 'Toggle background image'],
+ //P: togglePorts,
+
+ //X: [toggleNodeLock, 'Lock / unlock node positions'],
+ //Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
+ L: [cycleLabels, 'Cycle device labels'],
+ //U: [unpin, 'Unpin node (hover mouse over)'],
+ R: [resetZoom, 'Reset pan / zoom'],
+
+ //V: [showRelatedIntentsAction, 'Show all related intents'],
+ //rightArrow: [showNextIntentAction, 'Show next related intent'],
+ //leftArrow: [showPrevIntentAction, 'Show previous related intent'],
+ //W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
+ //A: [showAllTrafficAction, 'Monitor all traffic'],
+ //F: [showDeviceLinkFlowsAction, 'Show device link flows'],
+
+ //E: [equalizeMasters, 'Equalize mastership roles'],
+
+ //esc: handleEscape,
+
+ _helpFormat: [
+ ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
+ ['X', 'Z', 'L', 'U', 'R' ],
+ ['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
+ ]
+
};
- // -----------------
- // these functions are necessarily temporary examples....
- function logWarning() {
- $log.warn('You have been warned!');
+ // mouse gestures
+ var gestures = [
+ ['click', 'Select the item and show details'],
+ ['shift-click', 'Toggle selection state'],
+ ['drag', 'Reposition (and pin) device / host'],
+ ['cmd-scroll', 'Zoom in / out'],
+ ['cmd-drag', 'Pan']
+ ];
+
+ function toggleInstances() {
+ if (tis.isVisible()) {
+ tis.hide();
+ } else {
+ tis.show();
+ }
+ tfs.updateDeviceColors();
}
- function logError() {
- $log.error('You are erroneous!');
+
+ function cycleLabels() {
+ $log.debug('Cycle Labels.....');
}
- // -----------------
function resetZoom() {
zoomer.reset();
@@ -83,7 +124,6 @@
function zoomCallback() {
var tr = zoomer.translate(),
sc = zoomer.scale();
- $log.log('ZOOM: translate = ' + tr + ', scale = ' + sc);
// keep the map lines constant width while zooming
mapG.style('stroke-width', (2.0 / sc) + 'px');
@@ -150,16 +190,19 @@
function setUpMap() {
mapG = zoomLayer.append('g').attr('id', 'topo-map');
- //ms.loadMapInto(map, '*continental_us', {mapFillScale:0.5});
- ms.loadMapInto(mapG, '*continental_us');
+
//showCallibrationPoints();
+ //return ms.loadMapInto(map, '*continental_us', {mapFillScale:0.5});
+
+ // returns a promise for the projection...
+ return ms.loadMapInto(mapG, '*continental_us');
}
// --- Force Layout --------------------------------------------------
- function setUpForce() {
+ function setUpForce(xlink) {
forceG = zoomLayer.append('g').attr('id', 'topo-force');
- tfs.initForce(forceG, svg.attr('width'), svg.attr('height'));
+ tfs.initForce(forceG, xlink, svg.attr('width'), svg.attr('height'));
}
// --- Controller Definition -----------------------------------------
@@ -174,8 +217,12 @@
'TopoInstService',
function ($scope, _$log_, $loc, $timeout, _fs_, mast,
- _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, tis) {
- var self = this;
+ _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
+ var self = this,
+ xlink = {
+ showNoDevs: showNoDevs
+ };
+
$log = _$log_;
fs = _fs_;
ks = _ks_;
@@ -184,6 +231,7 @@
ms = _ms_;
sus = _sus_;
tfs = _tfs_;
+ tis = _tis_;
self.notifyResize = function () {
svgResized(fs.windowSize(mast.mastHeight()));
@@ -207,8 +255,8 @@
setUpDefs();
setUpZoom();
setUpNoDevs();
- setUpMap();
- setUpForce();
+ xlink.projectionPromise = setUpMap();
+ setUpForce(xlink);
tis.initInst();
tps.initPanels();
diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
index b4b5c74..6cd634f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -23,7 +23,7 @@
'use strict';
// injected refs
- var $log, wss, wes, tps, tis;
+ var $log, wss, wes, tps, tis, tfs;
// internal state
var wsock;
@@ -32,7 +32,9 @@
showSummary: showSummary,
addInstance: addInstance,
updateInstance: updateInstance,
- removeInstance: removeInstance
+ removeInstance: removeInstance,
+ addDevice: addDevice,
+ updateDevice: updateDevice
// TODO: implement remaining handlers..
};
@@ -63,6 +65,16 @@
tis.removeInstance(ev.payload);
}
+ function addDevice(ev) {
+ $log.debug(' **** Add Device **** ', ev.payload);
+ tfs.addDevice(ev.payload);
+ }
+
+ function updateDevice(ev) {
+ $log.debug(' **** Update Device **** ', ev.payload);
+ tfs.updateDevice(ev.payload);
+ }
+
// ==========================
var dispatcher = {
@@ -100,14 +112,15 @@
angular.module('ovTopo')
.factory('TopoEventService',
['$log', '$location', 'WebSocketService', 'WsEventService',
- 'TopoPanelService', 'TopoInstService',
+ 'TopoPanelService', 'TopoInstService', 'TopoForceService',
- function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_) {
+ function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_) {
$log = _$log_;
wss = _wss_;
wes = _wes_;
tps = _tps_;
tis = _tis_;
+ tfs = _tfs_;
function bindDispatcher(TopoDomElementsPassedHere) {
// TODO: store refs to topo DOM elements...
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 3a0791c..fb6ca06 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -23,10 +23,29 @@
'use strict';
// injected refs
- var $log, sus;
+ var $log, sus, is, ts, tis, xlink;
+
+ // configuration
+ var labelConfig = {
+ imgPad: 16,
+ padLR: 4,
+ padTB: 3,
+ marginLR: 3,
+ marginTB: 2,
+ port: {
+ gap: 3,
+ width: 18,
+ height: 14
+ }
+ };
+
+ var deviceIconConfig = {
+ xoff: -20,
+ yoff: -18
+ };
// internal state
- var settings,
+ var settings, // merged default settings and options
force, // force layout object
drag, // drag behavior handler
network = {
@@ -34,8 +53,10 @@
links: [],
lookup: {},
revLinkToKey: {}
- };
-
+ },
+ projection, // background map projection
+ deviceLabelIndex = 0, // for device label cycling
+ hostLabelIndex = 0; // for host label cycling
// SVG elements;
var linkG, linkLabelG, nodeG;
@@ -71,12 +92,517 @@
};
+ // ==========================
+ // === EVENT HANDLERS
+
+ function addDevice(data) {
+ var id = data.id,
+ d;
+
+ xlink.showNoDevs(false);
+
+ // although this is an add device event, if we already have the
+ // device, treat it as an update instead..
+ if (network.lookup[id]) {
+ updateDevice(data);
+ return;
+ }
+
+ d = createDeviceNode(data);
+ network.nodes.push(d);
+ network.lookup[id] = d;
+
+ $log.debug("Created new device.. ", d.id, d.x, d.y);
+
+ updateNodes();
+ fStart();
+ }
+
+ function updateDevice(data) {
+ var id = data.id,
+ d = network.lookup[id],
+ wasOnline;
+
+ if (d) {
+ wasOnline = d.online;
+ angular.extend(d, data);
+ if (positionNode(d, true)) {
+ sendUpdateMeta(d, true);
+ }
+ updateNodes();
+ if (wasOnline !== d.online) {
+ // TODO: re-instate link update, and offline visibility
+ //findAttachedLinks(d.id).forEach(restyleLinkElement);
+ //updateOfflineVisibility(d);
+ }
+ } else {
+ // TODO: decide whether we want to capture logic errors
+ //logicError('updateDevice lookup fail. ID = "' + id + '"');
+ }
+ }
+
+ function sendUpdateMeta(d, store) {
+ var metaUi = {},
+ ll;
+
+ // TODO: fix this code to send event to server...
+ //if (store) {
+ // ll = geoMapProj.invert([d.x, d.y]);
+ // metaUi = {
+ // x: d.x,
+ // y: d.y,
+ // lng: ll[0],
+ // lat: ll[1]
+ // };
+ //}
+ //d.metaUi = metaUi;
+ //sendMessage('updateMeta', {
+ // id: d.id,
+ // 'class': d.class,
+ // memento: metaUi
+ //});
+ }
+
+
+ function updateNodes() {
+ $log.debug('TODO updateNodes()...');
+ // TODO...
+ }
+
+ function fStart() {
+ $log.debug('TODO fStart()...');
+ // TODO...
+ }
+
+ function fResume() {
+ $log.debug('TODO fResume()...');
+ // TODO...
+ }
+
+ // ==========================
+ // === Devices and hosts - helper functions
+
+ function coordFromLngLat(loc) {
+ // Our hope is that the projection is installed before we start
+ // handling incoming nodes. But if not, we'll just return the origin.
+ return projection ? projection([loc.lng, loc.lat]) : [0, 0];
+ }
+
+ function positionNode(node, forUpdate) {
+ var meta = node.metaUi,
+ x = meta && meta.x,
+ y = meta && meta.y,
+ xy;
+
+ // If we have [x,y] already, use that...
+ if (x && y) {
+ node.fixed = true;
+ node.px = node.x = x;
+ node.py = node.y = y;
+ return;
+ }
+
+ var location = node.location,
+ coord;
+
+ if (location && location.type === 'latlng') {
+ coord = coordFromLngLat(location);
+ node.fixed = true;
+ node.px = node.x = coord[0];
+ node.py = node.y = coord[1];
+ return true;
+ }
+
+ // if this is a node update (not a node add).. skip randomizer
+ if (forUpdate) {
+ return;
+ }
+
+ // Note: Placing incoming unpinned nodes at exactly the same point
+ // (center of the view) causes them to explode outwards when
+ // the force layout kicks in. So, we spread them out a bit
+ // initially, to provide a more serene layout convergence.
+ // Additionally, if the node is a host, we place it near
+ // the device it is connected to.
+
+ function spread(s) {
+ return Math.floor((Math.random() * s) - s/2);
+ }
+
+ function randDim(dim) {
+ return dim / 2 + spread(dim * 0.7071);
+ }
+
+ function rand() {
+ return {
+ x: randDim(network.view.width()),
+ y: randDim(network.view.height())
+ };
+ }
+
+ function near(node) {
+ var min = 12,
+ dx = spread(12),
+ dy = spread(12);
+ return {
+ x: node.x + min + dx,
+ y: node.y + min + dy
+ };
+ }
+
+ function getDevice(cp) {
+ var d = network.lookup[cp.device];
+ return d || rand();
+ }
+
+ xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+ angular.extend(node, xy);
+ }
+
+ function createDeviceNode(device) {
+ // start with the object as is
+ var node = device,
+ type = device.type,
+ svgCls = type ? 'node device ' + type : 'node device';
+
+ // Augment as needed...
+ node.class = 'device';
+ node.svgClass = device.online ? svgCls + ' online' : svgCls;
+ positionNode(node);
+ return node;
+ }
+
+ // ==========================
+ // === Devices and hosts - D3 rendering
+
+ // Returns the newly computed bounding box of the rectangle
+ function adjustRectToFitText(n) {
+ var text = n.select('text'),
+ box = text.node().getBBox(),
+ lab = labelConfig;
+
+ text.attr('text-anchor', 'middle')
+ .attr('y', '-0.8em')
+ .attr('x', lab.imgPad/2);
+
+ // translate the bbox so that it is centered on [x,y]
+ box.x = -box.width / 2;
+ box.y = -box.height / 2;
+
+ // add padding
+ box.x -= (lab.padLR + lab.imgPad/2);
+ box.width += lab.padLR * 2 + lab.imgPad;
+ box.y -= lab.padTB;
+ box.height += lab.padTB * 2;
+
+ return box;
+ }
+
+ function mkSvgClass(d) {
+ return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+ }
+
+ function hostLabel(d) {
+ var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function deviceLabel(d) {
+ var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
+ return d.labels[idx];
+ }
+ function trimLabel(label) {
+ return (label && label.trim()) || '';
+ }
+
+ function emptyBox() {
+ return {
+ x: -2,
+ y: -2,
+ width: 4,
+ height: 4
+ };
+ }
+
+
+ function updateDeviceLabel(d) {
+ var label = trimLabel(deviceLabel(d)),
+ noLabel = !label,
+ node = d.el,
+ dim = is.iconConfig().device.dim,
+ devCfg = deviceIconConfig,
+ box, dx, dy;
+
+ node.select('text')
+ .text(label)
+ .style('opacity', 0)
+ .transition()
+ .style('opacity', 1);
+
+ if (noLabel) {
+ box = emptyBox();
+ dx = -dim/2;
+ dy = -dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + devCfg.xoff;
+ dy = box.y + devCfg.yoff;
+ }
+
+ node.select('rect')
+ .transition()
+ .attr(box);
+
+ node.select('g.deviceIcon')
+ .transition()
+ .attr('transform', sus.translate(dx, dy));
+ }
+
+ function updateHostLabel(d) {
+ var label = trimLabel(hostLabel(d));
+ d.el.select('text').text(label);
+ }
+
+ function nodeMouseOver(m) {
+ // TODO
+ $log.debug("TODO nodeMouseOver()...", m);
+ }
+
+ function nodeMouseOut(m) {
+ // TODO
+ $log.debug("TODO nodeMouseOut()...", m);
+ }
+
+ function updateDeviceColors(d) {
+ if (d) {
+ setDeviceColor(d);
+ } else {
+ node.filter('.device').each(function (d) {
+ setDeviceColor(d);
+ });
+ }
+ }
+
+ var dCol = {
+ black: '#000',
+ paleblue: '#acf',
+ offwhite: '#ddd',
+ midgrey: '#888',
+ lightgrey: '#bbb',
+ orange: '#f90'
+ };
+
+ // note: these are the device icon colors without affinity
+ var dColTheme = {
+ light: {
+ online: {
+ glyph: dCol.black,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.lightgrey
+ }
+ },
+ // TODO: theme
+ dark: {
+ online: {
+ glyph: dCol.black,
+ rect: dCol.paleblue
+ },
+ offline: {
+ glyph: dCol.midgrey,
+ rect: dCol.lightgrey
+ }
+ }
+ };
+
+ function devBaseColor(d) {
+ var o = d.online ? 'online' : 'offline';
+ return dColTheme[ts.theme()][o];
+ }
+
+ function setDeviceColor(d) {
+ var o = d.online,
+ s = d.el.classed('selected'),
+ c = devBaseColor(d),
+ a = instColor(d.master, o),
+ g, r,
+ icon = d.el.select('g.deviceIcon');
+
+ if (s) {
+ g = c.glyph;
+ r = dCol.orange;
+ } else if (tis.isVisible()) {
+ g = o ? a : c.glyph;
+ r = o ? dCol.offwhite : a;
+ } else {
+ g = c.glyph;
+ r = c.rect;
+ }
+
+ icon.select('use')
+ .style('fill', g);
+ icon.select('rect')
+ .style('fill', r);
+ }
+
+ function instColor(id, online) {
+ return sus.cat7().getColor(id, !online, ts.theme());
+ }
+
+ //============
+
+ function updateNodes() {
+ node = nodeG.selectAll('.node')
+ .data(network.nodes, function (d) { return d.id; });
+
+ // operate on existing nodes...
+ node.filter('.device').each(function (d) {
+ var node = d.el;
+ node.classed('online', d.online);
+ updateDeviceLabel(d);
+ positionNode(d, true);
+ });
+
+ node.filter('.host').each(function (d) {
+ updateHostLabel(d);
+ positionNode(d, true);
+ });
+
+ // operate on entering nodes:
+ var entering = node.enter()
+ .append('g')
+ .attr({
+ id: function (d) { return sus.safeId(d.id); },
+ class: mkSvgClass,
+ transform: function (d) { return sus.translate(d.x, d.y); },
+ opacity: 0
+ })
+ .call(drag)
+ .on('mouseover', nodeMouseOver)
+ .on('mouseout', nodeMouseOut)
+ .transition()
+ .attr('opacity', 1);
+
+ // augment device nodes...
+ entering.filter('.device').each(function (d) {
+ var node = d3.select(this),
+ glyphId = d.type || 'unknown',
+ label = trimLabel(deviceLabel(d)),
+ noLabel = !label,
+ box, dx, dy, icon;
+
+ // provide ref to element from backing data....
+ d.el = node;
+
+ node.append('rect').attr({ rx: 5, ry: 5 });
+ node.append('text').text(label).attr('dy', '1.1em');
+ box = adjustRectToFitText(node);
+ node.select('rect').attr(box);
+
+ icon = is.addDeviceIcon(node, glyphId);
+ d.iconDim = icon.dim;
+
+ if (noLabel) {
+ dx = -icon.dim/2;
+ dy = -icon.dim/2;
+ } else {
+ box = adjustRectToFitText(node);
+ dx = box.x + iconConfig.xoff;
+ dy = box.y + iconConfig.yoff;
+ }
+
+ icon.attr('transform', sus.translate(dx, dy));
+ });
+
+ // augment host nodes...
+ entering.filter('.host').each(function (d) {
+ var node = d3.select(this),
+ cfg = config.icons.host,
+ r = cfg.radius[d.type] || cfg.defaultRadius,
+ textDy = r + 10,
+ //TODO: iid = iconGlyphUrl(d),
+ _dummy;
+
+ // provide ref to element from backing data....
+ d.el = node;
+
+ //TODO: showHostVis(node);
+
+ node.append('circle').attr('r', r);
+ if (iid) {
+ //TODO: addHostIcon(node, r, iid);
+ }
+ node.append('text')
+ .text(hostLabel)
+ .attr('dy', textDy)
+ .attr('text-anchor', 'middle');
+ });
+
+ // operate on both existing and new nodes, if necessary
+ updateDeviceColors();
+
+ // operate on exiting nodes:
+ // Note that the node is removed after 2 seconds.
+ // Sub element animations should be shorter than 2 seconds.
+ var exiting = node.exit()
+ .transition()
+ .duration(2000)
+ .style('opacity', 0)
+ .remove();
+
+ // host node exits....
+ exiting.filter('.host').each(function (d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('text')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.select('circle')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(1500)
+ .attr('r', 0);
+ });
+
+ // device node exits....
+ exiting.filter('.device').each(function (d) {
+ var node = d.el;
+ node.select('use')
+ .style('opacity', 0.5)
+ .transition()
+ .duration(800)
+ .style('opacity', 0);
+
+ node.selectAll('rect')
+ .style('stroke-fill', '#555')
+ .style('fill', '#888')
+ .style('opacity', 0.5);
+ });
+ fResume();
+ }
+
+
+ // ==========================
// force layout tick function
function tick() {
}
+ // ==========================
+ // === MOUSE GESTURE HANDLERS
+
function selectCb() { }
function atDragEnd() {}
function dragEnabled() {}
@@ -84,23 +610,38 @@
// ==========================
+ // Module definition
angular.module('ovTopo')
.factory('TopoForceService',
- ['$log', 'SvgUtilService',
+ ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
+ 'TopoInstService',
- function (_$log_, _sus_) {
+ function (_$log_, _sus_, _is_, _ts_, _tis_) {
$log = _$log_;
sus = _sus_;
+ is = _is_;
+ ts = _ts_;
+ tis = _tis_;
// forceG is the SVG group to display the force layout in
+ // xlink is the cross-link api from the main topo source file
// w, h are the initial dimensions of the SVG
// opts are, well, optional :)
- function initForce(forceG, w, h, opts) {
+ function initForce(forceG, _xlink_, w, h, opts) {
$log.debug('initForce().. WxH = ' + w + 'x' + h);
+ xlink = _xlink_;
settings = angular.extend({}, defaultSettings, opts);
+ // when the projection promise is resolved, cache the projection
+ xlink.projectionPromise.then(
+ function (proj) {
+ projection = proj;
+ $log.debug('** We installed the projection: ', proj);
+ }
+ );
+
linkG = forceG.append('g').attr('id', 'topo-links');
linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
nodeG = forceG.append('g').attr('id', 'topo-nodes');
@@ -127,12 +668,16 @@
function resize(dim) {
force.size([dim.width, dim.height]);
// Review -- do we need to nudge the layout ?
-
}
return {
initForce: initForce,
- resize: resize
+ resize: resize,
+
+ updateDeviceColors: updateDeviceColors,
+
+ addDevice: addDevice,
+ updateDevice: updateDevice
};
}]);
}());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js
index 2ff51af..f68e46d 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoInst.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -325,7 +325,10 @@
destroyInst: destroyInst,
addInstance: addInstance,
updateInstance: updateInstance,
- removeInstance: removeInstance
+ removeInstance: removeInstance,
+ isVisible: function () { return oiBox.isVisible(); },
+ show: function () { oiBox.show(); },
+ hide: function () { oiBox.hide(); }
};
}]);
}());