Simon Hunt

GUI -- Test events: unpinned the first node; increased a few link widths.

 - added alerts pane to framework.
 - added library registration mechanism to framework.
 - created d3Utils library
 - reimplemented drag behavior of nodes.

Change-Id: I501f4ab6eded8393948cede903573580599258b1
/*
* Copyright 2014 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
Utility functions for D3 visualizations.
@author Simon Hunt
*/
(function (onos) {
'use strict';
function createDragBehavior(force, selectCb, atDragEnd) {
var draggedThreshold = d3.scale.linear()
.domain([0, 0.1])
.range([5, 20])
.clamp(true),
drag;
// TODO: better validation of parameters
if (!$.isFunction(selectCb)) {
alert('d3util.createDragBehavior(): selectCb is not a function')
}
if (!$.isFunction(atDragEnd)) {
alert('d3util.createDragBehavior(): atDragEnd is not a function')
}
function dragged(d) {
var threshold = draggedThreshold(force.alpha()),
dx = d.oldX - d.px,
dy = d.oldY - d.py;
if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
d.dragged = true;
}
return d.dragged;
}
drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on('dragstart', function(d) {
d.oldX = d.x;
d.oldY = d.y;
d.dragged = false;
d.fixed |= 2;
})
.on('drag', function(d) {
d.px = d3.event.x;
d.py = d3.event.y;
if (dragged(d)) {
if (!force.alpha()) {
force.alpha(.025);
}
}
})
.on('dragend', function(d) {
if (!dragged(d)) {
// consider this the same as a 'click' (selection of node)
selectCb(d, this); // TODO: set 'this' context instead of param
}
d.fixed &= ~6;
// hook at the end of a drag gesture
atDragEnd(d, this); // TODO: set 'this' context instead of param
});
return drag;
}
function appendGlow(svg) {
// TODO: parameterize color
var glow = svg.append('filter')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%')
.attr('id', 'blue-glow');
glow.append('feColorMatrix')
.attr('type', 'matrix')
.attr('values', '0 0 0 0 0 ' +
'0 0 0 0 0 ' +
'0 0 0 0 .7 ' +
'0 0 0 1 0 ');
glow.append('feGaussianBlur')
.attr('stdDeviation', 3)
.attr('result', 'coloredBlur');
glow.append('feMerge').selectAll('feMergeNode')
.data(['coloredBlur', 'SourceGraphic'])
.enter().append('feMergeNode')
.attr('in', String);
}
// === register the functions as a library
onos.ui.addLib('d3util', {
createDragBehavior: createDragBehavior,
appendGlow: appendGlow
});
}(ONOS));
......@@ -64,6 +64,9 @@
<div id="overlays">
<!-- NOTE: overlays injected here, as needed -->
</div>
<div id="alerts">
<!-- NOTE: alert content injected here, as needed -->
</div>
</div>
<!-- Initialize the UI...-->
......@@ -76,6 +79,9 @@
});
</script>
<!-- Library module files included here -->
<script src="d3Utils.js"></script>
<!-- Framework module files included here -->
<script src="mast2.js"></script>
......
......@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
"x": 832,
"y": 223
"Zx": 832,
"Zy": 223
}
}
}
......
......@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
"x": 840,
"y": 290
"Zx": 840,
"Zy": 290
}
}
}
......
......@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "10",
"type": "optical",
"linkWidth": 2,
"linkWidth": 6,
"props" : {
"BW": "80 G"
}
......
......@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "30",
"type": "optical",
"linkWidth": 2,
"linkWidth": 6,
"props" : {
"BW": "70 G"
}
......
......@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "20",
"type": "optical",
"linkWidth": 2,
"linkWidth": 6,
"props" : {
"BW": "70 G"
}
......
......@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "30",
"type": "optical",
"linkWidth": 2,
"linkWidth": 6,
"props" : {
"BW": "70 G"
}
......
......@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "10",
"type": "optical",
"linkWidth": 2,
"linkWidth": 6,
"props" : {
"BW": "70 G"
}
......
{
"event": "addLink",
"payload": {
"src": "of:0000ffffffffff04",
"srcPort": "27",
"dst": "of:0000ffffffffff08",
"dstPort": "10",
"type": "optical",
"linkWidth": 2,
"props" : {
"BW": "30 G"
}
}
}
......@@ -32,6 +32,34 @@ div.onosView.currentView {
display: block;
}
div#alerts {
display: none;
position: absolute;
z-index: 2000;
opacity: 0.65;
background-color: #006;
color: white;
top: 80px;
left: 40px;
padding: 3px 6px;
box-shadow: 4px 6px 12px #777;
}
div#alerts pre {
margin: 0.2em 6px;
}
div#alerts span.close {
color: #6af;
float: right;
right: 2px;
cursor: pointer;
}
div#alerts span.close:hover {
color: #fff;
}
/*
* ==============================================================
* END OF NEW ONOS.JS file
......@@ -54,12 +82,6 @@ svg #bg {
* Network Graph elements ======================================
*/
svg .link {
opacity: .7;
}
svg .link.host {
}
svg g.portLayer rect.port {
fill: #ccc;
......@@ -70,33 +92,13 @@ svg g.portLayer text {
pointer-events: none;
}
svg .node.device rect {
stroke-width: 1.5px;
}
svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
}
svg .node.device.roadm rect {
fill: #03c;
}
svg .node.device.switch rect {
fill: #06f;
}
svg .node.host circle {
fill: #c96;
stroke: #000;
}
svg .node text {
fill: white;
font: 10pt sans-serif;
pointer-events: none;
}
/* for debugging */
svg .node circle.debug {
......@@ -110,10 +112,6 @@ svg .node rect.debug {
}
svg .node.selected rect,
svg .node.selected circle {
filter: url(#blue-glow);
}
svg .link.inactive,
svg .port.inactive,
......
......@@ -32,7 +32,8 @@
$.onos = function (options) {
var uiApi,
viewApi,
navApi;
navApi,
libApi;
var defaultOptions = {
trace: false,
......@@ -331,6 +332,58 @@
}
}
var alerts = {
open: false,
count: 0
};
function createAlerts() {
var al = d3.select('#alerts')
.style('display', 'block');
al.append('span')
.attr('class', 'close')
.text('X')
.on('click', closeAlerts);
al.append('pre');
alerts.open = true;
alerts.count = 0;
}
function closeAlerts() {
d3.select('#alerts')
.style('display', 'none');
d3.select('#alerts span').remove();
d3.select('#alerts pre').remove();
alerts.open = false;
}
function addAlert(msg) {
var lines,
oldContent;
if (alerts.count) {
oldContent = d3.select('#alerts pre').html();
}
lines = msg.split('\n');
lines[0] += ' '; // spacing so we don't crowd 'X'
lines = lines.join('\n');
if (oldContent) {
lines += '\n----\n' + oldContent;
}
d3.select('#alerts pre').html(lines);
alerts.count++;
}
function doAlert(msg) {
if (!alerts.open) {
createAlerts();
}
addAlert(msg);
}
function keyIn() {
var event = d3.event,
keyCode = event.keyCode,
......@@ -408,7 +461,8 @@
uid: this.uid,
setRadio: this.setRadio,
setKeys: this.setKeys,
dataLoadError: this.dataLoadError
dataLoadError: this.dataLoadError,
alert: this.alert
}
},
......@@ -501,14 +555,20 @@
return makeUid(this, id);
},
// TODO : implement custom dialogs (don't use alerts)
// TODO : implement custom dialogs
// Consider enhancing alert mechanism to handle multiples
// as individually closable.
alert: function (msg) {
doAlert(msg);
},
dataLoadError: function (err, url) {
var msg = 'Data Load Error\n\n' +
err.status + ' -- ' + err.statusText + '\n\n' +
'relative-url: "' + url + '"\n\n' +
'complete-url: "' + err.responseURL + '"';
alert(msg);
this.alert(msg);
}
// TODO: consider schedule, clearTimer, etc.
......@@ -521,6 +581,12 @@
// UI API
uiApi = {
addLib: function (libName, api) {
// TODO: validation of args
libApi[libName] = api;
},
// TODO: it remains to be seen whether we keep this style of docs
/** @api ui addView( vid, nid, cb )
* Adds a view to the UI.
* <p>
......@@ -590,6 +656,12 @@
};
// ..........................................................
// Library API
libApi = {
};
// ..........................................................
// Exported API
// function to be called from index.html to build the ONOS UI
......@@ -623,7 +695,8 @@
// export the api and build-UI function
return {
ui: uiApi,
view: viewApi,
lib: libApi,
//view: viewApi,
nav: navApi,
buildUi: buildOnosUi
};
......
......@@ -24,12 +24,18 @@ svg #topo-bg {
opacity: 0.5;
}
/* NODES */
svg .node.device {
stroke: none;
stroke-width: 1.5px;
cursor: pointer;
}
svg .node.device rect {
stroke-width: 1.5px;
}
svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
......@@ -50,6 +56,17 @@ svg .node text {
pointer-events: none;
}
svg .node.selected rect,
svg .node.selected circle {
filter: url(#blue-glow);
}
/* LINKS */
svg .link {
opacity: .7;
}
/* for debugging */
svg .node circle.debug {
fill: white;
......
......@@ -23,6 +23,9 @@
(function (onos) {
'use strict';
// shorter names for library APIs
var d3u = onos.lib.d3util;
// configuration data
var config = {
useLiveData: false,
......@@ -61,6 +64,10 @@
height: 14
}
},
topo: {
linkInColor: '#66f',
linkInWidth: 14
},
icons: {
w: 28,
h: 28,
......@@ -106,7 +113,9 @@
// key bindings
var keyDispatch = {
space: injectTestEvent, // TODO: remove (testing only)
// M: testMe, // TODO: remove (testing only)
S: injectStartupEvents, // TODO: remove (testing only)
A: testAlert, // TODO: remove (testing only)
M: testMe, // TODO: remove (testing only)
B: toggleBg,
G: toggleLayout,
......@@ -141,7 +150,8 @@
// For Debugging / Development
var eventPrefix = 'json/eventTest_',
eventNumber = 0;
eventNumber = 0,
alertNumber = 0;
function note(label, msg) {
console.log('NOTE: ' + label + ': ' + msg);
......@@ -155,22 +165,12 @@
// ==============================
// Key Callbacks
function testAlert(view) {
alertNumber++;
view.alert("Test me! -- " + alertNumber);
}
function testMe(view) {
svg.append('line')
.attr({
x1: 100,
y1: 100,
x2: 500,
y2: 400,
stroke: '#2f3',
'stroke-width': 8
})
.transition()
.duration(1200)
.attr({
stroke: '#666',
'stroke-width': 6
});
}
function injectTestEvent(view) {
......@@ -187,6 +187,13 @@
});
}
function injectStartupEvents(view) {
var lastStartupEvent = 32;
while (eventNumber < lastStartupEvent) {
injectTestEvent(view);
}
}
function toggleBg() {
var vis = bgImg.style('visibility');
bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
......@@ -370,6 +377,11 @@
return lnk;
}
function linkWidth(w) {
// w is number of links between nodes. Scale appropriately.
return w * 1.2;
}
function updateLinks() {
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.id; });
......@@ -387,12 +399,12 @@
y1: function (d) { return d.y1; },
x2: function (d) { return d.x2; },
y2: function (d) { return d.y2; },
stroke: '#66f',
'stroke-width': 10
stroke: config.topo.linkInColor,
'stroke-width': config.topo.linkInWidth
})
.transition().duration(1000)
.attr({
'stroke-width': function (d) { return d.width; },
'stroke-width': function (d) { return linkWidth(d.width); },
stroke: '#666' // TODO: remove explicit stroke, rather...
});
......@@ -461,6 +473,10 @@
return box;
}
function mkSvgClass(d) {
return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
}
function updateNodes() {
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
......@@ -473,11 +489,11 @@
.append('g')
.attr({
id: function (d) { return safeId(d.id); },
class: function (d) { return d.svgClass; },
class: mkSvgClass,
transform: function (d) { return translate(d.x, d.y); },
opacity: 0
})
//.call(network.drag)
.call(network.drag)
//.on('mouseover', function (d) {})
//.on('mouseover', function (d) {})
.transition()
......@@ -578,6 +594,9 @@
svg = view.$div.append('svg');
setSize(svg, view);
// add blue glow filter to svg layer
d3u.appendGlow(svg);
// load the background image
bgImg = svg.append('svg:image')
.attr({
......@@ -612,6 +631,20 @@
return fcfg.charge[d.class] || -200;
}
function selectCb(d, self) {
// TODO: selectObject(d, self);
}
function atDragEnd(d, self) {
// once we've finished moving, pin the node in position,
// if it is a device (not a host)
if (d.class === 'device') {
d.fixed = true;
d3.select(self).classed('fixed', true)
// TODO: send new [x,y] back to server, via websocket.
}
}
// set up the force layout
network.force = d3.layout.force()
.size(forceDim)
......@@ -621,8 +654,9 @@
.linkDistance(ldist)
.linkStrength(lstrg)
.on('tick', tick);
}
network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
}
function load(view, ctx) {
// cache the view token, so network topo functions can access it
......