Simon Hunt

GUI -- Starting migration of topology view to the updated framework. WIP.

...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
70 <script type="text/javascript"> 70 <script type="text/javascript">
71 var ONOS = $.onos({ 71 var ONOS = $.onos({
72 comment: "configuration options", 72 comment: "configuration options",
73 + startVid: 'topo',
73 trace: false 74 trace: false
74 }); 75 });
75 </script> 76 </script>
...@@ -77,12 +78,15 @@ ...@@ -77,12 +78,15 @@
77 <!-- Framework module files included here --> 78 <!-- Framework module files included here -->
78 <script src="mast2.js"></script> 79 <script src="mast2.js"></script>
79 80
80 - <!-- Contributed (application) views injected here --> 81 + <!-- Sample views; can be dispensed with eventually -->
81 - <!-- TODO: replace with template marker and inject refs server-side -->
82 <script src="sample2.js"></script> 82 <script src="sample2.js"></script>
83 <script src="sampleAlt2.js"></script> 83 <script src="sampleAlt2.js"></script>
84 <script src="sampleRadio.js"></script> 84 <script src="sampleRadio.js"></script>
85 85
86 + <!-- Contributed (application) views injected here -->
87 + <!-- TODO: replace with template marker and inject refs server-side -->
88 + <script src="topo2.js"></script>
89 +
86 <!-- finally, build the UI--> 90 <!-- finally, build the UI-->
87 <script type="text/javascript"> 91 <script type="text/javascript">
88 $(ONOS.buildUi); 92 $(ONOS.buildUi);
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 var tsI = new Date().getTime(), // initialize time stamp 25 var tsI = new Date().getTime(), // initialize time stamp
26 tsB, // build time stamp 26 tsB, // build time stamp
27 mastHeight = 36, // see mast2.css 27 mastHeight = 36, // see mast2.css
28 - defaultHash = 'sample'; 28 + defaultVid = 'sample';
29 29
30 30
31 // attach our main function to the jQuery object 31 // attach our main function to the jQuery object
...@@ -35,7 +35,8 @@ ...@@ -35,7 +35,8 @@
35 navApi; 35 navApi;
36 36
37 var defaultOptions = { 37 var defaultOptions = {
38 - trace: false 38 + trace: false,
39 + startVid: defaultVid
39 }; 40 };
40 41
41 // compute runtime settings 42 // compute runtime settings
...@@ -91,7 +92,7 @@ ...@@ -91,7 +92,7 @@
91 traceFn('hash', hash); 92 traceFn('hash', hash);
92 93
93 if (!hash) { 94 if (!hash) {
94 - hash = defaultHash; 95 + hash = settings.startVid;
95 redo = true; 96 redo = true;
96 } 97 }
97 98
...@@ -336,10 +337,6 @@ ...@@ -336,10 +337,6 @@
336 } 337 }
337 338
338 var viewInstanceMethods = { 339 var viewInstanceMethods = {
339 - toString: function () {
340 - return '[View: id="' + this.vid + '"]';
341 - },
342 -
343 token: function () { 340 token: function () {
344 return { 341 return {
345 // attributes 342 // attributes
...@@ -350,6 +347,7 @@ ...@@ -350,6 +347,7 @@
350 // functions 347 // functions
351 width: this.width, 348 width: this.width,
352 height: this.height, 349 height: this.height,
350 + uid: this.uid,
353 setRadio: this.setRadio 351 setRadio: this.setRadio
354 } 352 }
355 }, 353 },
...@@ -433,6 +431,10 @@ ...@@ -433,6 +431,10 @@
433 431
434 setRadio: function (btnSet, cb) { 432 setRadio: function (btnSet, cb) {
435 setRadioButtons(this.vid, btnSet, cb); 433 setRadioButtons(this.vid, btnSet, cb);
434 + },
435 +
436 + uid: function (id) {
437 + return uid(this, id);
436 } 438 }
437 439
438 // TODO: consider schedule, clearTimer, etc. 440 // TODO: consider schedule, clearTimer, etc.
......
1 +/*
2 + * Copyright 2014 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +
17 +/*
18 + ONOS network topology viewer - PoC version 1.0
19 +
20 + @author Simon Hunt
21 + */
22 +
23 +(function (onos) {
24 + 'use strict';
25 +
26 + // configuration data
27 + var config = {
28 + useLiveData: true,
29 + debugOn: false,
30 + debug: {
31 + showNodeXY: false,
32 + showKeyHandler: true
33 + },
34 + options: {
35 + layering: true,
36 + collisionPrevention: true,
37 + loadBackground: true
38 + },
39 + backgroundUrl: 'img/us-map.png',
40 + data: {
41 + live: {
42 + jsonUrl: 'rs/topology/graph',
43 + detailPrefix: 'rs/topology/graph/',
44 + detailSuffix: ''
45 + },
46 + fake: {
47 + jsonUrl: 'json/network2.json',
48 + detailPrefix: 'json/',
49 + detailSuffix: '.json'
50 + }
51 + },
52 + iconUrl: {
53 + device: 'img/device.png',
54 + host: 'img/host.png',
55 + pkt: 'img/pkt.png',
56 + opt: 'img/opt.png'
57 + },
58 + mastHeight: 36,
59 + force: {
60 + note: 'node.class or link.class is used to differentiate',
61 + linkDistance: {
62 + infra: 200,
63 + host: 40
64 + },
65 + linkStrength: {
66 + infra: 1.0,
67 + host: 1.0
68 + },
69 + charge: {
70 + device: -800,
71 + host: -1000
72 + },
73 + ticksWithoutCollisions: 50,
74 + marginLR: 20,
75 + marginTB: 20,
76 + translate: function() {
77 + return 'translate(' +
78 + config.force.marginLR + ',' +
79 + config.force.marginTB + ')';
80 + }
81 + },
82 + labels: {
83 + imgPad: 16,
84 + padLR: 8,
85 + padTB: 6,
86 + marginLR: 3,
87 + marginTB: 2,
88 + port: {
89 + gap: 3,
90 + width: 18,
91 + height: 14
92 + }
93 + },
94 + icons: {
95 + w: 32,
96 + h: 32,
97 + xoff: -12,
98 + yoff: -8
99 + },
100 + constraints: {
101 + ypos: {
102 + host: 0.05,
103 + switch: 0.3,
104 + roadm: 0.7
105 + }
106 + },
107 + hostLinkWidth: 1.0,
108 + hostRadius: 7,
109 + mouseOutTimerDelayMs: 120
110 + };
111 +
112 + // state variables
113 + var netView = {},
114 + network = {},
115 + selected = {},
116 + highlighted = null,
117 + hovered = null,
118 + viewMode = 'showAll',
119 + portLabelsOn = false;
120 +
121 +
122 + function debug(what) {
123 + return config.debugOn && config.debug[what];
124 + }
125 +
126 + function urlData() {
127 + return config.data[config.useLiveData ? 'live' : 'fake'];
128 + }
129 +
130 + function networkJsonUrl() {
131 + return urlData().jsonUrl;
132 + }
133 +
134 + function safeId(id) {
135 + return id.replace(/[^a-z0-9]/gi, '_');
136 + }
137 +
138 + function detailJsonUrl(id) {
139 + var u = urlData(),
140 + encId = config.useLiveData ? encodeURIComponent(id) : safeId(id);
141 + return u.detailPrefix + encId + u.detailSuffix;
142 + }
143 +
144 +
145 + // load the topology view of the network
146 + function loadNetworkView() {
147 + // Hey, here I am, calling something on the ONOS api:
148 + api.printTime();
149 +
150 + resize();
151 +
152 + // go get our network data from the server...
153 + var url = networkJsonUrl();
154 + d3.json(url , function (err, data) {
155 + if (err) {
156 + alert('Oops! Error reading JSON...\n\n' +
157 + 'URL: ' + url + '\n\n' +
158 + 'Error: ' + err.message);
159 + return;
160 + }
161 +// console.log("here is the JSON data...");
162 +// console.log(data);
163 +
164 + network.data = data;
165 + drawNetwork();
166 + });
167 +
168 + // while we wait for the data, set up the handlers...
169 + setUpClickHandler();
170 + setUpRadioButtonHandler();
171 + setUpKeyHandler();
172 + $(window).on('resize', resize);
173 + }
174 +
175 + function setUpClickHandler() {
176 + // click handler for "selectable" objects
177 + $(document).on('click', '.select-object', function () {
178 + // when any object of class "select-object" is clicked...
179 + var obj = network.lookup[$(this).data('id')];
180 + if (obj) {
181 + selectObject(obj);
182 + }
183 + // stop propagation of event (I think) ...
184 + return false;
185 + });
186 + }
187 +
188 + function setUpRadioButtonHandler() {
189 + d3.selectAll('#displayModes .radio').on('click', function () {
190 + var id = d3.select(this).attr('id');
191 + if (id !== viewMode) {
192 + radioButton('displayModes', id);
193 + viewMode = id;
194 + doRadioAction(id);
195 + }
196 + });
197 + }
198 +
199 + function doRadioAction(id) {
200 + showAllLayers();
201 + if (id === 'showPkt') {
202 + showPacketLayer();
203 + } else if (id === 'showOpt') {
204 + showOpticalLayer();
205 + }
206 + }
207 +
208 + function showAllLayers() {
209 + network.node.classed('inactive', false);
210 + network.link.classed('inactive', false);
211 + d3.selectAll('svg .port').classed('inactive', false);
212 + d3.selectAll('svg .portText').classed('inactive', false);
213 + }
214 +
215 + function showPacketLayer() {
216 + network.node.each(function(d) {
217 + // deactivate nodes that are not hosts or switches
218 + if (d.class === 'device' && d.type !== 'switch') {
219 + d3.select(this).classed('inactive', true);
220 + }
221 + });
222 +
223 + network.link.each(function(lnk) {
224 + // deactivate infrastructure links that have opt's as endpoints
225 + if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
226 + d3.select(this).classed('inactive', true);
227 + }
228 + });
229 +
230 + // deactivate non-packet ports
231 + d3.selectAll('svg .optPort').classed('inactive', true)
232 + }
233 +
234 + function showOpticalLayer() {
235 + network.node.each(function(d) {
236 + // deactivate nodes that are not optical devices
237 + if (d.type !== 'roadm') {
238 + d3.select(this).classed('inactive', true);
239 + }
240 + });
241 +
242 + network.link.each(function(lnk) {
243 + // deactivate infrastructure links that have opt's as endpoints
244 + if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
245 + d3.select(this).classed('inactive', true);
246 + }
247 + });
248 +
249 + // deactivate non-packet ports
250 + d3.selectAll('svg .pktPort').classed('inactive', true)
251 + }
252 +
253 + function setUpKeyHandler() {
254 + d3.select('body')
255 + .on('keydown', function () {
256 + processKeyEvent();
257 + if (debug('showKeyHandler')) {
258 + network.svg.append('text')
259 + .attr('x', 5)
260 + .attr('y', 15)
261 + .style('font-size', '20pt')
262 + .text('keyCode: ' + d3.event.keyCode +
263 + ' applied to : ' + contextLabel())
264 + .transition().duration(2000)
265 + .style('font-size', '2pt')
266 + .style('fill-opacity', 0.01)
267 + .remove();
268 + }
269 + });
270 + }
271 +
272 + function contextLabel() {
273 + return hovered === null ? "(nothing)" : hovered.id;
274 + }
275 +
276 + function radioButton(group, id) {
277 + d3.selectAll("#" + group + " .radio").classed("active", false);
278 + d3.select("#" + group + " #" + id).classed("active", true);
279 + }
280 +
281 + function processKeyEvent() {
282 + var code = d3.event.keyCode;
283 + switch (code) {
284 + case 66: // B
285 + toggleBackground();
286 + break;
287 + case 71: // G
288 + cycleLayout();
289 + break;
290 + case 76: // L
291 + cycleLabels();
292 + break;
293 + case 80: // P
294 + togglePorts();
295 + break;
296 + case 85: // U
297 + unpin();
298 + break;
299 + }
300 +
301 + }
302 +
303 + function toggleBackground() {
304 + var bg = d3.select('#bg'),
305 + vis = bg.style('visibility'),
306 + newvis = (vis === 'hidden') ? 'visible' : 'hidden';
307 + bg.style('visibility', newvis);
308 + }
309 +
310 + function cycleLayout() {
311 + config.options.layering = !config.options.layering;
312 + network.force.resume();
313 + }
314 +
315 + function cycleLabels() {
316 + console.log('Cycle Labels - context = ' + contextLabel());
317 + }
318 +
319 + function togglePorts() {
320 + portLabelsOn = !portLabelsOn;
321 + var portVis = portLabelsOn ? 'visible' : 'hidden';
322 + d3.selectAll('.port').style('visibility', portVis);
323 + d3.selectAll('.portText').style('visibility', portVis);
324 + }
325 +
326 + function unpin() {
327 + if (hovered) {
328 + hovered.fixed = false;
329 + findNodeFromData(hovered).classed('fixed', false);
330 + network.force.resume();
331 + }
332 + console.log('Unpin - context = ' + contextLabel());
333 + }
334 +
335 +
336 + // ========================================================
337 +
338 + function drawNetwork() {
339 + $('#view').empty();
340 +
341 + prepareNodesAndLinks();
342 + createLayout();
343 + console.log("\n\nHere is the augmented network object...");
344 + console.log(network);
345 + }
346 +
347 + function prepareNodesAndLinks() {
348 + network.lookup = {};
349 + network.nodes = [];
350 + network.links = [];
351 +
352 + var nw = network.forceWidth,
353 + nh = network.forceHeight;
354 +
355 + function yPosConstraintForNode(n) {
356 + return config.constraints.ypos[n.type || 'host'];
357 + }
358 +
359 + // Note that both 'devices' and 'hosts' get mapped into the nodes array
360 +
361 + // first, the devices...
362 + network.data.devices.forEach(function(n) {
363 + var ypc = yPosConstraintForNode(n),
364 + ix = Math.random() * 0.6 * nw + 0.2 * nw,
365 + iy = ypc * nh,
366 + node = {
367 + id: n.id,
368 + labels: n.labels,
369 + class: 'device',
370 + icon: 'device',
371 + type: n.type,
372 + x: ix,
373 + y: iy,
374 + constraint: {
375 + weight: 0.7,
376 + y: iy
377 + }
378 + };
379 + network.lookup[n.id] = node;
380 + network.nodes.push(node);
381 + });
382 +
383 + // then, the hosts...
384 + network.data.hosts.forEach(function(n) {
385 + var ypc = yPosConstraintForNode(n),
386 + ix = Math.random() * 0.6 * nw + 0.2 * nw,
387 + iy = ypc * nh,
388 + node = {
389 + id: n.id,
390 + labels: n.labels,
391 + class: 'host',
392 + icon: 'host',
393 + type: n.type,
394 + x: ix,
395 + y: iy,
396 + constraint: {
397 + weight: 0.7,
398 + y: iy
399 + }
400 + };
401 + network.lookup[n.id] = node;
402 + network.nodes.push(node);
403 + });
404 +
405 +
406 + // now, process the explicit links...
407 + network.data.links.forEach(function(lnk) {
408 + var src = network.lookup[lnk.src],
409 + dst = network.lookup[lnk.dst],
410 + id = src.id + "-" + dst.id;
411 +
412 + var link = {
413 + class: 'infra',
414 + id: id,
415 + type: lnk.type,
416 + width: lnk.linkWidth,
417 + source: src,
418 + srcPort: lnk.srcPort,
419 + target: dst,
420 + tgtPort: lnk.dstPort,
421 + strength: config.force.linkStrength.infra
422 + };
423 + network.links.push(link);
424 + });
425 +
426 + // finally, infer host links...
427 + network.data.hosts.forEach(function(n) {
428 + var src = network.lookup[n.id],
429 + dst = network.lookup[n.cp.device],
430 + id = src.id + "-" + dst.id;
431 +
432 + var link = {
433 + class: 'host',
434 + id: id,
435 + type: 'hostLink',
436 + width: config.hostLinkWidth,
437 + source: src,
438 + target: dst,
439 + strength: config.force.linkStrength.host
440 + };
441 + network.links.push(link);
442 + });
443 + }
444 +
445 + function createLayout() {
446 +
447 + var cfg = config.force;
448 +
449 + network.force = d3.layout.force()
450 + .size([network.forceWidth, network.forceHeight])
451 + .nodes(network.nodes)
452 + .links(network.links)
453 + .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
454 + .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
455 + .charge(function(d) { return cfg.charge[d.class]; })
456 + .on('tick', tick);
457 +
458 + network.svg = d3.select('#view').append('svg')
459 + .attr('width', netView.width)
460 + .attr('height', netView.height)
461 + .append('g')
462 + .attr('transform', config.force.translate());
463 +// .attr('id', 'zoomable')
464 +// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
465 +
466 + network.svg.append('svg:image')
467 + .attr({
468 + id: 'bg',
469 + width: netView.width,
470 + height: netView.height,
471 + 'xlink:href': config.backgroundUrl
472 + })
473 + .style('visibility',
474 + config.options.loadBackground ? 'visible' : 'hidden');
475 +
476 +// function zoomRedraw() {
477 +// d3.select("#zoomable").attr("transform",
478 +// "translate(" + d3.event.translate + ")"
479 +// + " scale(" + d3.event.scale + ")");
480 +// }
481 +
482 + // TODO: move glow/blur stuff to util script
483 + var glow = network.svg.append('filter')
484 + .attr('x', '-50%')
485 + .attr('y', '-50%')
486 + .attr('width', '200%')
487 + .attr('height', '200%')
488 + .attr('id', 'blue-glow');
489 +
490 + glow.append('feColorMatrix')
491 + .attr('type', 'matrix')
492 + .attr('values', '0 0 0 0 0 ' +
493 + '0 0 0 0 0 ' +
494 + '0 0 0 0 .7 ' +
495 + '0 0 0 1 0 ');
496 +
497 + glow.append('feGaussianBlur')
498 + .attr('stdDeviation', 3)
499 + .attr('result', 'coloredBlur');
500 +
501 + glow.append('feMerge').selectAll('feMergeNode')
502 + .data(['coloredBlur', 'SourceGraphic'])
503 + .enter().append('feMergeNode')
504 + .attr('in', String);
505 +
506 + // TODO: legend (and auto adjust on scroll)
507 +// $('#view').on('scroll', function() {
508 +//
509 +// });
510 +
511 +
512 + // TODO: move drag behavior into separate method.
513 + // == define node drag behavior...
514 + network.draggedThreshold = d3.scale.linear()
515 + .domain([0, 0.1])
516 + .range([5, 20])
517 + .clamp(true);
518 +
519 + function dragged(d) {
520 + var threshold = network.draggedThreshold(network.force.alpha()),
521 + dx = d.oldX - d.px,
522 + dy = d.oldY - d.py;
523 + if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
524 + d.dragged = true;
525 + }
526 + return d.dragged;
527 + }
528 +
529 + network.drag = d3.behavior.drag()
530 + .origin(function(d) { return d; })
531 + .on('dragstart', function(d) {
532 + d.oldX = d.x;
533 + d.oldY = d.y;
534 + d.dragged = false;
535 + d.fixed |= 2;
536 + })
537 + .on('drag', function(d) {
538 + d.px = d3.event.x;
539 + d.py = d3.event.y;
540 + if (dragged(d)) {
541 + if (!network.force.alpha()) {
542 + network.force.alpha(.025);
543 + }
544 + }
545 + })
546 + .on('dragend', function(d) {
547 + if (!dragged(d)) {
548 + selectObject(d, this);
549 + }
550 + d.fixed &= ~6;
551 +
552 + // once we've finished moving, pin the node in position,
553 + // if it is a device (not a host)
554 + if (d.class === 'device') {
555 + d.fixed = true;
556 + d3.select(this).classed('fixed', true)
557 + }
558 + });
559 +
560 + $('#view').on('click', function(e) {
561 + if (!$(e.target).closest('.node').length) {
562 + deselectObject();
563 + }
564 + });
565 +
566 + // ...............................................................
567 +
568 + // add links to the display
569 + network.link = network.svg.append('g').attr('id', 'links')
570 + .selectAll('.link')
571 + .data(network.force.links(), function(d) {return d.id})
572 + .enter().append('line')
573 + .attr('class', function(d) {return 'link ' + d.class});
574 +
575 + network.linkSrcPort = network.svg.append('g')
576 + .attr({
577 + id: 'srcPorts',
578 + class: 'portLayer'
579 + });
580 + network.linkTgtPort = network.svg.append('g')
581 + .attr({
582 + id: 'tgtPorts',
583 + class: 'portLayer'
584 + });
585 +
586 + var portVis = portLabelsOn ? 'visible' : 'hidden',
587 + pw = config.labels.port.width,
588 + ph = config.labels.port.height;
589 +
590 + network.link.filter('.infra').each(function(d) {
591 + var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort',
592 + tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort';
593 +
594 + if (d.source.type)
595 +
596 + network.linkSrcPort.append('rect').attr({
597 + id: 'srcPort-' + safeId(d.id),
598 + class: 'port ' + srcType,
599 + width: pw,
600 + height: ph,
601 + rx: 4,
602 + ry: 4
603 + }).style('visibility', portVis);
604 +
605 + network.linkTgtPort.append('rect').attr({
606 + id: 'tgtPort-' + safeId(d.id),
607 + class: 'port ' + tgtType,
608 + width: pw,
609 + height: ph,
610 + rx: 4,
611 + ry: 4
612 + }).style('visibility', portVis);
613 +
614 + network.linkSrcPort.append('text').attr({
615 + id: 'srcText-' + safeId(d.id),
616 + class: 'portText ' + srcType
617 + }).text(d.srcPort)
618 + .style('visibility', portVis);
619 +
620 + network.linkTgtPort.append('text').attr({
621 + id: 'tgtText-' + safeId(d.id),
622 + class: 'portText ' + tgtType
623 + }).text(d.tgtPort)
624 + .style('visibility', portVis);
625 + });
626 +
627 + // ...............................................................
628 +
629 + // add nodes to the display
630 + network.node = network.svg.selectAll('.node')
631 + .data(network.force.nodes(), function(d) {return d.id})
632 + .enter().append('g')
633 + .attr('class', function(d) {
634 + var cls = 'node ' + d.class;
635 + if (d.type) {
636 + cls += ' ' + d.type;
637 + }
638 + return cls;
639 + })
640 + .attr('transform', function(d) {
641 + return translate(d.x, d.y);
642 + })
643 + .call(network.drag)
644 + .on('mouseover', function(d) {
645 + // TODO: show tooltip
646 + if (network.mouseoutTimeout) {
647 + clearTimeout(network.mouseoutTimeout);
648 + network.mouseoutTimeout = null;
649 + }
650 + hoverObject(d);
651 + })
652 + .on('mouseout', function(d) {
653 + // TODO: hide tooltip
654 + if (network.mouseoutTimeout) {
655 + clearTimeout(network.mouseoutTimeout);
656 + network.mouseoutTimeout = null;
657 + }
658 + network.mouseoutTimeout = setTimeout(function() {
659 + hoverObject(null);
660 + }, config.mouseOutTimerDelayMs);
661 + });
662 +
663 +
664 + // deal with device nodes first
665 + network.nodeRect = network.node.filter('.device')
666 + .append('rect')
667 + .attr({
668 + rx: 5,
669 + ry: 5,
670 + width: 100,
671 + height: 12
672 + });
673 + // note that width/height are adjusted to fit the label text
674 + // then padded, and space made for the icon.
675 +
676 + network.node.filter('.device').each(function(d) {
677 + var node = d3.select(this),
678 + icon = iconUrl(d);
679 +
680 + node.append('text')
681 + // TODO: add label cycle behavior
682 + .text(d.id)
683 + .attr('dy', '1.1em');
684 +
685 + if (icon) {
686 + var cfg = config.icons;
687 + node.append('svg:image')
688 + .attr({
689 + width: cfg.w,
690 + height: cfg.h,
691 + 'xlink:href': icon
692 + });
693 + // note, icon relative positioning (x,y) is done after we have
694 + // adjusted the bounds of the rectangle...
695 + }
696 +
697 + // debug function to show the modelled x,y coordinates of nodes...
698 + if (debug('showNodeXY')) {
699 + node.select('rect').attr('fill-opacity', 0.5);
700 + node.append('circle')
701 + .attr({
702 + class: 'debug',
703 + cx: 0,
704 + cy: 0,
705 + r: '3px'
706 + });
707 + }
708 + });
709 +
710 + // now process host nodes
711 + network.nodeCircle = network.node.filter('.host')
712 + .append('circle')
713 + .attr({
714 + r: config.hostRadius
715 + });
716 +
717 + network.node.filter('.host').each(function(d) {
718 + var node = d3.select(this),
719 + icon = iconUrl(d);
720 +
721 + // debug function to show the modelled x,y coordinates of nodes...
722 + if (debug('showNodeXY')) {
723 + node.select('circle').attr('fill-opacity', 0.5);
724 + node.append('circle')
725 + .attr({
726 + class: 'debug',
727 + cx: 0,
728 + cy: 0,
729 + r: '3px'
730 + });
731 + }
732 + });
733 +
734 + // this function is scheduled to happen soon after the given thread ends
735 + setTimeout(function() {
736 + var lab = config.labels,
737 + portGap = lab.port.gap,
738 + midW = portGap + lab.port.width/ 2,
739 + midH = portGap + lab.port.height / 2;
740 +
741 + // post process the device nodes, to pad their size to fit the
742 + // label text and attach the icon to the right location.
743 + network.node.filter('.device').each(function(d) {
744 + // for every node, recompute size, padding, etc. so text fits
745 + var node = d3.select(this),
746 + text = node.select('text'),
747 + box = adjustRectToFitText(node);
748 +
749 + // now make the computed adjustment
750 + node.select('rect')
751 + .attr(box);
752 +
753 + node.select('image')
754 + .attr('x', box.x + config.icons.xoff)
755 + .attr('y', box.y + config.icons.yoff);
756 +
757 + var bounds = boundsFromBox(box),
758 + portBounds = {
759 + x1: bounds.x1 - midW,
760 + x2: bounds.x2 + midW,
761 + y1: bounds.y1 - midH,
762 + y2: bounds.y2 + midH
763 + };
764 +
765 + // todo: clean up extent and edge work..
766 + d.extent = {
767 + left: bounds.x1 - lab.marginLR,
768 + right: bounds.x2 + lab.marginLR,
769 + top: bounds.y1 - lab.marginTB,
770 + bottom: bounds.y2 + lab.marginTB
771 + };
772 +
773 + d.edge = {
774 + left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
775 + right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
776 + top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
777 + bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
778 + };
779 +
780 + d.portEdge = {
781 + left : new geo.LineSegment(
782 + portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2
783 + ),
784 + right : new geo.LineSegment(
785 + portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2
786 + ),
787 + top : new geo.LineSegment(
788 + portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1
789 + ),
790 + bottom : new geo.LineSegment(
791 + portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2
792 + )
793 + };
794 +
795 + });
796 +
797 + network.numTicks = 0;
798 + network.preventCollisions = false;
799 + network.force.start();
800 + for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
801 + network.force.tick();
802 + }
803 + network.preventCollisions = true;
804 + $('#view').css('visibility', 'visible');
805 + });
806 +
807 +
808 + // returns the newly computed bounding box of the rectangle
809 + function adjustRectToFitText(n) {
810 + var text = n.select('text'),
811 + box = text.node().getBBox(),
812 + lab = config.labels;
813 +
814 + // not sure why n.data() returns an array of 1 element...
815 + var data = n.data()[0];
816 +
817 + text.attr('text-anchor', 'middle')
818 + .attr('y', '-0.8em')
819 + .attr('x', lab.imgPad/2)
820 + ;
821 +
822 + // translate the bbox so that it is centered on [x,y]
823 + box.x = -box.width / 2;
824 + box.y = -box.height / 2;
825 +
826 + // add padding
827 + box.x -= (lab.padLR + lab.imgPad/2);
828 + box.width += lab.padLR * 2 + lab.imgPad;
829 + box.y -= lab.padTB;
830 + box.height += lab.padTB * 2;
831 +
832 + return box;
833 + }
834 +
835 + function boundsFromBox(box) {
836 + return {
837 + x1: box.x,
838 + y1: box.y,
839 + x2: box.x + box.width,
840 + y2: box.y + box.height
841 + };
842 + }
843 +
844 + }
845 +
846 + function iconUrl(d) {
847 + return 'img/' + d.type + '.png';
848 +// return config.iconUrl[d.icon];
849 + }
850 +
851 + function translate(x, y) {
852 + return 'translate(' + x + ',' + y + ')';
853 + }
854 +
855 + // prevents collisions amongst device nodes
856 + function preventCollisions() {
857 + var quadtree = d3.geom.quadtree(network.nodes),
858 + hrad = config.hostRadius;
859 +
860 + network.nodes.forEach(function(n) {
861 + var nx1, nx2, ny1, ny2;
862 +
863 + if (n.class === 'device') {
864 + nx1 = n.x + n.extent.left;
865 + nx2 = n.x + n.extent.right;
866 + ny1 = n.y + n.extent.top;
867 + ny2 = n.y + n.extent.bottom;
868 +
869 + } else {
870 + nx1 = n.x - hrad;
871 + nx2 = n.x + hrad;
872 + ny1 = n.y - hrad;
873 + ny2 = n.y + hrad;
874 + }
875 +
876 + quadtree.visit(function(quad, x1, y1, x2, y2) {
877 + if (quad.point && quad.point !== n) {
878 + // check if the rectangles/circles intersect
879 + var p = quad.point,
880 + px1, px2, py1, py2, ix;
881 +
882 + if (p.class === 'device') {
883 + px1 = p.x + p.extent.left;
884 + px2 = p.x + p.extent.right;
885 + py1 = p.y + p.extent.top;
886 + py2 = p.y + p.extent.bottom;
887 +
888 + } else {
889 + px1 = p.x - hrad;
890 + px2 = p.x + hrad;
891 + py1 = p.y - hrad;
892 + py2 = p.y + hrad;
893 + }
894 +
895 + ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
896 +
897 + if (ix) {
898 + var xa1 = nx2 - px1, // shift n left , p right
899 + xa2 = px2 - nx1, // shift n right, p left
900 + ya1 = ny2 - py1, // shift n up , p down
901 + ya2 = py2 - ny1, // shift n down , p up
902 + adj = Math.min(xa1, xa2, ya1, ya2);
903 +
904 + if (adj == xa1) {
905 + n.x -= adj / 2;
906 + p.x += adj / 2;
907 + } else if (adj == xa2) {
908 + n.x += adj / 2;
909 + p.x -= adj / 2;
910 + } else if (adj == ya1) {
911 + n.y -= adj / 2;
912 + p.y += adj / 2;
913 + } else if (adj == ya2) {
914 + n.y += adj / 2;
915 + p.y -= adj / 2;
916 + }
917 + }
918 + return ix;
919 + }
920 + });
921 +
922 + });
923 + }
924 +
925 + function tick(e) {
926 + network.numTicks++;
927 +
928 + if (config.options.layering) {
929 + // adjust the y-coord of each node, based on y-pos constraints
930 + network.nodes.forEach(function (n) {
931 + var z = e.alpha * n.constraint.weight;
932 + if (!isNaN(n.constraint.y)) {
933 + n.y = (n.constraint.y * z + n.y * (1 - z));
934 + }
935 + });
936 + }
937 +
938 + if (config.options.collisionPrevention && network.preventCollisions) {
939 + preventCollisions();
940 + }
941 +
942 + var portHalfW = config.labels.port.width / 2,
943 + portHalfH = config.labels.port.height / 2;
944 +
945 + // clip visualization of links at bounds of nodes...
946 + network.link.each(function(d) {
947 + var xs = d.source.x,
948 + ys = d.source.y,
949 + xt = d.target.x,
950 + yt = d.target.y,
951 + line = new geo.LineSegment(xs, ys, xt, yt),
952 + e, ix,
953 + exs, eys, ext, eyt,
954 + pxs, pys, pxt, pyt;
955 +
956 + if (d.class === 'host') {
957 + // no adjustment for source end of link, since hosts are dots
958 + exs = xs;
959 + eys = ys;
960 +
961 + } else {
962 + for (e in d.source.edge) {
963 + ix = line.intersect(d.source.edge[e].offset(xs, ys));
964 + if (ix.in1 && ix.in2) {
965 + exs = ix.x;
966 + eys = ix.y;
967 +
968 + // also pick off the port label intersection
969 + ix = line.intersect(d.source.portEdge[e].offset(xs, ys));
970 + pxs = ix.x;
971 + pys = ix.y;
972 + break;
973 + }
974 + }
975 + }
976 +
977 + for (e in d.target.edge) {
978 + ix = line.intersect(d.target.edge[e].offset(xt, yt));
979 + if (ix.in1 && ix.in2) {
980 + ext = ix.x;
981 + eyt = ix.y;
982 +
983 + // also pick off the port label intersection
984 + ix = line.intersect(d.target.portEdge[e].offset(xt, yt));
985 + pxt = ix.x;
986 + pyt = ix.y;
987 + break;
988 + }
989 + }
990 +
991 + // adjust the endpoints of the link's line to match rectangles
992 + var sid = safeId(d.id);
993 + d3.select(this)
994 + .attr('x1', exs)
995 + .attr('y1', eys)
996 + .attr('x2', ext)
997 + .attr('y2', eyt);
998 +
999 + d3.select('#srcPort-' + sid)
1000 + .attr('x', pxs - portHalfW)
1001 + .attr('y', pys - portHalfH);
1002 +
1003 + d3.select('#tgtPort-' + sid)
1004 + .attr('x', pxt - portHalfW)
1005 + .attr('y', pyt - portHalfH);
1006 +
1007 + // TODO: fit label rect to size of port number.
1008 + d3.select('#srcText-' + sid)
1009 + .attr('x', pxs - 5)
1010 + .attr('y', pys + 3);
1011 +
1012 + d3.select('#tgtText-' + sid)
1013 + .attr('x', pxt - 5)
1014 + .attr('y', pyt + 3);
1015 +
1016 + });
1017 +
1018 + // position each node by translating the node (group) by x,y
1019 + network.node
1020 + .attr('transform', function(d) {
1021 + return translate(d.x, d.y);
1022 + });
1023 +
1024 + }
1025 +
1026 + // $('#docs-close').on('click', function() {
1027 + // deselectObject();
1028 + // return false;
1029 + // });
1030 +
1031 + // $(document).on('click', '.select-object', function() {
1032 + // var obj = graph.data[$(this).data('name')];
1033 + // if (obj) {
1034 + // selectObject(obj);
1035 + // }
1036 + // return false;
1037 + // });
1038 +
1039 + function findNodeFromData(d) {
1040 + var el = null;
1041 + network.node.filter('.' + d.class).each(function(n) {
1042 + if (n.id === d.id) {
1043 + el = d3.select(this);
1044 + }
1045 + });
1046 + return el;
1047 + }
1048 +
1049 + function selectObject(obj, el) {
1050 + var node;
1051 + if (el) {
1052 + node = d3.select(el);
1053 + } else {
1054 + network.node.each(function(d) {
1055 + if (d == obj) {
1056 + node = d3.select(el = this);
1057 + }
1058 + });
1059 + }
1060 + if (!node) return;
1061 +
1062 + if (node.classed('selected')) {
1063 + deselectObject();
1064 + flyinPane(null);
1065 + return;
1066 + }
1067 + deselectObject(false);
1068 +
1069 + selected = {
1070 + obj : obj,
1071 + el : el
1072 + };
1073 +
1074 + node.classed('selected', true);
1075 + flyinPane(obj);
1076 + }
1077 +
1078 + function deselectObject(doResize) {
1079 + // Review: logic of 'resize(...)' function.
1080 + if (doResize || typeof doResize == 'undefined') {
1081 + resize(false);
1082 + }
1083 +
1084 + // deselect all nodes in the network...
1085 + network.node.classed('selected', false);
1086 + selected = {};
1087 + flyinPane(null);
1088 + }
1089 +
1090 + function flyinPane(obj) {
1091 + var pane = d3.select('#flyout'),
1092 + url;
1093 +
1094 + if (obj) {
1095 + // go get details of the selected object from the server...
1096 + url = detailJsonUrl(obj.id);
1097 + d3.json(url, function (err, data) {
1098 + if (err) {
1099 + alert('Oops! Error reading JSON...\n\n' +
1100 + 'URL: ' + url + '\n\n' +
1101 + 'Error: ' + err.message);
1102 + return;
1103 + }
1104 +// console.log("JSON data... " + url);
1105 +// console.log(data);
1106 +
1107 + displayDetails(data, pane);
1108 + });
1109 +
1110 + } else {
1111 + // hide pane
1112 + pane.transition().duration(750)
1113 + .style('right', '-320px')
1114 + .style('opacity', 0.0);
1115 + }
1116 + }
1117 +
1118 + function displayDetails(data, pane) {
1119 + $('#flyout').empty();
1120 +
1121 + var title = pane.append("h2"),
1122 + table = pane.append("table"),
1123 + tbody = table.append("tbody");
1124 +
1125 + $('<img src="img/' + data.type + '.png">').appendTo(title);
1126 + $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1127 +
1128 +
1129 + // TODO: consider using d3 data bind to TR/TD
1130 +
1131 + data.propOrder.forEach(function(p) {
1132 + if (p === '-') {
1133 + addSep(tbody);
1134 + } else {
1135 + addProp(tbody, p, data.props[p]);
1136 + }
1137 + });
1138 +
1139 + function addSep(tbody) {
1140 + var tr = tbody.append('tr');
1141 + $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1142 + }
1143 +
1144 + function addProp(tbody, label, value) {
1145 + var tr = tbody.append('tr');
1146 +
1147 + tr.append('td')
1148 + .attr('class', 'label')
1149 + .text(label + ' :');
1150 +
1151 + tr.append('td')
1152 + .attr('class', 'value')
1153 + .text(value);
1154 + }
1155 +
1156 + // show pane
1157 + pane.transition().duration(750)
1158 + .style('right', '20px')
1159 + .style('opacity', 1.0);
1160 + }
1161 +
1162 + function highlightObject(obj) {
1163 + if (obj) {
1164 + if (obj != highlighted) {
1165 + // TODO set or clear "inactive" class on nodes, based on criteria
1166 + network.node.classed('inactive', function(d) {
1167 + // return (obj !== d &&
1168 + // d.relation(obj.id));
1169 + return (obj !== d);
1170 + });
1171 + // TODO: same with links
1172 + network.link.classed('inactive', function(d) {
1173 + return (obj !== d.source && obj !== d.target);
1174 + });
1175 + }
1176 + highlighted = obj;
1177 + } else {
1178 + if (highlighted) {
1179 + // clear the inactive flag (no longer suppressed visually)
1180 + network.node.classed('inactive', false);
1181 + network.link.classed('inactive', false);
1182 + }
1183 + highlighted = null;
1184 +
1185 + }
1186 + }
1187 +
1188 + function hoverObject(obj) {
1189 + if (obj) {
1190 + hovered = obj;
1191 + } else {
1192 + if (hovered) {
1193 + hovered = null;
1194 + }
1195 + }
1196 + }
1197 +
1198 +
1199 + function resize() {
1200 + netView.height = window.innerHeight - config.mastHeight;
1201 + netView.width = window.innerWidth;
1202 + $('#view')
1203 + .css('height', netView.height + 'px')
1204 + .css('width', netView.width + 'px');
1205 +
1206 + network.forceWidth = netView.width - config.force.marginLR;
1207 + network.forceHeight = netView.height - config.force.marginTB;
1208 + }
1209 +
1210 + // ======================================================================
1211 + // register with the UI framework
1212 +
1213 + onos.ui.addView('topo', {
1214 + load: loadNetworkView
1215 + });
1216 +
1217 +
1218 +}(ONOS));
1219 +
...@@ -20,3 +20,7 @@ ...@@ -20,3 +20,7 @@
20 @author Simon Hunt 20 @author Simon Hunt
21 */ 21 */
22 22
23 +svg #topo-bg {
24 + opacity: 0.5;
25 +}
26 +
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
15 */ 15 */
16 16
17 /* 17 /*
18 - ONOS network topology viewer - PoC version 1.0 18 + ONOS network topology viewer - version 1.1
19 19
20 @author Simon Hunt 20 @author Simon Hunt
21 */ 21 */
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
25 25
26 // configuration data 26 // configuration data
27 var config = { 27 var config = {
28 - useLiveData: true, 28 + useLiveData: false,
29 debugOn: false, 29 debugOn: false,
30 debug: { 30 debug: {
31 showNodeXY: false, 31 showNodeXY: false,
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
34 options: { 34 options: {
35 layering: true, 35 layering: true,
36 collisionPrevention: true, 36 collisionPrevention: true,
37 - loadBackground: true 37 + showBackground: true
38 }, 38 },
39 backgroundUrl: 'img/us-map.png', 39 backgroundUrl: 'img/us-map.png',
40 data: { 40 data: {
...@@ -55,22 +55,7 @@ ...@@ -55,22 +55,7 @@
55 pkt: 'img/pkt.png', 55 pkt: 'img/pkt.png',
56 opt: 'img/opt.png' 56 opt: 'img/opt.png'
57 }, 57 },
58 - mastHeight: 36,
59 force: { 58 force: {
60 - note: 'node.class or link.class is used to differentiate',
61 - linkDistance: {
62 - infra: 200,
63 - host: 40
64 - },
65 - linkStrength: {
66 - infra: 1.0,
67 - host: 1.0
68 - },
69 - charge: {
70 - device: -800,
71 - host: -1000
72 - },
73 - ticksWithoutCollisions: 50,
74 marginLR: 20, 59 marginLR: 20,
75 marginTB: 20, 60 marginTB: 20,
76 translate: function() { 61 translate: function() {
...@@ -78,39 +63,19 @@ ...@@ -78,39 +63,19 @@
78 config.force.marginLR + ',' + 63 config.force.marginLR + ',' +
79 config.force.marginTB + ')'; 64 config.force.marginTB + ')';
80 } 65 }
81 - }, 66 + }
82 - labels: {
83 - imgPad: 16,
84 - padLR: 8,
85 - padTB: 6,
86 - marginLR: 3,
87 - marginTB: 2,
88 - port: {
89 - gap: 3,
90 - width: 18,
91 - height: 14
92 - }
93 - },
94 - icons: {
95 - w: 32,
96 - h: 32,
97 - xoff: -12,
98 - yoff: -8
99 - },
100 - constraints: {
101 - ypos: {
102 - host: 0.05,
103 - switch: 0.3,
104 - roadm: 0.7
105 - }
106 - },
107 - hostLinkWidth: 1.0,
108 - hostRadius: 7,
109 - mouseOutTimerDelayMs: 120
110 }; 67 };
111 68
69 + // radio buttons
70 + var btnSet = [
71 + { id: 'showAll', text: 'All Layers' },
72 + { id: 'showPkt', text: 'Packet Only' },
73 + { id: 'showOpt', text: 'Optical Only' }
74 + ];
75 +
112 // state variables 76 // state variables
113 - var view = {}, 77 + var svg,
78 + bgImg,
114 network = {}, 79 network = {},
115 selected = {}, 80 selected = {},
116 highlighted = null, 81 highlighted = null,
...@@ -119,84 +84,19 @@ ...@@ -119,84 +84,19 @@
119 portLabelsOn = false; 84 portLabelsOn = false;
120 85
121 86
122 - function debug(what) { 87 + // ==============================
123 - return config.debugOn && config.debug[what]; 88 + // Private functions
124 - }
125 -
126 - function urlData() {
127 - return config.data[config.useLiveData ? 'live' : 'fake'];
128 - }
129 -
130 - function networkJsonUrl() {
131 - return urlData().jsonUrl;
132 - }
133 -
134 - function safeId(id) {
135 - return id.replace(/[^a-z0-9]/gi, '_');
136 - }
137 -
138 - function detailJsonUrl(id) {
139 - var u = urlData(),
140 - encId = config.useLiveData ? encodeURIComponent(id) : safeId(id);
141 - return u.detailPrefix + encId + u.detailSuffix;
142 - }
143 -
144 -
145 - // load the topology view of the network
146 - function loadNetworkView() {
147 - // Hey, here I am, calling something on the ONOS api:
148 - api.printTime();
149 89
150 - resize(); 90 + // set the size of the SVG layer (or other element) to that of the view
151 - 91 + function setSize(view, el) {
152 - // go get our network data from the server... 92 + var thing = el || svg;
153 - var url = networkJsonUrl(); 93 + thing.attr({
154 - d3.json(url , function (err, data) { 94 + width: view.width(),
155 - if (err) { 95 + height: view.height()
156 - alert('Oops! Error reading JSON...\n\n' +
157 - 'URL: ' + url + '\n\n' +
158 - 'Error: ' + err.message);
159 - return;
160 - }
161 -// console.log("here is the JSON data...");
162 -// console.log(data);
163 -
164 - network.data = data;
165 - drawNetwork();
166 - });
167 -
168 - // while we wait for the data, set up the handlers...
169 - setUpClickHandler();
170 - setUpRadioButtonHandler();
171 - setUpKeyHandler();
172 - $(window).on('resize', resize);
173 - }
174 -
175 - function setUpClickHandler() {
176 - // click handler for "selectable" objects
177 - $(document).on('click', '.select-object', function () {
178 - // when any object of class "select-object" is clicked...
179 - var obj = network.lookup[$(this).data('id')];
180 - if (obj) {
181 - selectObject(obj);
182 - }
183 - // stop propagation of event (I think) ...
184 - return false;
185 - });
186 - }
187 -
188 - function setUpRadioButtonHandler() {
189 - d3.selectAll('#displayModes .radio').on('click', function () {
190 - var id = d3.select(this).attr('id');
191 - if (id !== viewMode) {
192 - radioButton('displayModes', id);
193 - viewMode = id;
194 - doRadioAction(id);
195 - }
196 }); 96 });
197 } 97 }
198 98
199 - function doRadioAction(id) { 99 + function doRadio(view, id) {
200 showAllLayers(); 100 showAllLayers();
201 if (id === 'showPkt') { 101 if (id === 'showPkt') {
202 showPacketLayer(); 102 showPacketLayer();
...@@ -206,1014 +106,68 @@ ...@@ -206,1014 +106,68 @@
206 } 106 }
207 107
208 function showAllLayers() { 108 function showAllLayers() {
209 - network.node.classed('inactive', false); 109 +// network.node.classed('inactive', false);
210 - network.link.classed('inactive', false); 110 +// network.link.classed('inactive', false);
211 - d3.selectAll('svg .port').classed('inactive', false) 111 +// d3.selectAll('svg .port').classed('inactive', false);
212 - d3.selectAll('svg .portText').classed('inactive', false) 112 +// d3.selectAll('svg .portText').classed('inactive', false);
113 + alert('show all layers');
213 } 114 }
214 115
215 function showPacketLayer() { 116 function showPacketLayer() {
216 - network.node.each(function(d) { 117 + alert('show packet layer');
217 - // deactivate nodes that are not hosts or switches
218 - if (d.class === 'device' && d.type !== 'switch') {
219 - d3.select(this).classed('inactive', true);
220 - }
221 - });
222 -
223 - network.link.each(function(lnk) {
224 - // deactivate infrastructure links that have opt's as endpoints
225 - if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
226 - d3.select(this).classed('inactive', true);
227 - }
228 - });
229 -
230 - // deactivate non-packet ports
231 - d3.selectAll('svg .optPort').classed('inactive', true)
232 } 118 }
233 119
234 function showOpticalLayer() { 120 function showOpticalLayer() {
235 - network.node.each(function(d) { 121 + alert('show optical layer');
236 - // deactivate nodes that are not optical devices
237 - if (d.type !== 'roadm') {
238 - d3.select(this).classed('inactive', true);
239 - }
240 - });
241 -
242 - network.link.each(function(lnk) {
243 - // deactivate infrastructure links that have opt's as endpoints
244 - if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
245 - d3.select(this).classed('inactive', true);
246 - }
247 - });
248 -
249 - // deactivate non-packet ports
250 - d3.selectAll('svg .pktPort').classed('inactive', true)
251 - }
252 -
253 - function setUpKeyHandler() {
254 - d3.select('body')
255 - .on('keydown', function () {
256 - processKeyEvent();
257 - if (debug('showKeyHandler')) {
258 - network.svg.append('text')
259 - .attr('x', 5)
260 - .attr('y', 15)
261 - .style('font-size', '20pt')
262 - .text('keyCode: ' + d3.event.keyCode +
263 - ' applied to : ' + contextLabel())
264 - .transition().duration(2000)
265 - .style('font-size', '2pt')
266 - .style('fill-opacity', 0.01)
267 - .remove();
268 - }
269 - });
270 - }
271 -
272 - function contextLabel() {
273 - return hovered === null ? "(nothing)" : hovered.id;
274 - }
275 -
276 - function radioButton(group, id) {
277 - d3.selectAll("#" + group + " .radio").classed("active", false);
278 - d3.select("#" + group + " #" + id).classed("active", true);
279 - }
280 -
281 - function processKeyEvent() {
282 - var code = d3.event.keyCode;
283 - switch (code) {
284 - case 66: // B
285 - toggleBackground();
286 - break;
287 - case 71: // G
288 - cycleLayout();
289 - break;
290 - case 76: // L
291 - cycleLabels();
292 - break;
293 - case 80: // P
294 - togglePorts();
295 - break;
296 - case 85: // U
297 - unpin();
298 - break;
299 - }
300 -
301 - }
302 -
303 - function toggleBackground() {
304 - var bg = d3.select('#bg'),
305 - vis = bg.style('visibility'),
306 - newvis = (vis === 'hidden') ? 'visible' : 'hidden';
307 - bg.style('visibility', newvis);
308 - }
309 -
310 - function cycleLayout() {
311 - config.options.layering = !config.options.layering;
312 - network.force.resume();
313 - }
314 -
315 - function cycleLabels() {
316 - console.log('Cycle Labels - context = ' + contextLabel());
317 - }
318 -
319 - function togglePorts() {
320 - portLabelsOn = !portLabelsOn;
321 - var portVis = portLabelsOn ? 'visible' : 'hidden';
322 - d3.selectAll('.port').style('visibility', portVis);
323 - d3.selectAll('.portText').style('visibility', portVis);
324 - }
325 -
326 - function unpin() {
327 - if (hovered) {
328 - hovered.fixed = false;
329 - findNodeFromData(hovered).classed('fixed', false);
330 - network.force.resume();
331 - }
332 - console.log('Unpin - context = ' + contextLabel());
333 - }
334 -
335 -
336 - // ========================================================
337 -
338 - function drawNetwork() {
339 - $('#view').empty();
340 -
341 - prepareNodesAndLinks();
342 - createLayout();
343 - console.log("\n\nHere is the augmented network object...");
344 - console.log(network);
345 - }
346 -
347 - function prepareNodesAndLinks() {
348 - network.lookup = {};
349 - network.nodes = [];
350 - network.links = [];
351 -
352 - var nw = network.forceWidth,
353 - nh = network.forceHeight;
354 -
355 - function yPosConstraintForNode(n) {
356 - return config.constraints.ypos[n.type || 'host'];
357 - }
358 -
359 - // Note that both 'devices' and 'hosts' get mapped into the nodes array
360 -
361 - // first, the devices...
362 - network.data.devices.forEach(function(n) {
363 - var ypc = yPosConstraintForNode(n),
364 - ix = Math.random() * 0.6 * nw + 0.2 * nw,
365 - iy = ypc * nh,
366 - node = {
367 - id: n.id,
368 - labels: n.labels,
369 - class: 'device',
370 - icon: 'device',
371 - type: n.type,
372 - x: ix,
373 - y: iy,
374 - constraint: {
375 - weight: 0.7,
376 - y: iy
377 - }
378 - };
379 - network.lookup[n.id] = node;
380 - network.nodes.push(node);
381 - });
382 -
383 - // then, the hosts...
384 - network.data.hosts.forEach(function(n) {
385 - var ypc = yPosConstraintForNode(n),
386 - ix = Math.random() * 0.6 * nw + 0.2 * nw,
387 - iy = ypc * nh,
388 - node = {
389 - id: n.id,
390 - labels: n.labels,
391 - class: 'host',
392 - icon: 'host',
393 - type: n.type,
394 - x: ix,
395 - y: iy,
396 - constraint: {
397 - weight: 0.7,
398 - y: iy
399 - }
400 - };
401 - network.lookup[n.id] = node;
402 - network.nodes.push(node);
403 - });
404 -
405 -
406 - // now, process the explicit links...
407 - network.data.links.forEach(function(lnk) {
408 - var src = network.lookup[lnk.src],
409 - dst = network.lookup[lnk.dst],
410 - id = src.id + "-" + dst.id;
411 -
412 - var link = {
413 - class: 'infra',
414 - id: id,
415 - type: lnk.type,
416 - width: lnk.linkWidth,
417 - source: src,
418 - srcPort: lnk.srcPort,
419 - target: dst,
420 - tgtPort: lnk.dstPort,
421 - strength: config.force.linkStrength.infra
422 - };
423 - network.links.push(link);
424 - });
425 -
426 - // finally, infer host links...
427 - network.data.hosts.forEach(function(n) {
428 - var src = network.lookup[n.id],
429 - dst = network.lookup[n.cp.device],
430 - id = src.id + "-" + dst.id;
431 -
432 - var link = {
433 - class: 'host',
434 - id: id,
435 - type: 'hostLink',
436 - width: config.hostLinkWidth,
437 - source: src,
438 - target: dst,
439 - strength: config.force.linkStrength.host
440 - };
441 - network.links.push(link);
442 - });
443 } 122 }
444 123
445 - function createLayout() { 124 + // ==============================
446 - 125 + // View life-cycle callbacks
447 - var cfg = config.force;
448 126
449 - network.force = d3.layout.force() 127 + function preload(view, ctx) {
450 - .size([network.forceWidth, network.forceHeight]) 128 + var w = view.width(),
451 - .nodes(network.nodes) 129 + h = view.height(),
452 - .links(network.links) 130 + idBg = view.uid('bg'),
453 - .linkStrength(function(d) { return cfg.linkStrength[d.class]; }) 131 + showBg = config.options.showBackground ? 'visible' : 'hidden';
454 - .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
455 - .charge(function(d) { return cfg.charge[d.class]; })
456 - .on('tick', tick);
457 132
458 - network.svg = d3.select('#view').append('svg') 133 + // NOTE: view.$div is a D3 selection of the view's div
459 - .attr('width', view.width) 134 + svg = view.$div.append('svg');
460 - .attr('height', view.height) 135 + setSize(view);
461 - .append('g') 136 + svg.append('g')
462 .attr('transform', config.force.translate()); 137 .attr('transform', config.force.translate());
463 -// .attr('id', 'zoomable')
464 -// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
465 138
466 - network.svg.append('svg:image') 139 + // load the background image
140 + bgImg = svg.append('svg:image')
467 .attr({ 141 .attr({
468 - id: 'bg', 142 + id: idBg,
469 - width: view.width, 143 + width: w,
470 - height: view.height, 144 + height: h,
471 'xlink:href': config.backgroundUrl 145 'xlink:href': config.backgroundUrl
472 }) 146 })
473 - .style('visibility', 147 + .style({
474 - config.options.loadBackground ? 'visible' : 'hidden'); 148 + visibility: showBg
475 -
476 -// function zoomRedraw() {
477 -// d3.select("#zoomable").attr("transform",
478 -// "translate(" + d3.event.translate + ")"
479 -// + " scale(" + d3.event.scale + ")");
480 -// }
481 -
482 - // TODO: move glow/blur stuff to util script
483 - var glow = network.svg.append('filter')
484 - .attr('x', '-50%')
485 - .attr('y', '-50%')
486 - .attr('width', '200%')
487 - .attr('height', '200%')
488 - .attr('id', 'blue-glow');
489 -
490 - glow.append('feColorMatrix')
491 - .attr('type', 'matrix')
492 - .attr('values', '0 0 0 0 0 ' +
493 - '0 0 0 0 0 ' +
494 - '0 0 0 0 .7 ' +
495 - '0 0 0 1 0 ');
496 -
497 - glow.append('feGaussianBlur')
498 - .attr('stdDeviation', 3)
499 - .attr('result', 'coloredBlur');
500 -
501 - glow.append('feMerge').selectAll('feMergeNode')
502 - .data(['coloredBlur', 'SourceGraphic'])
503 - .enter().append('feMergeNode')
504 - .attr('in', String);
505 -
506 - // TODO: legend (and auto adjust on scroll)
507 -// $('#view').on('scroll', function() {
508 -//
509 -// });
510 -
511 -
512 - // TODO: move drag behavior into separate method.
513 - // == define node drag behavior...
514 - network.draggedThreshold = d3.scale.linear()
515 - .domain([0, 0.1])
516 - .range([5, 20])
517 - .clamp(true);
518 -
519 - function dragged(d) {
520 - var threshold = network.draggedThreshold(network.force.alpha()),
521 - dx = d.oldX - d.px,
522 - dy = d.oldY - d.py;
523 - if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
524 - d.dragged = true;
525 - }
526 - return d.dragged;
527 - }
528 -
529 - network.drag = d3.behavior.drag()
530 - .origin(function(d) { return d; })
531 - .on('dragstart', function(d) {
532 - d.oldX = d.x;
533 - d.oldY = d.y;
534 - d.dragged = false;
535 - d.fixed |= 2;
536 - })
537 - .on('drag', function(d) {
538 - d.px = d3.event.x;
539 - d.py = d3.event.y;
540 - if (dragged(d)) {
541 - if (!network.force.alpha()) {
542 - network.force.alpha(.025);
543 - }
544 - }
545 - })
546 - .on('dragend', function(d) {
547 - if (!dragged(d)) {
548 - selectObject(d, this);
549 - }
550 - d.fixed &= ~6;
551 -
552 - // once we've finished moving, pin the node in position,
553 - // if it is a device (not a host)
554 - if (d.class === 'device') {
555 - d.fixed = true;
556 - d3.select(this).classed('fixed', true)
557 - }
558 - });
559 -
560 - $('#view').on('click', function(e) {
561 - if (!$(e.target).closest('.node').length) {
562 - deselectObject();
563 - }
564 - });
565 -
566 - // ...............................................................
567 -
568 - // add links to the display
569 - network.link = network.svg.append('g').attr('id', 'links')
570 - .selectAll('.link')
571 - .data(network.force.links(), function(d) {return d.id})
572 - .enter().append('line')
573 - .attr('class', function(d) {return 'link ' + d.class});
574 -
575 - network.linkSrcPort = network.svg.append('g')
576 - .attr({
577 - id: 'srcPorts',
578 - class: 'portLayer'
579 - });
580 - network.linkTgtPort = network.svg.append('g')
581 - .attr({
582 - id: 'tgtPorts',
583 - class: 'portLayer'
584 - });
585 -
586 - var portVis = portLabelsOn ? 'visible' : 'hidden',
587 - pw = config.labels.port.width,
588 - ph = config.labels.port.height;
589 -
590 - network.link.filter('.infra').each(function(d) {
591 - var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort',
592 - tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort';
593 -
594 - if (d.source.type)
595 -
596 - network.linkSrcPort.append('rect').attr({
597 - id: 'srcPort-' + safeId(d.id),
598 - class: 'port ' + srcType,
599 - width: pw,
600 - height: ph,
601 - rx: 4,
602 - ry: 4
603 - }).style('visibility', portVis);
604 -
605 - network.linkTgtPort.append('rect').attr({
606 - id: 'tgtPort-' + safeId(d.id),
607 - class: 'port ' + tgtType,
608 - width: pw,
609 - height: ph,
610 - rx: 4,
611 - ry: 4
612 - }).style('visibility', portVis);
613 -
614 - network.linkSrcPort.append('text').attr({
615 - id: 'srcText-' + safeId(d.id),
616 - class: 'portText ' + srcType
617 - }).text(d.srcPort)
618 - .style('visibility', portVis);
619 -
620 - network.linkTgtPort.append('text').attr({
621 - id: 'tgtText-' + safeId(d.id),
622 - class: 'portText ' + tgtType
623 - }).text(d.tgtPort)
624 - .style('visibility', portVis);
625 - });
626 -
627 - // ...............................................................
628 -
629 - // add nodes to the display
630 - network.node = network.svg.selectAll('.node')
631 - .data(network.force.nodes(), function(d) {return d.id})
632 - .enter().append('g')
633 - .attr('class', function(d) {
634 - var cls = 'node ' + d.class;
635 - if (d.type) {
636 - cls += ' ' + d.type;
637 - }
638 - return cls;
639 - })
640 - .attr('transform', function(d) {
641 - return translate(d.x, d.y);
642 - })
643 - .call(network.drag)
644 - .on('mouseover', function(d) {
645 - // TODO: show tooltip
646 - if (network.mouseoutTimeout) {
647 - clearTimeout(network.mouseoutTimeout);
648 - network.mouseoutTimeout = null;
649 - }
650 - hoverObject(d);
651 - })
652 - .on('mouseout', function(d) {
653 - // TODO: hide tooltip
654 - if (network.mouseoutTimeout) {
655 - clearTimeout(network.mouseoutTimeout);
656 - network.mouseoutTimeout = null;
657 - }
658 - network.mouseoutTimeout = setTimeout(function() {
659 - hoverObject(null);
660 - }, config.mouseOutTimerDelayMs);
661 - });
662 -
663 -
664 - // deal with device nodes first
665 - network.nodeRect = network.node.filter('.device')
666 - .append('rect')
667 - .attr({
668 - rx: 5,
669 - ry: 5,
670 - width: 100,
671 - height: 12
672 }); 149 });
673 - // note that width/height are adjusted to fit the label text
674 - // then padded, and space made for the icon.
675 -
676 - network.node.filter('.device').each(function(d) {
677 - var node = d3.select(this),
678 - icon = iconUrl(d);
679 -
680 - node.append('text')
681 - // TODO: add label cycle behavior
682 - .text(d.id)
683 - .attr('dy', '1.1em');
684 -
685 - if (icon) {
686 - var cfg = config.icons;
687 - node.append('svg:image')
688 - .attr({
689 - width: cfg.w,
690 - height: cfg.h,
691 - 'xlink:href': icon
692 - });
693 - // note, icon relative positioning (x,y) is done after we have
694 - // adjusted the bounds of the rectangle...
695 - }
696 -
697 - // debug function to show the modelled x,y coordinates of nodes...
698 - if (debug('showNodeXY')) {
699 - node.select('rect').attr('fill-opacity', 0.5);
700 - node.append('circle')
701 - .attr({
702 - class: 'debug',
703 - cx: 0,
704 - cy: 0,
705 - r: '3px'
706 - });
707 - }
708 - });
709 -
710 - // now process host nodes
711 - network.nodeCircle = network.node.filter('.host')
712 - .append('circle')
713 - .attr({
714 - r: config.hostRadius
715 - });
716 -
717 - network.node.filter('.host').each(function(d) {
718 - var node = d3.select(this),
719 - icon = iconUrl(d);
720 -
721 - // debug function to show the modelled x,y coordinates of nodes...
722 - if (debug('showNodeXY')) {
723 - node.select('circle').attr('fill-opacity', 0.5);
724 - node.append('circle')
725 - .attr({
726 - class: 'debug',
727 - cx: 0,
728 - cy: 0,
729 - r: '3px'
730 - });
731 - }
732 - });
733 -
734 - // this function is scheduled to happen soon after the given thread ends
735 - setTimeout(function() {
736 - var lab = config.labels,
737 - portGap = lab.port.gap,
738 - midW = portGap + lab.port.width/ 2,
739 - midH = portGap + lab.port.height / 2;
740 -
741 - // post process the device nodes, to pad their size to fit the
742 - // label text and attach the icon to the right location.
743 - network.node.filter('.device').each(function(d) {
744 - // for every node, recompute size, padding, etc. so text fits
745 - var node = d3.select(this),
746 - text = node.select('text'),
747 - box = adjustRectToFitText(node);
748 -
749 - // now make the computed adjustment
750 - node.select('rect')
751 - .attr(box);
752 -
753 - node.select('image')
754 - .attr('x', box.x + config.icons.xoff)
755 - .attr('y', box.y + config.icons.yoff);
756 -
757 - var bounds = boundsFromBox(box),
758 - portBounds = {
759 - x1: bounds.x1 - midW,
760 - x2: bounds.x2 + midW,
761 - y1: bounds.y1 - midH,
762 - y2: bounds.y2 + midH
763 - };
764 -
765 - // todo: clean up extent and edge work..
766 - d.extent = {
767 - left: bounds.x1 - lab.marginLR,
768 - right: bounds.x2 + lab.marginLR,
769 - top: bounds.y1 - lab.marginTB,
770 - bottom: bounds.y2 + lab.marginTB
771 - };
772 -
773 - d.edge = {
774 - left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
775 - right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
776 - top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
777 - bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
778 - };
779 -
780 - d.portEdge = {
781 - left : new geo.LineSegment(
782 - portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2
783 - ),
784 - right : new geo.LineSegment(
785 - portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2
786 - ),
787 - top : new geo.LineSegment(
788 - portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1
789 - ),
790 - bottom : new geo.LineSegment(
791 - portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2
792 - )
793 - };
794 -
795 - });
796 -
797 - network.numTicks = 0;
798 - network.preventCollisions = false;
799 - network.force.start();
800 - for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
801 - network.force.tick();
802 - }
803 - network.preventCollisions = true;
804 - $('#view').css('visibility', 'visible');
805 - });
806 -
807 -
808 - // returns the newly computed bounding box of the rectangle
809 - function adjustRectToFitText(n) {
810 - var text = n.select('text'),
811 - box = text.node().getBBox(),
812 - lab = config.labels;
813 -
814 - // not sure why n.data() returns an array of 1 element...
815 - var data = n.data()[0];
816 -
817 - text.attr('text-anchor', 'middle')
818 - .attr('y', '-0.8em')
819 - .attr('x', lab.imgPad/2)
820 - ;
821 -
822 - // translate the bbox so that it is centered on [x,y]
823 - box.x = -box.width / 2;
824 - box.y = -box.height / 2;
825 -
826 - // add padding
827 - box.x -= (lab.padLR + lab.imgPad/2);
828 - box.width += lab.padLR * 2 + lab.imgPad;
829 - box.y -= lab.padTB;
830 - box.height += lab.padTB * 2;
831 -
832 - return box;
833 - }
834 -
835 - function boundsFromBox(box) {
836 - return {
837 - x1: box.x,
838 - y1: box.y,
839 - x2: box.x + box.width,
840 - y2: box.y + box.height
841 - };
842 - }
843 -
844 } 150 }
845 151
846 - function iconUrl(d) {
847 - return 'img/' + d.type + '.png';
848 -// return config.iconUrl[d.icon];
849 - }
850 -
851 - function translate(x, y) {
852 - return 'translate(' + x + ',' + y + ')';
853 - }
854 -
855 - // prevents collisions amongst device nodes
856 - function preventCollisions() {
857 - var quadtree = d3.geom.quadtree(network.nodes),
858 - hrad = config.hostRadius;
859 -
860 - network.nodes.forEach(function(n) {
861 - var nx1, nx2, ny1, ny2;
862 152
863 - if (n.class === 'device') { 153 + function load(view, ctx) {
864 - nx1 = n.x + n.extent.left; 154 + view.setRadio(btnSet, doRadio);
865 - nx2 = n.x + n.extent.right;
866 - ny1 = n.y + n.extent.top;
867 - ny2 = n.y + n.extent.bottom;
868 155
869 - } else {
870 - nx1 = n.x - hrad;
871 - nx2 = n.x + hrad;
872 - ny1 = n.y - hrad;
873 - ny2 = n.y + hrad;
874 - }
875 -
876 - quadtree.visit(function(quad, x1, y1, x2, y2) {
877 - if (quad.point && quad.point !== n) {
878 - // check if the rectangles/circles intersect
879 - var p = quad.point,
880 - px1, px2, py1, py2, ix;
881 -
882 - if (p.class === 'device') {
883 - px1 = p.x + p.extent.left;
884 - px2 = p.x + p.extent.right;
885 - py1 = p.y + p.extent.top;
886 - py2 = p.y + p.extent.bottom;
887 -
888 - } else {
889 - px1 = p.x - hrad;
890 - px2 = p.x + hrad;
891 - py1 = p.y - hrad;
892 - py2 = p.y + hrad;
893 - }
894 -
895 - ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
896 -
897 - if (ix) {
898 - var xa1 = nx2 - px1, // shift n left , p right
899 - xa2 = px2 - nx1, // shift n right, p left
900 - ya1 = ny2 - py1, // shift n up , p down
901 - ya2 = py2 - ny1, // shift n down , p up
902 - adj = Math.min(xa1, xa2, ya1, ya2);
903 -
904 - if (adj == xa1) {
905 - n.x -= adj / 2;
906 - p.x += adj / 2;
907 - } else if (adj == xa2) {
908 - n.x += adj / 2;
909 - p.x -= adj / 2;
910 - } else if (adj == ya1) {
911 - n.y -= adj / 2;
912 - p.y += adj / 2;
913 - } else if (adj == ya2) {
914 - n.y += adj / 2;
915 - p.y -= adj / 2;
916 - }
917 - }
918 - return ix;
919 - }
920 - });
921 -
922 - });
923 } 156 }
924 157
925 - function tick(e) { 158 + function resize(view, ctx) {
926 - network.numTicks++; 159 + setSize(view);
927 - 160 + setSize(view, bgImg);
928 - if (config.options.layering) {
929 - // adjust the y-coord of each node, based on y-pos constraints
930 - network.nodes.forEach(function (n) {
931 - var z = e.alpha * n.constraint.weight;
932 - if (!isNaN(n.constraint.y)) {
933 - n.y = (n.constraint.y * z + n.y * (1 - z));
934 - }
935 - });
936 - }
937 -
938 - if (config.options.collisionPrevention && network.preventCollisions) {
939 - preventCollisions();
940 - }
941 -
942 - var portHalfW = config.labels.port.width / 2,
943 - portHalfH = config.labels.port.height / 2;
944 -
945 - // clip visualization of links at bounds of nodes...
946 - network.link.each(function(d) {
947 - var xs = d.source.x,
948 - ys = d.source.y,
949 - xt = d.target.x,
950 - yt = d.target.y,
951 - line = new geo.LineSegment(xs, ys, xt, yt),
952 - e, ix,
953 - exs, eys, ext, eyt,
954 - pxs, pys, pxt, pyt;
955 -
956 - if (d.class === 'host') {
957 - // no adjustment for source end of link, since hosts are dots
958 - exs = xs;
959 - eys = ys;
960 -
961 - } else {
962 - for (e in d.source.edge) {
963 - ix = line.intersect(d.source.edge[e].offset(xs, ys));
964 - if (ix.in1 && ix.in2) {
965 - exs = ix.x;
966 - eys = ix.y;
967 -
968 - // also pick off the port label intersection
969 - ix = line.intersect(d.source.portEdge[e].offset(xs, ys));
970 - pxs = ix.x;
971 - pys = ix.y;
972 - break;
973 - }
974 - }
975 - }
976 -
977 - for (e in d.target.edge) {
978 - ix = line.intersect(d.target.edge[e].offset(xt, yt));
979 - if (ix.in1 && ix.in2) {
980 - ext = ix.x;
981 - eyt = ix.y;
982 -
983 - // also pick off the port label intersection
984 - ix = line.intersect(d.target.portEdge[e].offset(xt, yt));
985 - pxt = ix.x;
986 - pyt = ix.y;
987 - break;
988 - }
989 - }
990 -
991 - // adjust the endpoints of the link's line to match rectangles
992 - var sid = safeId(d.id);
993 - d3.select(this)
994 - .attr('x1', exs)
995 - .attr('y1', eys)
996 - .attr('x2', ext)
997 - .attr('y2', eyt);
998 -
999 - d3.select('#srcPort-' + sid)
1000 - .attr('x', pxs - portHalfW)
1001 - .attr('y', pys - portHalfH);
1002 -
1003 - d3.select('#tgtPort-' + sid)
1004 - .attr('x', pxt - portHalfW)
1005 - .attr('y', pyt - portHalfH);
1006 -
1007 - // TODO: fit label rect to size of port number.
1008 - d3.select('#srcText-' + sid)
1009 - .attr('x', pxs - 5)
1010 - .attr('y', pys + 3);
1011 -
1012 - d3.select('#tgtText-' + sid)
1013 - .attr('x', pxt - 5)
1014 - .attr('y', pyt + 3);
1015 -
1016 - });
1017 -
1018 - // position each node by translating the node (group) by x,y
1019 - network.node
1020 - .attr('transform', function(d) {
1021 - return translate(d.x, d.y);
1022 - });
1023 -
1024 - }
1025 -
1026 - // $('#docs-close').on('click', function() {
1027 - // deselectObject();
1028 - // return false;
1029 - // });
1030 -
1031 - // $(document).on('click', '.select-object', function() {
1032 - // var obj = graph.data[$(this).data('name')];
1033 - // if (obj) {
1034 - // selectObject(obj);
1035 - // }
1036 - // return false;
1037 - // });
1038 -
1039 - function findNodeFromData(d) {
1040 - var el = null;
1041 - network.node.filter('.' + d.class).each(function(n) {
1042 - if (n.id === d.id) {
1043 - el = d3.select(this);
1044 - }
1045 - });
1046 - return el;
1047 - }
1048 -
1049 - function selectObject(obj, el) {
1050 - var node;
1051 - if (el) {
1052 - node = d3.select(el);
1053 - } else {
1054 - network.node.each(function(d) {
1055 - if (d == obj) {
1056 - node = d3.select(el = this);
1057 - }
1058 - });
1059 - }
1060 - if (!node) return;
1061 -
1062 - if (node.classed('selected')) {
1063 - deselectObject();
1064 - flyinPane(null);
1065 - return;
1066 - }
1067 - deselectObject(false);
1068 -
1069 - selected = {
1070 - obj : obj,
1071 - el : el
1072 - };
1073 -
1074 - node.classed('selected', true);
1075 - flyinPane(obj);
1076 - }
1077 -
1078 - function deselectObject(doResize) {
1079 - // Review: logic of 'resize(...)' function.
1080 - if (doResize || typeof doResize == 'undefined') {
1081 - resize(false);
1082 - }
1083 -
1084 - // deselect all nodes in the network...
1085 - network.node.classed('selected', false);
1086 - selected = {};
1087 - flyinPane(null);
1088 - }
1089 -
1090 - function flyinPane(obj) {
1091 - var pane = d3.select('#flyout'),
1092 - url;
1093 -
1094 - if (obj) {
1095 - // go get details of the selected object from the server...
1096 - url = detailJsonUrl(obj.id);
1097 - d3.json(url, function (err, data) {
1098 - if (err) {
1099 - alert('Oops! Error reading JSON...\n\n' +
1100 - 'URL: ' + url + '\n\n' +
1101 - 'Error: ' + err.message);
1102 - return;
1103 - }
1104 -// console.log("JSON data... " + url);
1105 -// console.log(data);
1106 -
1107 - displayDetails(data, pane);
1108 - });
1109 -
1110 - } else {
1111 - // hide pane
1112 - pane.transition().duration(750)
1113 - .style('right', '-320px')
1114 - .style('opacity', 0.0);
1115 - }
1116 } 161 }
1117 162
1118 - function displayDetails(data, pane) {
1119 - $('#flyout').empty();
1120 -
1121 - var title = pane.append("h2"),
1122 - table = pane.append("table"),
1123 - tbody = table.append("tbody");
1124 -
1125 - $('<img src="img/' + data.type + '.png">').appendTo(title);
1126 - $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1127 -
1128 163
1129 - // TODO: consider using d3 data bind to TR/TD 164 + // ==============================
1130 - 165 + // View registration
1131 - data.propOrder.forEach(function(p) {
1132 - if (p === '-') {
1133 - addSep(tbody);
1134 - } else {
1135 - addProp(tbody, p, data.props[p]);
1136 - }
1137 - });
1138 -
1139 - function addSep(tbody) {
1140 - var tr = tbody.append('tr');
1141 - $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1142 - }
1143 -
1144 - function addProp(tbody, label, value) {
1145 - var tr = tbody.append('tr');
1146 -
1147 - tr.append('td')
1148 - .attr('class', 'label')
1149 - .text(label + ' :');
1150 -
1151 - tr.append('td')
1152 - .attr('class', 'value')
1153 - .text(value);
1154 - }
1155 -
1156 - // show pane
1157 - pane.transition().duration(750)
1158 - .style('right', '20px')
1159 - .style('opacity', 1.0);
1160 - }
1161 -
1162 - function highlightObject(obj) {
1163 - if (obj) {
1164 - if (obj != highlighted) {
1165 - // TODO set or clear "inactive" class on nodes, based on criteria
1166 - network.node.classed('inactive', function(d) {
1167 - // return (obj !== d &&
1168 - // d.relation(obj.id));
1169 - return (obj !== d);
1170 - });
1171 - // TODO: same with links
1172 - network.link.classed('inactive', function(d) {
1173 - return (obj !== d.source && obj !== d.target);
1174 - });
1175 - }
1176 - highlighted = obj;
1177 - } else {
1178 - if (highlighted) {
1179 - // clear the inactive flag (no longer suppressed visually)
1180 - network.node.classed('inactive', false);
1181 - network.link.classed('inactive', false);
1182 - }
1183 - highlighted = null;
1184 -
1185 - }
1186 - }
1187 -
1188 - function hoverObject(obj) {
1189 - if (obj) {
1190 - hovered = obj;
1191 - } else {
1192 - if (hovered) {
1193 - hovered = null;
1194 - }
1195 - }
1196 - }
1197 -
1198 -
1199 - function resize() {
1200 - view.height = window.innerHeight - config.mastHeight;
1201 - view.width = window.innerWidth;
1202 - $('#view')
1203 - .css('height', view.height + 'px')
1204 - .css('width', view.width + 'px');
1205 -
1206 - network.forceWidth = view.width - config.force.marginLR;
1207 - network.forceHeight = view.height - config.force.marginTB;
1208 - }
1209 -
1210 - // ======================================================================
1211 - // register with the UI framework
1212 166
1213 onos.ui.addView('topo', { 167 onos.ui.addView('topo', {
1214 - load: loadNetworkView 168 + preload: preload,
169 + load: load,
170 + resize: resize
1215 }); 171 });
1216 172
1217 -
1218 }(ONOS)); 173 }(ONOS));
1219 -
......