blob: 8d1f9c15199fbd13cd86bbc5064283caaae2c56b [file] [log] [blame]
Simon Hunt195cb382014-11-03 17:50:51 -08001/*
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
Simon Hunt195cb382014-11-03 17:50:51 -080026 // 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 view = {},
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', view.width)
460 .attr('height', view.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: view.width,
470 height: view.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 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
Simon Hunt25248912014-11-04 11:25:48 -08001213 onos.ui.addView('topo', {
Simon Hunt195cb382014-11-03 17:50:51 -08001214 load: loadNetworkView
1215 });
1216
1217
1218}(ONOS));
1219