diff --git a/web/gui/src/main/webapp/geometry2.js b/web/gui/src/main/webapp/geometry2.js new file mode 100644 index 0000000000..5ede643c34 --- /dev/null +++ b/web/gui/src/main/webapp/geometry2.js @@ -0,0 +1,123 @@ +/* + * 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. + */ + +/* + Geometry library - based on work by Mike Bostock. + */ + +(function() { + + if (typeof geo == 'undefined') { + geo = {}; + } + + var tolerance = 1e-10; + + function eq(a, b) { + return (Math.abs(a - b) < tolerance); + } + + function gt(a, b) { + return (a - b > -tolerance); + } + + function lt(a, b) { + return gt(b, a); + } + + geo.eq = eq; + geo.gt = gt; + geo.lt = lt; + + geo.LineSegment = function(x1, y1, x2, y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + + // Ax + By = C + this.a = y2 - y1; + this.b = x1 - x2; + this.c = x1 * this.a + y1 * this.b; + + if (eq(this.a, 0) && eq(this.b, 0)) { + throw new Error( + 'Cannot construct a LineSegment with two equal endpoints.'); + } + }; + + geo.LineSegment.prototype.intersect = function(that) { + var d = (this.x1 - this.x2) * (that.y1 - that.y2) - + (this.y1 - this.y2) * (that.x1 - that.x2); + + if (eq(d, 0)) { + // The two lines are parallel or very close. + return { + x : NaN, + y : NaN + }; + } + + var t1 = this.x1 * this.y2 - this.y1 * this.x2, + t2 = that.x1 * that.y2 - that.y1 * that.x2, + x = (t1 * (that.x1 - that.x2) - t2 * (this.x1 - this.x2)) / d, + y = (t1 * (that.y1 - that.y2) - t2 * (this.y1 - this.y2)) / d, + in1 = (gt(x, Math.min(this.x1, this.x2)) && lt(x, Math.max(this.x1, this.x2)) && + gt(y, Math.min(this.y1, this.y2)) && lt(y, Math.max(this.y1, this.y2))), + in2 = (gt(x, Math.min(that.x1, that.x2)) && lt(x, Math.max(that.x1, that.x2)) && + gt(y, Math.min(that.y1, that.y2)) && lt(y, Math.max(that.y1, that.y2))); + + return { + x : x, + y : y, + in1 : in1, + in2 : in2 + }; + }; + + geo.LineSegment.prototype.x = function(y) { + // x = (C - By) / a; + if (this.a) { + return (this.c - this.b * y) / this.a; + } else { + // a == 0 -> horizontal line + return NaN; + } + }; + + geo.LineSegment.prototype.y = function(x) { + // y = (C - Ax) / b; + if (this.b) { + return (this.c - this.a * x) / this.b; + } else { + // b == 0 -> vertical line + return NaN; + } + }; + + geo.LineSegment.prototype.length = function() { + return Math.sqrt( + (this.y2 - this.y1) * (this.y2 - this.y1) + + (this.x2 - this.x1) * (this.x2 - this.x1)); + }; + + geo.LineSegment.prototype.offset = function(x, y) { + return new geo.LineSegment( + this.x1 + x, this.y1 + y, + this.x2 + x, this.y2 + y); + }; + +})(); diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html new file mode 100644 index 0000000000..4c4a9c8a2e --- /dev/null +++ b/web/gui/src/main/webapp/index2.html @@ -0,0 +1,87 @@ + + + + + + + + ONOS GUI (v1.1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css new file mode 100644 index 0000000000..4502776144 --- /dev/null +++ b/web/gui/src/main/webapp/mast2.css @@ -0,0 +1,69 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Masthead -- CSS file + + @author Simon Hunt + */ + +#mast { + height: 36px; + padding: 4px; + background-color: #bbb; + vertical-align: baseline; + box-shadow: 0px 2px 8px #777; +} + +#mast img#logo { + height: 38px; + padding-left: 8px; + padding-right: 8px; +} + +#mast span.title { + color: #369; + font-size: 14pt; + font-style: italic; + vertical-align: 12px; +} + +#mast span.right { + padding-top: 8px; + padding-right: 16px; + float: right; +} + +#mast span.radio { + color: darkslateblue; + font-size: 10pt; +} + +#mast span.radio { + margin: 4px 0; + border: 1px dotted #222; + padding: 1px 6px; + color: #eee; + cursor: pointer; +} + +#mast span.radio.active { + background-color: #bbb; + border: 1px solid #eee; + padding: 1px 6px; + color: #666; + font-weight: bold; +} diff --git a/web/gui/src/main/webapp/mast2.js b/web/gui/src/main/webapp/mast2.js new file mode 100644 index 0000000000..169dd3589c --- /dev/null +++ b/web/gui/src/main/webapp/mast2.js @@ -0,0 +1,56 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Masthead + + Defines the masthead for the UI. Injects logo and title, as well as providing + the placeholder for a set of radio buttons. + + @author Simon Hunt + */ + +(function (onos){ + 'use strict'; + + // API's + var api = onos.api; + + // Config variables + var guiTitle = 'Open Networking Operating System'; + + // DOM elements and the like + var mast = d3.select('#mast'); + + mast.append('img') + .attr({ + id: 'logo', + src: 'img/onos-logo.png' + }); + + mast.append('span') + .attr({ + class: 'title' + }) + .text(guiTitle); + + mast.append('span') + .attr({ + id: 'mastRadio', + class: 'right' + }); + +}(ONOS)); \ No newline at end of file diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css new file mode 100644 index 0000000000..63acd00ff3 --- /dev/null +++ b/web/gui/src/main/webapp/onos2.css @@ -0,0 +1,204 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Base Framework -- CSS file + + @author Simon Hunt + */ + +html, body { + height: 100%; +} + + +/* + * === DEBUGGING ====== + */ +svg { + /*border: 1px dashed red;*/ +} + +svg #bg { + opacity: 0.5; +} + + +/* + * Network Graph elements ====================================== + */ + +svg .link { + fill: none; + stroke: #666; + stroke-width: 2.0px; + opacity: .7; + + transition: opacity 250ms; + -webkit-transition: opacity 250ms; + -moz-transition: opacity 250ms; +} + +svg .link.host { + stroke: #666; + stroke-width: 1px; +} + +svg g.portLayer rect.port { + fill: #ccc; +} + +svg g.portLayer text { + font: 8pt sans-serif; + pointer-events: none; +} + +svg .node.device rect { + stroke-width: 1.5px; + + transition: opacity 250ms; + -webkit-transition: opacity 250ms; + -moz-transition: opacity 250ms; +} + +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 { + fill: white; + stroke: red; +} +svg .node rect.debug { + fill: yellow; + stroke: red; + opacity: 0.35; +} + + +svg .node.selected rect, +svg .node.selected circle { + filter: url(#blue-glow); +} + +svg .link.inactive, +svg .port.inactive, +svg .portText.inactive, +svg .node.inactive rect, +svg .node.inactive circle, +svg .node.inactive text, +svg .node.inactive image { + opacity: .1; +} + +svg .node.inactive.selected rect, +svg .node.inactive.selected text, +svg .node.inactive.selected image { + opacity: .6; +} + +/* + * ============================================================= + */ + +/* + * Specific structural elements + */ + +/* This is to ensure that the body does not expand to account for the + flyout details pane, that is positioned "off screen". + */ +body { + overflow: hidden; +} + + +#frame { + width: 100%; + height: 100%; + background-color: #fff; +} + +#flyout { + position: absolute; + z-index: 100; + display: block; + top: 10%; + width: 280px; + right: -300px; + opacity: 0; + background-color: rgba(255,255,255,0.8); + + padding: 10px; + color: black; + font-size: 10pt; + box-shadow: 2px 2px 16px #777; +} + +#flyout h2 { + margin: 8px 4px; + color: black; + vertical-align: middle; +} + +#flyout h2 img { + height: 32px; + padding-right: 8px; + vertical-align: middle; +} + +#flyout p, table { + margin: 4px 4px; +} + +#flyout td.label { + font-style: italic; + color: #777; + padding-right: 12px; +} + +#flyout td.value { + +} + +#flyout hr { + height: 1px; + color: #ccc; + background-color: #ccc; + border: 0; +} + diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js new file mode 100644 index 0000000000..bd001a591c --- /dev/null +++ b/web/gui/src/main/webapp/onos2.js @@ -0,0 +1,303 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Base Framework + + @author Simon Hunt + */ + +(function ($) { + 'use strict'; + var tsI = new Date().getTime(), // initialize time stamp + tsB, // build time stamp + defaultHash = 'temp1'; + + + // attach our main function to the jQuery object + $.onos = function (options) { + var publicApi; // public api + + // internal state + var views = {}, + current = { + view: null, + ctx: '' + }, + built = false, + errorCount = 0; + + // DOM elements etc. + var $view; + + + // .......................................................... + // Internal functions + + // throw an error + function throwError(msg) { + // separate function, as we might add tracing here too, later + throw new Error(msg); + } + + function doError(msg) { + errorCount++; + console.warn(msg); + } + + // hash navigation + function hash() { + var hash = window.location.hash, + redo = false, + view, + t; + + if (!hash) { + hash = defaultHash; + redo = true; + } + + t = parseHash(hash); + if (!t || !t.vid) { + doError('Unable to parse target hash: ' + hash); + } + + view = views[t.vid]; + if (!view) { + doError('No view defined with id: ' + t.vid); + } + + if (redo) { + window.location.hash = makeHash(t); + // the above will result in a hashchange event, invoking + // this function again + } else { + // hash was not modified... navigate to where we need to be + navigate(hash, view, t); + } + + } + + function parseHash(s) { + // extract navigation coordinates from the supplied string + // "vid,ctx" --> { vid:vid, ctx:ctx } + + var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s); + if (m) { + return { vid: m[1], ctx: m[2] }; + } + + m = /^[#]{0,1}(\S+)$/.exec(s); + return m ? { vid: m[1] } : null; + } + + function makeHash(t, ctx) { + // make a hash string from the given navigation coordinates. + // if t is not an object, then it is a vid + var h = t, + c = ctx || ''; + + if ($.isPlainObject(t)) { + h = t.vid; + c = t.ctx || ''; + } + + if (c) { + h += ',' + c; + } + return h; + } + + function navigate(hash, view, t) { + // closePanes() // flyouts etc. + // updateNav() // accordion / selected nav item + createView(view); + setView(view, hash, t); + } + + function reportBuildErrors() { + // TODO: validate registered views / nav-item linkage etc. + console.log('(no build errors)'); + } + + // .......................................................... + // View life-cycle functions + + function createView(view) { + var $d; + // lazy initialization of the view + if (view && !view.$div) { + $d = $view.append('div') + .attr({ + id: view.vid + }); + view.$div = $d; // cache a reference to the selected div + } + } + + function setView(view, hash, t) { + // set the specified view as current, while invoking the + // appropriate life-cycle callbacks + + // if there is a current view, and it is not the same as + // the incoming view, then unload it... + if (current.view && !(current.view.vid !== view.vid)) { + current.view.unload(); + } + + // cache new view and context + current.view = view; + current.ctx = t.ctx || ''; + + // TODO: clear radio button set (store on view?) + + // preload is called only once, after the view is in the DOM + if (!view.preloaded) { + view.preload(t.ctx); + } + + // clear the view of stale data + view.reset(); + + // load the view + view.load(t.ctx); + } + + function resizeView() { + if (current.view) { + current.view.resize(); + } + } + + // .......................................................... + // View class + // Captures state information about a view. + + // Constructor + // vid : view id + // nid : id of associated nav-item (optional) + // cb : callbacks (preload, reset, load, resize, unload, error) + // data: custom data object (optional) + function View(vid) { + var av = 'addView(): ', + args = Array.prototype.slice.call(arguments), + nid, + cb, + data; + + args.shift(); // first arg is always vid + if (typeof args[0] === 'string') { // nid specified + nid = args.shift(); + } + cb = args.shift(); + data = args.shift(); + + this.vid = vid; + + if (validateViewArgs(vid)) { + this.nid = nid; // explicit navitem id (can be null) + this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks + this.data = data; // custom data (can be null) + this.$div = null; // view not yet added to DOM + this.ok = true; // valid view + } + + } + + function validateViewArgs(vid) { + var ok = false; + if (typeof vid !== 'string' || !vid) { + doError(av + 'vid required'); + } else if (views[vid]) { + doError(av + 'View ID "' + vid + '" already exists'); + } else { + ok = true; + } + return ok; + } + + var viewInstanceMethods = { + toString: function () { + return '[View: id="' + this.vid + '"]'; + }, + + token: function() { + return { + vid: this.vid, + nid: this.nid, + data: this.data + } + } + // TODO: create, preload, reset, load, error, resize, unload + }; + + // attach instance methods to the view prototype + $.extend(View.prototype, viewInstanceMethods); + + // .......................................................... + // Exported API + + publicApi = { + printTime: function () { + console.log("the time is " + new Date()); + }, + + addView: function (vid, nid, cb, data) { + var view = new View(vid, nid, cb, data), + token; + if (view.ok) { + views[vid] = view; + token = view.token(); + } else { + token = { vid: view.vid, bad: true }; + } + return token; + } + }; + + // function to be called from index.html to build the ONOS UI + function buildOnosUi() { + tsB = new Date().getTime(); + tsI = tsB - tsI; // initialization duration + + console.log('ONOS UI initialized in ' + tsI + 'ms'); + + if (built) { + throwError("ONOS UI already built!"); + } + built = true; + + $view = d3.select('#view'); + + $(window).on('hashchange', hash); + + // Invoke hashchange callback to navigate to content + // indicated by the window location hash. + hash(); + + // If there were any build errors, report them + reportBuildErrors(); + } + + + // export the api and build-UI function + return { + api: publicApi, + buildUi: buildOnosUi + }; + }; + +}(jQuery)); \ No newline at end of file diff --git a/web/gui/src/main/webapp/preamble.js b/web/gui/src/main/webapp/preamble.js new file mode 100644 index 0000000000..8ee8e45462 --- /dev/null +++ b/web/gui/src/main/webapp/preamble.js @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Preamble -- the first thing we do + + @author Simon Hunt + */ + +(function () { + // Check if the URL in the address bar contains a parameter section + // (delineated by '?'). If this is the case, rewrite using '#' instead. + + var m = /([^?]*)\?(.*)/.exec(window.location.href); + if (m) { + window.location.href = m[1] + '#' + m[2]; + } + +}()); diff --git a/web/gui/src/main/webapp/temp2.js b/web/gui/src/main/webapp/temp2.js new file mode 100644 index 0000000000..cc174c022f --- /dev/null +++ b/web/gui/src/main/webapp/temp2.js @@ -0,0 +1,75 @@ +/* + * 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. + */ + +/* + Temporary module file to test the framework integration. + + @author Simon Hunt + */ + +(function (onos) { + 'use strict'; + + var api = onos.api; + + var vid, + svg; + + // == define your functions here..... + + + // NOTE: view is a data structure: + // { + // id: 'view-id', + // el: ... // d3 selection of dom view div. + // } + + function load(view) { + vid = view.id; + svg = view.el.append('svg') + .attr({ + width: 400, + height: 300 + }); + + var fill = (vid === 'temp1') ? 'red' : 'blue', + stroke = (vid === 'temp2') ? 'yellow' : 'black'; + + svg.append('circle') + .attr({ + cx: 200, + cy: 150, + r: 30 + }) + .style({ + fill: fill, + stroke: stroke, + 'stroke-width': 3.5 + }); + } + + // == register views here, with links to lifecycle callbacks + + api.addView('temp1', { + load: load + }); + + api.addView('temp2', { + load: load + }); + + +}(ONOS)); diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css new file mode 100644 index 0000000000..2e058c160d --- /dev/null +++ b/web/gui/src/main/webapp/topo2.css @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* + ONOS GUI -- Topology view -- CSS file + + @author Simon Hunt + */ + diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js new file mode 100644 index 0000000000..b249c09654 --- /dev/null +++ b/web/gui/src/main/webapp/topo2.js @@ -0,0 +1,1222 @@ +/* + * 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. + */ + +/* + ONOS network topology viewer - PoC version 1.0 + + @author Simon Hunt + */ + +(function (onos) { + 'use strict'; + + // reference to the framework api + var api = onos.api; + + // configuration data + var config = { + useLiveData: true, + debugOn: false, + debug: { + showNodeXY: false, + showKeyHandler: true + }, + options: { + layering: true, + collisionPrevention: true, + loadBackground: true + }, + backgroundUrl: 'img/us-map.png', + data: { + live: { + jsonUrl: 'rs/topology/graph', + detailPrefix: 'rs/topology/graph/', + detailSuffix: '' + }, + fake: { + jsonUrl: 'json/network2.json', + detailPrefix: 'json/', + detailSuffix: '.json' + } + }, + iconUrl: { + device: 'img/device.png', + host: 'img/host.png', + pkt: 'img/pkt.png', + opt: 'img/opt.png' + }, + mastHeight: 36, + force: { + note: 'node.class or link.class is used to differentiate', + linkDistance: { + infra: 200, + host: 40 + }, + linkStrength: { + infra: 1.0, + host: 1.0 + }, + charge: { + device: -800, + host: -1000 + }, + ticksWithoutCollisions: 50, + marginLR: 20, + marginTB: 20, + translate: function() { + return 'translate(' + + config.force.marginLR + ',' + + config.force.marginTB + ')'; + } + }, + labels: { + imgPad: 16, + padLR: 8, + padTB: 6, + marginLR: 3, + marginTB: 2, + port: { + gap: 3, + width: 18, + height: 14 + } + }, + icons: { + w: 32, + h: 32, + xoff: -12, + yoff: -8 + }, + constraints: { + ypos: { + host: 0.05, + switch: 0.3, + roadm: 0.7 + } + }, + hostLinkWidth: 1.0, + hostRadius: 7, + mouseOutTimerDelayMs: 120 + }; + + // state variables + var view = {}, + network = {}, + selected = {}, + highlighted = null, + hovered = null, + viewMode = 'showAll', + portLabelsOn = false; + + + function debug(what) { + return config.debugOn && config.debug[what]; + } + + function urlData() { + return config.data[config.useLiveData ? 'live' : 'fake']; + } + + function networkJsonUrl() { + return urlData().jsonUrl; + } + + function safeId(id) { + return id.replace(/[^a-z0-9]/gi, '_'); + } + + function detailJsonUrl(id) { + var u = urlData(), + encId = config.useLiveData ? encodeURIComponent(id) : safeId(id); + return u.detailPrefix + encId + u.detailSuffix; + } + + + // load the topology view of the network + function loadNetworkView() { + // Hey, here I am, calling something on the ONOS api: + api.printTime(); + + resize(); + + // go get our network data from the server... + var url = networkJsonUrl(); + d3.json(url , function (err, data) { + if (err) { + alert('Oops! Error reading JSON...\n\n' + + 'URL: ' + url + '\n\n' + + 'Error: ' + err.message); + return; + } +// console.log("here is the JSON data..."); +// console.log(data); + + network.data = data; + drawNetwork(); + }); + + // while we wait for the data, set up the handlers... + setUpClickHandler(); + setUpRadioButtonHandler(); + setUpKeyHandler(); + $(window).on('resize', resize); + } + + function setUpClickHandler() { + // click handler for "selectable" objects + $(document).on('click', '.select-object', function () { + // when any object of class "select-object" is clicked... + var obj = network.lookup[$(this).data('id')]; + if (obj) { + selectObject(obj); + } + // stop propagation of event (I think) ... + return false; + }); + } + + function setUpRadioButtonHandler() { + d3.selectAll('#displayModes .radio').on('click', function () { + var id = d3.select(this).attr('id'); + if (id !== viewMode) { + radioButton('displayModes', id); + viewMode = id; + doRadioAction(id); + } + }); + } + + function doRadioAction(id) { + showAllLayers(); + if (id === 'showPkt') { + showPacketLayer(); + } else if (id === 'showOpt') { + showOpticalLayer(); + } + } + + function showAllLayers() { + network.node.classed('inactive', false); + network.link.classed('inactive', false); + d3.selectAll('svg .port').classed('inactive', false) + d3.selectAll('svg .portText').classed('inactive', false) + } + + function showPacketLayer() { + network.node.each(function(d) { + // deactivate nodes that are not hosts or switches + if (d.class === 'device' && d.type !== 'switch') { + d3.select(this).classed('inactive', true); + } + }); + + network.link.each(function(lnk) { + // deactivate infrastructure links that have opt's as endpoints + if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') { + d3.select(this).classed('inactive', true); + } + }); + + // deactivate non-packet ports + d3.selectAll('svg .optPort').classed('inactive', true) + } + + function showOpticalLayer() { + network.node.each(function(d) { + // deactivate nodes that are not optical devices + if (d.type !== 'roadm') { + d3.select(this).classed('inactive', true); + } + }); + + network.link.each(function(lnk) { + // deactivate infrastructure links that have opt's as endpoints + if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') { + d3.select(this).classed('inactive', true); + } + }); + + // deactivate non-packet ports + d3.selectAll('svg .pktPort').classed('inactive', true) + } + + function setUpKeyHandler() { + d3.select('body') + .on('keydown', function () { + processKeyEvent(); + if (debug('showKeyHandler')) { + network.svg.append('text') + .attr('x', 5) + .attr('y', 15) + .style('font-size', '20pt') + .text('keyCode: ' + d3.event.keyCode + + ' applied to : ' + contextLabel()) + .transition().duration(2000) + .style('font-size', '2pt') + .style('fill-opacity', 0.01) + .remove(); + } + }); + } + + function contextLabel() { + return hovered === null ? "(nothing)" : hovered.id; + } + + function radioButton(group, id) { + d3.selectAll("#" + group + " .radio").classed("active", false); + d3.select("#" + group + " #" + id).classed("active", true); + } + + function processKeyEvent() { + var code = d3.event.keyCode; + switch (code) { + case 66: // B + toggleBackground(); + break; + case 71: // G + cycleLayout(); + break; + case 76: // L + cycleLabels(); + break; + case 80: // P + togglePorts(); + break; + case 85: // U + unpin(); + break; + } + + } + + function toggleBackground() { + var bg = d3.select('#bg'), + vis = bg.style('visibility'), + newvis = (vis === 'hidden') ? 'visible' : 'hidden'; + bg.style('visibility', newvis); + } + + function cycleLayout() { + config.options.layering = !config.options.layering; + network.force.resume(); + } + + function cycleLabels() { + console.log('Cycle Labels - context = ' + contextLabel()); + } + + function togglePorts() { + portLabelsOn = !portLabelsOn; + var portVis = portLabelsOn ? 'visible' : 'hidden'; + d3.selectAll('.port').style('visibility', portVis); + d3.selectAll('.portText').style('visibility', portVis); + } + + function unpin() { + if (hovered) { + hovered.fixed = false; + findNodeFromData(hovered).classed('fixed', false); + network.force.resume(); + } + console.log('Unpin - context = ' + contextLabel()); + } + + + // ======================================================== + + function drawNetwork() { + $('#view').empty(); + + prepareNodesAndLinks(); + createLayout(); + console.log("\n\nHere is the augmented network object..."); + console.log(network); + } + + function prepareNodesAndLinks() { + network.lookup = {}; + network.nodes = []; + network.links = []; + + var nw = network.forceWidth, + nh = network.forceHeight; + + function yPosConstraintForNode(n) { + return config.constraints.ypos[n.type || 'host']; + } + + // Note that both 'devices' and 'hosts' get mapped into the nodes array + + // first, the devices... + network.data.devices.forEach(function(n) { + var ypc = yPosConstraintForNode(n), + ix = Math.random() * 0.6 * nw + 0.2 * nw, + iy = ypc * nh, + node = { + id: n.id, + labels: n.labels, + class: 'device', + icon: 'device', + type: n.type, + x: ix, + y: iy, + constraint: { + weight: 0.7, + y: iy + } + }; + network.lookup[n.id] = node; + network.nodes.push(node); + }); + + // then, the hosts... + network.data.hosts.forEach(function(n) { + var ypc = yPosConstraintForNode(n), + ix = Math.random() * 0.6 * nw + 0.2 * nw, + iy = ypc * nh, + node = { + id: n.id, + labels: n.labels, + class: 'host', + icon: 'host', + type: n.type, + x: ix, + y: iy, + constraint: { + weight: 0.7, + y: iy + } + }; + network.lookup[n.id] = node; + network.nodes.push(node); + }); + + + // now, process the explicit links... + network.data.links.forEach(function(lnk) { + var src = network.lookup[lnk.src], + dst = network.lookup[lnk.dst], + id = src.id + "-" + dst.id; + + var link = { + class: 'infra', + id: id, + type: lnk.type, + width: lnk.linkWidth, + source: src, + srcPort: lnk.srcPort, + target: dst, + tgtPort: lnk.dstPort, + strength: config.force.linkStrength.infra + }; + network.links.push(link); + }); + + // finally, infer host links... + network.data.hosts.forEach(function(n) { + var src = network.lookup[n.id], + dst = network.lookup[n.cp.device], + id = src.id + "-" + dst.id; + + var link = { + class: 'host', + id: id, + type: 'hostLink', + width: config.hostLinkWidth, + source: src, + target: dst, + strength: config.force.linkStrength.host + }; + network.links.push(link); + }); + } + + function createLayout() { + + var cfg = config.force; + + network.force = d3.layout.force() + .size([network.forceWidth, network.forceHeight]) + .nodes(network.nodes) + .links(network.links) + .linkStrength(function(d) { return cfg.linkStrength[d.class]; }) + .linkDistance(function(d) { return cfg.linkDistance[d.class]; }) + .charge(function(d) { return cfg.charge[d.class]; }) + .on('tick', tick); + + network.svg = d3.select('#view').append('svg') + .attr('width', view.width) + .attr('height', view.height) + .append('g') + .attr('transform', config.force.translate()); +// .attr('id', 'zoomable') +// .call(d3.behavior.zoom().on("zoom", zoomRedraw)); + + network.svg.append('svg:image') + .attr({ + id: 'bg', + width: view.width, + height: view.height, + 'xlink:href': config.backgroundUrl + }) + .style('visibility', + config.options.loadBackground ? 'visible' : 'hidden'); + +// function zoomRedraw() { +// d3.select("#zoomable").attr("transform", +// "translate(" + d3.event.translate + ")" +// + " scale(" + d3.event.scale + ")"); +// } + + // TODO: move glow/blur stuff to util script + var glow = network.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); + + // TODO: legend (and auto adjust on scroll) +// $('#view').on('scroll', function() { +// +// }); + + + // TODO: move drag behavior into separate method. + // == define node drag behavior... + network.draggedThreshold = d3.scale.linear() + .domain([0, 0.1]) + .range([5, 20]) + .clamp(true); + + function dragged(d) { + var threshold = network.draggedThreshold(network.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; + } + + network.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 (!network.force.alpha()) { + network.force.alpha(.025); + } + } + }) + .on('dragend', function(d) { + if (!dragged(d)) { + selectObject(d, this); + } + d.fixed &= ~6; + + // 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(this).classed('fixed', true) + } + }); + + $('#view').on('click', function(e) { + if (!$(e.target).closest('.node').length) { + deselectObject(); + } + }); + + // ............................................................... + + // add links to the display + network.link = network.svg.append('g').attr('id', 'links') + .selectAll('.link') + .data(network.force.links(), function(d) {return d.id}) + .enter().append('line') + .attr('class', function(d) {return 'link ' + d.class}); + + network.linkSrcPort = network.svg.append('g') + .attr({ + id: 'srcPorts', + class: 'portLayer' + }); + network.linkTgtPort = network.svg.append('g') + .attr({ + id: 'tgtPorts', + class: 'portLayer' + }); + + var portVis = portLabelsOn ? 'visible' : 'hidden', + pw = config.labels.port.width, + ph = config.labels.port.height; + + network.link.filter('.infra').each(function(d) { + var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort', + tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort'; + + if (d.source.type) + + network.linkSrcPort.append('rect').attr({ + id: 'srcPort-' + safeId(d.id), + class: 'port ' + srcType, + width: pw, + height: ph, + rx: 4, + ry: 4 + }).style('visibility', portVis); + + network.linkTgtPort.append('rect').attr({ + id: 'tgtPort-' + safeId(d.id), + class: 'port ' + tgtType, + width: pw, + height: ph, + rx: 4, + ry: 4 + }).style('visibility', portVis); + + network.linkSrcPort.append('text').attr({ + id: 'srcText-' + safeId(d.id), + class: 'portText ' + srcType + }).text(d.srcPort) + .style('visibility', portVis); + + network.linkTgtPort.append('text').attr({ + id: 'tgtText-' + safeId(d.id), + class: 'portText ' + tgtType + }).text(d.tgtPort) + .style('visibility', portVis); + }); + + // ............................................................... + + // add nodes to the display + network.node = network.svg.selectAll('.node') + .data(network.force.nodes(), function(d) {return d.id}) + .enter().append('g') + .attr('class', function(d) { + var cls = 'node ' + d.class; + if (d.type) { + cls += ' ' + d.type; + } + return cls; + }) + .attr('transform', function(d) { + return translate(d.x, d.y); + }) + .call(network.drag) + .on('mouseover', function(d) { + // TODO: show tooltip + if (network.mouseoutTimeout) { + clearTimeout(network.mouseoutTimeout); + network.mouseoutTimeout = null; + } + hoverObject(d); + }) + .on('mouseout', function(d) { + // TODO: hide tooltip + if (network.mouseoutTimeout) { + clearTimeout(network.mouseoutTimeout); + network.mouseoutTimeout = null; + } + network.mouseoutTimeout = setTimeout(function() { + hoverObject(null); + }, config.mouseOutTimerDelayMs); + }); + + + // deal with device nodes first + network.nodeRect = network.node.filter('.device') + .append('rect') + .attr({ + rx: 5, + ry: 5, + width: 100, + height: 12 + }); + // note that width/height are adjusted to fit the label text + // then padded, and space made for the icon. + + network.node.filter('.device').each(function(d) { + var node = d3.select(this), + icon = iconUrl(d); + + node.append('text') + // TODO: add label cycle behavior + .text(d.id) + .attr('dy', '1.1em'); + + if (icon) { + var cfg = config.icons; + node.append('svg:image') + .attr({ + width: cfg.w, + height: cfg.h, + 'xlink:href': icon + }); + // note, icon relative positioning (x,y) is done after we have + // adjusted the bounds of the rectangle... + } + + // debug function to show the modelled x,y coordinates of nodes... + if (debug('showNodeXY')) { + node.select('rect').attr('fill-opacity', 0.5); + node.append('circle') + .attr({ + class: 'debug', + cx: 0, + cy: 0, + r: '3px' + }); + } + }); + + // now process host nodes + network.nodeCircle = network.node.filter('.host') + .append('circle') + .attr({ + r: config.hostRadius + }); + + network.node.filter('.host').each(function(d) { + var node = d3.select(this), + icon = iconUrl(d); + + // debug function to show the modelled x,y coordinates of nodes... + if (debug('showNodeXY')) { + node.select('circle').attr('fill-opacity', 0.5); + node.append('circle') + .attr({ + class: 'debug', + cx: 0, + cy: 0, + r: '3px' + }); + } + }); + + // this function is scheduled to happen soon after the given thread ends + setTimeout(function() { + var lab = config.labels, + portGap = lab.port.gap, + midW = portGap + lab.port.width/ 2, + midH = portGap + lab.port.height / 2; + + // post process the device nodes, to pad their size to fit the + // label text and attach the icon to the right location. + network.node.filter('.device').each(function(d) { + // for every node, recompute size, padding, etc. so text fits + var node = d3.select(this), + text = node.select('text'), + box = adjustRectToFitText(node); + + // now make the computed adjustment + node.select('rect') + .attr(box); + + node.select('image') + .attr('x', box.x + config.icons.xoff) + .attr('y', box.y + config.icons.yoff); + + var bounds = boundsFromBox(box), + portBounds = { + x1: bounds.x1 - midW, + x2: bounds.x2 + midW, + y1: bounds.y1 - midH, + y2: bounds.y2 + midH + }; + + // todo: clean up extent and edge work.. + d.extent = { + left: bounds.x1 - lab.marginLR, + right: bounds.x2 + lab.marginLR, + top: bounds.y1 - lab.marginTB, + bottom: bounds.y2 + lab.marginTB + }; + + d.edge = { + left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2), + right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2), + top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1), + bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2) + }; + + d.portEdge = { + left : new geo.LineSegment( + portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2 + ), + right : new geo.LineSegment( + portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2 + ), + top : new geo.LineSegment( + portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1 + ), + bottom : new geo.LineSegment( + portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2 + ) + }; + + }); + + network.numTicks = 0; + network.preventCollisions = false; + network.force.start(); + for (var i = 0; i < config.force.ticksWithoutCollisions; i++) { + network.force.tick(); + } + network.preventCollisions = true; + $('#view').css('visibility', 'visible'); + }); + + + // returns the newly computed bounding box of the rectangle + function adjustRectToFitText(n) { + var text = n.select('text'), + box = text.node().getBBox(), + lab = config.labels; + + // not sure why n.data() returns an array of 1 element... + var data = n.data()[0]; + + 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 boundsFromBox(box) { + return { + x1: box.x, + y1: box.y, + x2: box.x + box.width, + y2: box.y + box.height + }; + } + + } + + function iconUrl(d) { + return 'img/' + d.type + '.png'; +// return config.iconUrl[d.icon]; + } + + function translate(x, y) { + return 'translate(' + x + ',' + y + ')'; + } + + // prevents collisions amongst device nodes + function preventCollisions() { + var quadtree = d3.geom.quadtree(network.nodes), + hrad = config.hostRadius; + + network.nodes.forEach(function(n) { + var nx1, nx2, ny1, ny2; + + if (n.class === 'device') { + nx1 = n.x + n.extent.left; + nx2 = n.x + n.extent.right; + ny1 = n.y + n.extent.top; + ny2 = n.y + n.extent.bottom; + + } else { + nx1 = n.x - hrad; + nx2 = n.x + hrad; + ny1 = n.y - hrad; + ny2 = n.y + hrad; + } + + quadtree.visit(function(quad, x1, y1, x2, y2) { + if (quad.point && quad.point !== n) { + // check if the rectangles/circles intersect + var p = quad.point, + px1, px2, py1, py2, ix; + + if (p.class === 'device') { + px1 = p.x + p.extent.left; + px2 = p.x + p.extent.right; + py1 = p.y + p.extent.top; + py2 = p.y + p.extent.bottom; + + } else { + px1 = p.x - hrad; + px2 = p.x + hrad; + py1 = p.y - hrad; + py2 = p.y + hrad; + } + + ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2); + + if (ix) { + var xa1 = nx2 - px1, // shift n left , p right + xa2 = px2 - nx1, // shift n right, p left + ya1 = ny2 - py1, // shift n up , p down + ya2 = py2 - ny1, // shift n down , p up + adj = Math.min(xa1, xa2, ya1, ya2); + + if (adj == xa1) { + n.x -= adj / 2; + p.x += adj / 2; + } else if (adj == xa2) { + n.x += adj / 2; + p.x -= adj / 2; + } else if (adj == ya1) { + n.y -= adj / 2; + p.y += adj / 2; + } else if (adj == ya2) { + n.y += adj / 2; + p.y -= adj / 2; + } + } + return ix; + } + }); + + }); + } + + function tick(e) { + network.numTicks++; + + if (config.options.layering) { + // adjust the y-coord of each node, based on y-pos constraints + network.nodes.forEach(function (n) { + var z = e.alpha * n.constraint.weight; + if (!isNaN(n.constraint.y)) { + n.y = (n.constraint.y * z + n.y * (1 - z)); + } + }); + } + + if (config.options.collisionPrevention && network.preventCollisions) { + preventCollisions(); + } + + var portHalfW = config.labels.port.width / 2, + portHalfH = config.labels.port.height / 2; + + // clip visualization of links at bounds of nodes... + network.link.each(function(d) { + var xs = d.source.x, + ys = d.source.y, + xt = d.target.x, + yt = d.target.y, + line = new geo.LineSegment(xs, ys, xt, yt), + e, ix, + exs, eys, ext, eyt, + pxs, pys, pxt, pyt; + + if (d.class === 'host') { + // no adjustment for source end of link, since hosts are dots + exs = xs; + eys = ys; + + } else { + for (e in d.source.edge) { + ix = line.intersect(d.source.edge[e].offset(xs, ys)); + if (ix.in1 && ix.in2) { + exs = ix.x; + eys = ix.y; + + // also pick off the port label intersection + ix = line.intersect(d.source.portEdge[e].offset(xs, ys)); + pxs = ix.x; + pys = ix.y; + break; + } + } + } + + for (e in d.target.edge) { + ix = line.intersect(d.target.edge[e].offset(xt, yt)); + if (ix.in1 && ix.in2) { + ext = ix.x; + eyt = ix.y; + + // also pick off the port label intersection + ix = line.intersect(d.target.portEdge[e].offset(xt, yt)); + pxt = ix.x; + pyt = ix.y; + break; + } + } + + // adjust the endpoints of the link's line to match rectangles + var sid = safeId(d.id); + d3.select(this) + .attr('x1', exs) + .attr('y1', eys) + .attr('x2', ext) + .attr('y2', eyt); + + d3.select('#srcPort-' + sid) + .attr('x', pxs - portHalfW) + .attr('y', pys - portHalfH); + + d3.select('#tgtPort-' + sid) + .attr('x', pxt - portHalfW) + .attr('y', pyt - portHalfH); + + // TODO: fit label rect to size of port number. + d3.select('#srcText-' + sid) + .attr('x', pxs - 5) + .attr('y', pys + 3); + + d3.select('#tgtText-' + sid) + .attr('x', pxt - 5) + .attr('y', pyt + 3); + + }); + + // position each node by translating the node (group) by x,y + network.node + .attr('transform', function(d) { + return translate(d.x, d.y); + }); + + } + + // $('#docs-close').on('click', function() { + // deselectObject(); + // return false; + // }); + + // $(document).on('click', '.select-object', function() { + // var obj = graph.data[$(this).data('name')]; + // if (obj) { + // selectObject(obj); + // } + // return false; + // }); + + function findNodeFromData(d) { + var el = null; + network.node.filter('.' + d.class).each(function(n) { + if (n.id === d.id) { + el = d3.select(this); + } + }); + return el; + } + + function selectObject(obj, el) { + var node; + if (el) { + node = d3.select(el); + } else { + network.node.each(function(d) { + if (d == obj) { + node = d3.select(el = this); + } + }); + } + if (!node) return; + + if (node.classed('selected')) { + deselectObject(); + flyinPane(null); + return; + } + deselectObject(false); + + selected = { + obj : obj, + el : el + }; + + node.classed('selected', true); + flyinPane(obj); + } + + function deselectObject(doResize) { + // Review: logic of 'resize(...)' function. + if (doResize || typeof doResize == 'undefined') { + resize(false); + } + + // deselect all nodes in the network... + network.node.classed('selected', false); + selected = {}; + flyinPane(null); + } + + function flyinPane(obj) { + var pane = d3.select('#flyout'), + url; + + if (obj) { + // go get details of the selected object from the server... + url = detailJsonUrl(obj.id); + d3.json(url, function (err, data) { + if (err) { + alert('Oops! Error reading JSON...\n\n' + + 'URL: ' + url + '\n\n' + + 'Error: ' + err.message); + return; + } +// console.log("JSON data... " + url); +// console.log(data); + + displayDetails(data, pane); + }); + + } else { + // hide pane + pane.transition().duration(750) + .style('right', '-320px') + .style('opacity', 0.0); + } + } + + function displayDetails(data, pane) { + $('#flyout').empty(); + + var title = pane.append("h2"), + table = pane.append("table"), + tbody = table.append("tbody"); + + $('').appendTo(title); + $('').attr('class', 'icon').text(data.id).appendTo(title); + + + // TODO: consider using d3 data bind to TR/TD + + data.propOrder.forEach(function(p) { + if (p === '-') { + addSep(tbody); + } else { + addProp(tbody, p, data.props[p]); + } + }); + + function addSep(tbody) { + var tr = tbody.append('tr'); + $('
').appendTo(tr.append('td').attr('colspan', 2)); + } + + function addProp(tbody, label, value) { + var tr = tbody.append('tr'); + + tr.append('td') + .attr('class', 'label') + .text(label + ' :'); + + tr.append('td') + .attr('class', 'value') + .text(value); + } + + // show pane + pane.transition().duration(750) + .style('right', '20px') + .style('opacity', 1.0); + } + + function highlightObject(obj) { + if (obj) { + if (obj != highlighted) { + // TODO set or clear "inactive" class on nodes, based on criteria + network.node.classed('inactive', function(d) { + // return (obj !== d && + // d.relation(obj.id)); + return (obj !== d); + }); + // TODO: same with links + network.link.classed('inactive', function(d) { + return (obj !== d.source && obj !== d.target); + }); + } + highlighted = obj; + } else { + if (highlighted) { + // clear the inactive flag (no longer suppressed visually) + network.node.classed('inactive', false); + network.link.classed('inactive', false); + } + highlighted = null; + + } + } + + function hoverObject(obj) { + if (obj) { + hovered = obj; + } else { + if (hovered) { + hovered = null; + } + } + } + + + function resize() { + view.height = window.innerHeight - config.mastHeight; + view.width = window.innerWidth; + $('#view') + .css('height', view.height + 'px') + .css('width', view.width + 'px'); + + network.forceWidth = view.width - config.force.marginLR; + network.forceHeight = view.height - config.force.marginTB; + } + + // ====================================================================== + // register with the UI framework + + api.addView('network', { + load: loadNetworkView + }); + + +}(ONOS)); +