Simon Hunt

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

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

Change-Id: I501f4ab6eded8393948cede903573580599258b1
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 + Utility functions for D3 visualizations.
19 +
20 + @author Simon Hunt
21 + */
22 +
23 +(function (onos) {
24 + 'use strict';
25 +
26 + function createDragBehavior(force, selectCb, atDragEnd) {
27 + var draggedThreshold = d3.scale.linear()
28 + .domain([0, 0.1])
29 + .range([5, 20])
30 + .clamp(true),
31 + drag;
32 +
33 + // TODO: better validation of parameters
34 + if (!$.isFunction(selectCb)) {
35 + alert('d3util.createDragBehavior(): selectCb is not a function')
36 + }
37 + if (!$.isFunction(atDragEnd)) {
38 + alert('d3util.createDragBehavior(): atDragEnd is not a function')
39 + }
40 +
41 + function dragged(d) {
42 + var threshold = draggedThreshold(force.alpha()),
43 + dx = d.oldX - d.px,
44 + dy = d.oldY - d.py;
45 + if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
46 + d.dragged = true;
47 + }
48 + return d.dragged;
49 + }
50 +
51 + drag = d3.behavior.drag()
52 + .origin(function(d) { return d; })
53 + .on('dragstart', function(d) {
54 + d.oldX = d.x;
55 + d.oldY = d.y;
56 + d.dragged = false;
57 + d.fixed |= 2;
58 + })
59 + .on('drag', function(d) {
60 + d.px = d3.event.x;
61 + d.py = d3.event.y;
62 + if (dragged(d)) {
63 + if (!force.alpha()) {
64 + force.alpha(.025);
65 + }
66 + }
67 + })
68 + .on('dragend', function(d) {
69 + if (!dragged(d)) {
70 + // consider this the same as a 'click' (selection of node)
71 + selectCb(d, this); // TODO: set 'this' context instead of param
72 + }
73 + d.fixed &= ~6;
74 +
75 + // hook at the end of a drag gesture
76 + atDragEnd(d, this); // TODO: set 'this' context instead of param
77 + });
78 +
79 + return drag;
80 + }
81 +
82 + function appendGlow(svg) {
83 + // TODO: parameterize color
84 +
85 + var glow = svg.append('filter')
86 + .attr('x', '-50%')
87 + .attr('y', '-50%')
88 + .attr('width', '200%')
89 + .attr('height', '200%')
90 + .attr('id', 'blue-glow');
91 +
92 + glow.append('feColorMatrix')
93 + .attr('type', 'matrix')
94 + .attr('values', '0 0 0 0 0 ' +
95 + '0 0 0 0 0 ' +
96 + '0 0 0 0 .7 ' +
97 + '0 0 0 1 0 ');
98 +
99 + glow.append('feGaussianBlur')
100 + .attr('stdDeviation', 3)
101 + .attr('result', 'coloredBlur');
102 +
103 + glow.append('feMerge').selectAll('feMergeNode')
104 + .data(['coloredBlur', 'SourceGraphic'])
105 + .enter().append('feMergeNode')
106 + .attr('in', String);
107 + }
108 +
109 + // === register the functions as a library
110 + onos.ui.addLib('d3util', {
111 + createDragBehavior: createDragBehavior,
112 + appendGlow: appendGlow
113 + });
114 +
115 +}(ONOS));
...@@ -64,6 +64,9 @@ ...@@ -64,6 +64,9 @@
64 <div id="overlays"> 64 <div id="overlays">
65 <!-- NOTE: overlays injected here, as needed --> 65 <!-- NOTE: overlays injected here, as needed -->
66 </div> 66 </div>
67 + <div id="alerts">
68 + <!-- NOTE: alert content injected here, as needed -->
69 + </div>
67 </div> 70 </div>
68 71
69 <!-- Initialize the UI...--> 72 <!-- Initialize the UI...-->
...@@ -76,6 +79,9 @@ ...@@ -76,6 +79,9 @@
76 }); 79 });
77 </script> 80 </script>
78 81
82 + <!-- Library module files included here -->
83 + <script src="d3Utils.js"></script>
84 +
79 <!-- Framework module files included here --> 85 <!-- Framework module files included here -->
80 <script src="mast2.js"></script> 86 <script src="mast2.js"></script>
81 87
......
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
10 "?" 10 "?"
11 ], 11 ],
12 "metaUi": { 12 "metaUi": {
13 - "x": 832, 13 + "Zx": 832,
14 - "y": 223 14 + "Zy": 223
15 } 15 }
16 } 16 }
17 } 17 }
......
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
10 "?" 10 "?"
11 ], 11 ],
12 "metaUi": { 12 "metaUi": {
13 - "x": 840, 13 + "Zx": 840,
14 - "y": 290 14 + "Zy": 290
15 } 15 }
16 } 16 }
17 } 17 }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 "dst": "of:0000ffffffffff05", 6 "dst": "of:0000ffffffffff05",
7 "dstPort": "10", 7 "dstPort": "10",
8 "type": "optical", 8 "type": "optical",
9 - "linkWidth": 2, 9 + "linkWidth": 6,
10 "props" : { 10 "props" : {
11 "BW": "80 G" 11 "BW": "80 G"
12 } 12 }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 "dst": "of:0000ffffffffff05", 6 "dst": "of:0000ffffffffff05",
7 "dstPort": "30", 7 "dstPort": "30",
8 "type": "optical", 8 "type": "optical",
9 - "linkWidth": 2, 9 + "linkWidth": 6,
10 "props" : { 10 "props" : {
11 "BW": "70 G" 11 "BW": "70 G"
12 } 12 }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 "dst": "of:0000ffffffffff08", 6 "dst": "of:0000ffffffffff08",
7 "dstPort": "20", 7 "dstPort": "20",
8 "type": "optical", 8 "type": "optical",
9 - "linkWidth": 2, 9 + "linkWidth": 6,
10 "props" : { 10 "props" : {
11 "BW": "70 G" 11 "BW": "70 G"
12 } 12 }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 "dst": "of:0000ffffffffff08", 6 "dst": "of:0000ffffffffff08",
7 "dstPort": "30", 7 "dstPort": "30",
8 "type": "optical", 8 "type": "optical",
9 - "linkWidth": 2, 9 + "linkWidth": 6,
10 "props" : { 10 "props" : {
11 "BW": "70 G" 11 "BW": "70 G"
12 } 12 }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
6 "dst": "of:0000ffffffffff08", 6 "dst": "of:0000ffffffffff08",
7 "dstPort": "10", 7 "dstPort": "10",
8 "type": "optical", 8 "type": "optical",
9 - "linkWidth": 2, 9 + "linkWidth": 6,
10 "props" : { 10 "props" : {
11 "BW": "70 G" 11 "BW": "70 G"
12 } 12 }
......
1 +{
2 + "event": "addLink",
3 + "payload": {
4 + "src": "of:0000ffffffffff04",
5 + "srcPort": "27",
6 + "dst": "of:0000ffffffffff08",
7 + "dstPort": "10",
8 + "type": "optical",
9 + "linkWidth": 2,
10 + "props" : {
11 + "BW": "30 G"
12 + }
13 + }
14 +}
...@@ -32,6 +32,34 @@ div.onosView.currentView { ...@@ -32,6 +32,34 @@ div.onosView.currentView {
32 display: block; 32 display: block;
33 } 33 }
34 34
35 +div#alerts {
36 + display: none;
37 + position: absolute;
38 + z-index: 2000;
39 + opacity: 0.65;
40 + background-color: #006;
41 + color: white;
42 + top: 80px;
43 + left: 40px;
44 + padding: 3px 6px;
45 + box-shadow: 4px 6px 12px #777;
46 +}
47 +
48 +div#alerts pre {
49 + margin: 0.2em 6px;
50 +}
51 +
52 +div#alerts span.close {
53 + color: #6af;
54 + float: right;
55 + right: 2px;
56 + cursor: pointer;
57 +}
58 +
59 +div#alerts span.close:hover {
60 + color: #fff;
61 +}
62 +
35 /* 63 /*
36 * ============================================================== 64 * ==============================================================
37 * END OF NEW ONOS.JS file 65 * END OF NEW ONOS.JS file
...@@ -54,12 +82,6 @@ svg #bg { ...@@ -54,12 +82,6 @@ svg #bg {
54 * Network Graph elements ====================================== 82 * Network Graph elements ======================================
55 */ 83 */
56 84
57 -svg .link {
58 - opacity: .7;
59 -}
60 -
61 -svg .link.host {
62 -}
63 85
64 svg g.portLayer rect.port { 86 svg g.portLayer rect.port {
65 fill: #ccc; 87 fill: #ccc;
...@@ -70,33 +92,13 @@ svg g.portLayer text { ...@@ -70,33 +92,13 @@ svg g.portLayer text {
70 pointer-events: none; 92 pointer-events: none;
71 } 93 }
72 94
73 -svg .node.device rect {
74 - stroke-width: 1.5px;
75 -}
76 -
77 -svg .node.device.fixed rect {
78 - stroke-width: 1.5;
79 - stroke: #ccc;
80 -}
81 95
82 -svg .node.device.roadm rect {
83 - fill: #03c;
84 -}
85 -
86 -svg .node.device.switch rect {
87 - fill: #06f;
88 -}
89 96
90 svg .node.host circle { 97 svg .node.host circle {
91 fill: #c96; 98 fill: #c96;
92 stroke: #000; 99 stroke: #000;
93 } 100 }
94 101
95 -svg .node text {
96 - fill: white;
97 - font: 10pt sans-serif;
98 - pointer-events: none;
99 -}
100 102
101 /* for debugging */ 103 /* for debugging */
102 svg .node circle.debug { 104 svg .node circle.debug {
...@@ -110,10 +112,6 @@ svg .node rect.debug { ...@@ -110,10 +112,6 @@ svg .node rect.debug {
110 } 112 }
111 113
112 114
113 -svg .node.selected rect,
114 -svg .node.selected circle {
115 - filter: url(#blue-glow);
116 -}
117 115
118 svg .link.inactive, 116 svg .link.inactive,
119 svg .port.inactive, 117 svg .port.inactive,
......
...@@ -32,7 +32,8 @@ ...@@ -32,7 +32,8 @@
32 $.onos = function (options) { 32 $.onos = function (options) {
33 var uiApi, 33 var uiApi,
34 viewApi, 34 viewApi,
35 - navApi; 35 + navApi,
36 + libApi;
36 37
37 var defaultOptions = { 38 var defaultOptions = {
38 trace: false, 39 trace: false,
...@@ -331,6 +332,58 @@ ...@@ -331,6 +332,58 @@
331 } 332 }
332 } 333 }
333 334
335 + var alerts = {
336 + open: false,
337 + count: 0
338 + };
339 +
340 + function createAlerts() {
341 + var al = d3.select('#alerts')
342 + .style('display', 'block');
343 + al.append('span')
344 + .attr('class', 'close')
345 + .text('X')
346 + .on('click', closeAlerts);
347 + al.append('pre');
348 + alerts.open = true;
349 + alerts.count = 0;
350 + }
351 +
352 + function closeAlerts() {
353 + d3.select('#alerts')
354 + .style('display', 'none');
355 + d3.select('#alerts span').remove();
356 + d3.select('#alerts pre').remove();
357 + alerts.open = false;
358 + }
359 +
360 + function addAlert(msg) {
361 + var lines,
362 + oldContent;
363 +
364 + if (alerts.count) {
365 + oldContent = d3.select('#alerts pre').html();
366 + }
367 +
368 + lines = msg.split('\n');
369 + lines[0] += ' '; // spacing so we don't crowd 'X'
370 + lines = lines.join('\n');
371 +
372 + if (oldContent) {
373 + lines += '\n----\n' + oldContent;
374 + }
375 +
376 + d3.select('#alerts pre').html(lines);
377 + alerts.count++;
378 + }
379 +
380 + function doAlert(msg) {
381 + if (!alerts.open) {
382 + createAlerts();
383 + }
384 + addAlert(msg);
385 + }
386 +
334 function keyIn() { 387 function keyIn() {
335 var event = d3.event, 388 var event = d3.event,
336 keyCode = event.keyCode, 389 keyCode = event.keyCode,
...@@ -408,7 +461,8 @@ ...@@ -408,7 +461,8 @@
408 uid: this.uid, 461 uid: this.uid,
409 setRadio: this.setRadio, 462 setRadio: this.setRadio,
410 setKeys: this.setKeys, 463 setKeys: this.setKeys,
411 - dataLoadError: this.dataLoadError 464 + dataLoadError: this.dataLoadError,
465 + alert: this.alert
412 } 466 }
413 }, 467 },
414 468
...@@ -501,14 +555,20 @@ ...@@ -501,14 +555,20 @@
501 return makeUid(this, id); 555 return makeUid(this, id);
502 }, 556 },
503 557
504 - // TODO : implement custom dialogs (don't use alerts) 558 + // TODO : implement custom dialogs
559 +
560 + // Consider enhancing alert mechanism to handle multiples
561 + // as individually closable.
562 + alert: function (msg) {
563 + doAlert(msg);
564 + },
505 565
506 dataLoadError: function (err, url) { 566 dataLoadError: function (err, url) {
507 var msg = 'Data Load Error\n\n' + 567 var msg = 'Data Load Error\n\n' +
508 err.status + ' -- ' + err.statusText + '\n\n' + 568 err.status + ' -- ' + err.statusText + '\n\n' +
509 'relative-url: "' + url + '"\n\n' + 569 'relative-url: "' + url + '"\n\n' +
510 'complete-url: "' + err.responseURL + '"'; 570 'complete-url: "' + err.responseURL + '"';
511 - alert(msg); 571 + this.alert(msg);
512 } 572 }
513 573
514 // TODO: consider schedule, clearTimer, etc. 574 // TODO: consider schedule, clearTimer, etc.
...@@ -521,6 +581,12 @@ ...@@ -521,6 +581,12 @@
521 // UI API 581 // UI API
522 582
523 uiApi = { 583 uiApi = {
584 + addLib: function (libName, api) {
585 + // TODO: validation of args
586 + libApi[libName] = api;
587 + },
588 +
589 + // TODO: it remains to be seen whether we keep this style of docs
524 /** @api ui addView( vid, nid, cb ) 590 /** @api ui addView( vid, nid, cb )
525 * Adds a view to the UI. 591 * Adds a view to the UI.
526 * <p> 592 * <p>
...@@ -590,6 +656,12 @@ ...@@ -590,6 +656,12 @@
590 }; 656 };
591 657
592 // .......................................................... 658 // ..........................................................
659 + // Library API
660 + libApi = {
661 +
662 + };
663 +
664 + // ..........................................................
593 // Exported API 665 // Exported API
594 666
595 // function to be called from index.html to build the ONOS UI 667 // function to be called from index.html to build the ONOS UI
...@@ -623,7 +695,8 @@ ...@@ -623,7 +695,8 @@
623 // export the api and build-UI function 695 // export the api and build-UI function
624 return { 696 return {
625 ui: uiApi, 697 ui: uiApi,
626 - view: viewApi, 698 + lib: libApi,
699 + //view: viewApi,
627 nav: navApi, 700 nav: navApi,
628 buildUi: buildOnosUi 701 buildUi: buildOnosUi
629 }; 702 };
......
...@@ -24,12 +24,18 @@ svg #topo-bg { ...@@ -24,12 +24,18 @@ svg #topo-bg {
24 opacity: 0.5; 24 opacity: 0.5;
25 } 25 }
26 26
27 +/* NODES */
28 +
27 svg .node.device { 29 svg .node.device {
28 stroke: none; 30 stroke: none;
29 stroke-width: 1.5px; 31 stroke-width: 1.5px;
30 cursor: pointer; 32 cursor: pointer;
31 } 33 }
32 34
35 +svg .node.device rect {
36 + stroke-width: 1.5px;
37 +}
38 +
33 svg .node.device.fixed rect { 39 svg .node.device.fixed rect {
34 stroke-width: 1.5; 40 stroke-width: 1.5;
35 stroke: #ccc; 41 stroke: #ccc;
...@@ -50,6 +56,17 @@ svg .node text { ...@@ -50,6 +56,17 @@ svg .node text {
50 pointer-events: none; 56 pointer-events: none;
51 } 57 }
52 58
59 +svg .node.selected rect,
60 +svg .node.selected circle {
61 + filter: url(#blue-glow);
62 +}
63 +
64 +/* LINKS */
65 +
66 +svg .link {
67 + opacity: .7;
68 +}
69 +
53 /* for debugging */ 70 /* for debugging */
54 svg .node circle.debug { 71 svg .node circle.debug {
55 fill: white; 72 fill: white;
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
23 (function (onos) { 23 (function (onos) {
24 'use strict'; 24 'use strict';
25 25
26 + // shorter names for library APIs
27 + var d3u = onos.lib.d3util;
28 +
26 // configuration data 29 // configuration data
27 var config = { 30 var config = {
28 useLiveData: false, 31 useLiveData: false,
...@@ -61,6 +64,10 @@ ...@@ -61,6 +64,10 @@
61 height: 14 64 height: 14
62 } 65 }
63 }, 66 },
67 + topo: {
68 + linkInColor: '#66f',
69 + linkInWidth: 14
70 + },
64 icons: { 71 icons: {
65 w: 28, 72 w: 28,
66 h: 28, 73 h: 28,
...@@ -106,7 +113,9 @@ ...@@ -106,7 +113,9 @@
106 // key bindings 113 // key bindings
107 var keyDispatch = { 114 var keyDispatch = {
108 space: injectTestEvent, // TODO: remove (testing only) 115 space: injectTestEvent, // TODO: remove (testing only)
109 - // M: testMe, // TODO: remove (testing only) 116 + S: injectStartupEvents, // TODO: remove (testing only)
117 + A: testAlert, // TODO: remove (testing only)
118 + M: testMe, // TODO: remove (testing only)
110 119
111 B: toggleBg, 120 B: toggleBg,
112 G: toggleLayout, 121 G: toggleLayout,
...@@ -141,7 +150,8 @@ ...@@ -141,7 +150,8 @@
141 // For Debugging / Development 150 // For Debugging / Development
142 151
143 var eventPrefix = 'json/eventTest_', 152 var eventPrefix = 'json/eventTest_',
144 - eventNumber = 0; 153 + eventNumber = 0,
154 + alertNumber = 0;
145 155
146 function note(label, msg) { 156 function note(label, msg) {
147 console.log('NOTE: ' + label + ': ' + msg); 157 console.log('NOTE: ' + label + ': ' + msg);
...@@ -155,22 +165,12 @@ ...@@ -155,22 +165,12 @@
155 // ============================== 165 // ==============================
156 // Key Callbacks 166 // Key Callbacks
157 167
168 + function testAlert(view) {
169 + alertNumber++;
170 + view.alert("Test me! -- " + alertNumber);
171 + }
172 +
158 function testMe(view) { 173 function testMe(view) {
159 - svg.append('line')
160 - .attr({
161 - x1: 100,
162 - y1: 100,
163 - x2: 500,
164 - y2: 400,
165 - stroke: '#2f3',
166 - 'stroke-width': 8
167 - })
168 - .transition()
169 - .duration(1200)
170 - .attr({
171 - stroke: '#666',
172 - 'stroke-width': 6
173 - });
174 } 174 }
175 175
176 function injectTestEvent(view) { 176 function injectTestEvent(view) {
...@@ -187,6 +187,13 @@ ...@@ -187,6 +187,13 @@
187 }); 187 });
188 } 188 }
189 189
190 + function injectStartupEvents(view) {
191 + var lastStartupEvent = 32;
192 + while (eventNumber < lastStartupEvent) {
193 + injectTestEvent(view);
194 + }
195 + }
196 +
190 function toggleBg() { 197 function toggleBg() {
191 var vis = bgImg.style('visibility'); 198 var vis = bgImg.style('visibility');
192 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden'); 199 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
...@@ -370,6 +377,11 @@ ...@@ -370,6 +377,11 @@
370 return lnk; 377 return lnk;
371 } 378 }
372 379
380 + function linkWidth(w) {
381 + // w is number of links between nodes. Scale appropriately.
382 + return w * 1.2;
383 + }
384 +
373 function updateLinks() { 385 function updateLinks() {
374 link = linkG.selectAll('.link') 386 link = linkG.selectAll('.link')
375 .data(network.links, function (d) { return d.id; }); 387 .data(network.links, function (d) { return d.id; });
...@@ -387,12 +399,12 @@ ...@@ -387,12 +399,12 @@
387 y1: function (d) { return d.y1; }, 399 y1: function (d) { return d.y1; },
388 x2: function (d) { return d.x2; }, 400 x2: function (d) { return d.x2; },
389 y2: function (d) { return d.y2; }, 401 y2: function (d) { return d.y2; },
390 - stroke: '#66f', 402 + stroke: config.topo.linkInColor,
391 - 'stroke-width': 10 403 + 'stroke-width': config.topo.linkInWidth
392 }) 404 })
393 .transition().duration(1000) 405 .transition().duration(1000)
394 .attr({ 406 .attr({
395 - 'stroke-width': function (d) { return d.width; }, 407 + 'stroke-width': function (d) { return linkWidth(d.width); },
396 stroke: '#666' // TODO: remove explicit stroke, rather... 408 stroke: '#666' // TODO: remove explicit stroke, rather...
397 }); 409 });
398 410
...@@ -461,6 +473,10 @@ ...@@ -461,6 +473,10 @@
461 return box; 473 return box;
462 } 474 }
463 475
476 + function mkSvgClass(d) {
477 + return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
478 + }
479 +
464 function updateNodes() { 480 function updateNodes() {
465 node = nodeG.selectAll('.node') 481 node = nodeG.selectAll('.node')
466 .data(network.nodes, function (d) { return d.id; }); 482 .data(network.nodes, function (d) { return d.id; });
...@@ -473,11 +489,11 @@ ...@@ -473,11 +489,11 @@
473 .append('g') 489 .append('g')
474 .attr({ 490 .attr({
475 id: function (d) { return safeId(d.id); }, 491 id: function (d) { return safeId(d.id); },
476 - class: function (d) { return d.svgClass; }, 492 + class: mkSvgClass,
477 transform: function (d) { return translate(d.x, d.y); }, 493 transform: function (d) { return translate(d.x, d.y); },
478 opacity: 0 494 opacity: 0
479 }) 495 })
480 - //.call(network.drag) 496 + .call(network.drag)
481 //.on('mouseover', function (d) {}) 497 //.on('mouseover', function (d) {})
482 //.on('mouseover', function (d) {}) 498 //.on('mouseover', function (d) {})
483 .transition() 499 .transition()
...@@ -578,6 +594,9 @@ ...@@ -578,6 +594,9 @@
578 svg = view.$div.append('svg'); 594 svg = view.$div.append('svg');
579 setSize(svg, view); 595 setSize(svg, view);
580 596
597 + // add blue glow filter to svg layer
598 + d3u.appendGlow(svg);
599 +
581 // load the background image 600 // load the background image
582 bgImg = svg.append('svg:image') 601 bgImg = svg.append('svg:image')
583 .attr({ 602 .attr({
...@@ -612,6 +631,20 @@ ...@@ -612,6 +631,20 @@
612 return fcfg.charge[d.class] || -200; 631 return fcfg.charge[d.class] || -200;
613 } 632 }
614 633
634 + function selectCb(d, self) {
635 + // TODO: selectObject(d, self);
636 + }
637 +
638 + function atDragEnd(d, self) {
639 + // once we've finished moving, pin the node in position,
640 + // if it is a device (not a host)
641 + if (d.class === 'device') {
642 + d.fixed = true;
643 + d3.select(self).classed('fixed', true)
644 + // TODO: send new [x,y] back to server, via websocket.
645 + }
646 + }
647 +
615 // set up the force layout 648 // set up the force layout
616 network.force = d3.layout.force() 649 network.force = d3.layout.force()
617 .size(forceDim) 650 .size(forceDim)
...@@ -621,8 +654,9 @@ ...@@ -621,8 +654,9 @@
621 .linkDistance(ldist) 654 .linkDistance(ldist)
622 .linkStrength(lstrg) 655 .linkStrength(lstrg)
623 .on('tick', tick); 656 .on('tick', tick);
624 - }
625 657
658 + network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
659 + }
626 660
627 function load(view, ctx) { 661 function load(view, ctx) {
628 // cache the view token, so network topo functions can access it 662 // cache the view token, so network topo functions can access it
......