blob: 9b4dc5e59484bc922b79786908a57459d0e3e1cd [file] [log] [blame]
Steven Burrowsec1f45c2016-08-08 16:14:41 +01001/*
2 * Copyright 2016-present 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 GUI -- Topology Layout Module.
19 Module that contains the d3.force.layout logic
20 */
21
22(function () {
23 'use strict';
24
Steven Burrows9edc7e02016-08-29 11:52:07 +010025 var $log, sus, t2rs, t2d3, t2vs, t2ss;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010026
Steven Burrows9edc7e02016-08-29 11:52:07 +010027 var uplink, linkG, linkLabelG, numLinkLabelsG, nodeG, portLabelG;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010028 var link, linkLabel, node;
29
Steven Burrows9edc7e02016-08-29 11:52:07 +010030 var nodes, links, highlightedLink;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010031
32 var force;
33
34 // default settings for force layout
35 var defaultSettings = {
36 gravity: 0.4,
37 friction: 0.7,
38 charge: {
39 // note: key is node.class
40 device: -8000,
41 host: -5000,
42 _def_: -12000
43 },
44 linkDistance: {
45 // note: key is link.type
46 direct: 100,
47 optical: 120,
48 hostLink: 3,
49 _def_: 50
50 },
51 linkStrength: {
52 // note: key is link.type
53 // range: {0.0 ... 1.0}
54 //direct: 1.0,
55 //optical: 1.0,
56 //hostLink: 1.0,
57 _def_: 1.0
58 }
59 };
60
61 // configuration
62 var linkConfig = {
63 light: {
64 baseColor: '#939598',
65 inColor: '#66f',
66 outColor: '#f00'
67 },
68 dark: {
69 // TODO : theme
70 baseColor: '#939598',
71 inColor: '#66f',
72 outColor: '#f00'
73 },
74 inWidth: 12,
75 outWidth: 10
76 };
77
78 // internal state
79 var settings, // merged default settings and options
80 force, // force layout object
81 drag, // drag behavior handler
82 network = {
83 nodes: [],
84 links: [],
85 linksByDevice: {},
86 lookup: {},
87 revLinkToKey: {}
88 },
89 lu, // shorthand for lookup
90 rlk, // shorthand for revLinktoKey
91 showHosts = false, // whether hosts are displayed
92 showOffline = true, // whether offline devices are displayed
93 nodeLock = false, // whether nodes can be dragged or not (locked)
94 fTimer, // timer for delayed force layout
95 fNodesTimer, // timer for delayed nodes update
96 fLinksTimer, // timer for delayed links update
97 dim, // the dimensions of the force layout [w,h]
98 linkNums = []; // array of link number labels
99
100 var tickStuff = {
101 nodeAttr: {
102 transform: function (d) {
103 var dx = isNaN(d.x) ? 0 : d.x,
104 dy = isNaN(d.y) ? 0 : d.y;
105 return sus.translate(dx, dy);
106 }
107 },
108 linkAttr: {
109 x1: function (d) { return d.get('position').x1; },
110 y1: function (d) { return d.get('position').y1; },
111 x2: function (d) { return d.get('position').x2; },
112 y2: function (d) { return d.get('position').y2; }
113 },
114 linkLabelAttr: {
115 transform: function (d) {
116 var lnk = tms.findLinkById(d.get('key'));
117 if (lnk) {
118 return t2d3.transformLabel(lnk.get('position'));
119 }
120 }
121 }
122 };
123
124 function init(_svg_, forceG, _uplink_, _dim_, opts) {
125
126 $log.debug("Initialising Topology Layout");
Steven Burrows9edc7e02016-08-29 11:52:07 +0100127 uplink = _uplink_;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100128 settings = angular.extend({}, defaultSettings, opts);
129
130 linkG = forceG.append('g').attr('id', 'topo-links');
131 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
132 numLinkLabelsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
133 nodeG = forceG.append('g').attr('id', 'topo-nodes');
134 portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
135
136 link = linkG.selectAll('.link');
137 linkLabel = linkLabelG.selectAll('.linkLabel');
138 node = nodeG.selectAll('.node');
139
140 force = d3.layout.force()
141 .size(t2vs.getDimensions())
142 .nodes(t2rs.regionNodes())
143 .links(t2rs.regionLinks())
144 .gravity(settings.gravity)
145 .friction(settings.friction)
146 .charge(settings.charge._def_)
147 .linkDistance(settings.linkDistance._def_)
148 .linkStrength(settings.linkStrength._def_)
149 .on('tick', tick);
Steven Burrows9edc7e02016-08-29 11:52:07 +0100150
151 drag = sus.createDragBehavior(force,
152 t2ss.selectObject, atDragEnd, dragEnabled, clickEnabled);
153
154 _svg_.on('mousemove', mouseMoveHandler)
155 }
156
157 function zoomingOrPanning(ev) {
158 return ev.metaKey || ev.altKey;
159 }
160
161 function atDragEnd(d) {
162 // once we've finished moving, pin the node in position
163 d.fixed = true;
164 d3.select(this).classed('fixed', true);
165 // TODO: sendUpdateMeta(d);
166 t2ss.clickConsumed(true);
167 }
168
169 // predicate that indicates when dragging is active
170 function dragEnabled() {
171 var ev = d3.event.sourceEvent;
172 // nodeLock means we aren't allowing nodes to be dragged...
173 return !nodeLock && !zoomingOrPanning(ev);
174 }
175
176 // predicate that indicates when clicking is active
177 function clickEnabled() {
178 return true;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100179 }
180
181 function tick() {
182 // guard against null (which can happen when our view pages out)...
183 if (node && node.size()) {
184 node.attr(tickStuff.nodeAttr);
185 }
186 if (link && link.size()) {
187 link.call(calcPosition)
188 .attr(tickStuff.linkAttr);
189 // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
190 }
191 if (linkLabel && linkLabel.size()) {
192 linkLabel.attr(tickStuff.linkLabelAttr);
193 }
194 }
195
196 function update() {
197 _updateNodes();
198 _updateLinks();
199 }
200
201 function _updateNodes() {
202
203 var regionNodes = t2rs.regionNodes();
204
205 // select all the nodes in the layout:
206 node = nodeG.selectAll('.node')
207 .data(regionNodes, function (d) { return d.get('id'); });
208
209 var entering = node.enter()
210 .append('g')
211 .attr({
212 id: function (d) { return sus.safeId(d.get('id')); },
213 class: function (d) { return d.svgClassName() },
214 transform: function (d) {
215 // Need to guard against NaN here ??
216 return sus.translate(d.node.x, d.node.y);
217 },
218 opacity: 0
219 })
Steven Burrows9edc7e02016-08-29 11:52:07 +0100220 .call(drag)
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100221 // .on('mouseover', tss.nodeMouseOver)
222 // .on('mouseout', tss.nodeMouseOut)
223 .transition()
224 .attr('opacity', 1);
225
Steven Burrows6deb4ce2016-08-26 16:06:23 +0100226 entering.filter('.device').each(t2d3.nodeEnter);
227 entering.filter('.sub-region').each(t2d3.nodeEnter);
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100228 entering.filter('.host').each(t2d3.hostEnter);
229
230 // operate on both existing and new nodes:
231 // node.filter('.device').each(function (device) {
232 // t2d3.updateDeviceColors(device);
233 // });
234 }
235
236 function _updateLinks() {
237
238 // var th = ts.theme();
239 var regionLinks = t2rs.regionLinks();
240
241 link = linkG.selectAll('.link')
242 .data(regionLinks, function (d) { return d.get('key'); });
243
244 // operate on existing links:
245 link.each(function (d) {
246 // this is supposed to be an existing link, but we have observed
247 // occasions (where links are deleted and added rapidly?) where
248 // the DOM element has not been defined. So protect against that...
249 if (d.el) {
250 restyleLinkElement(d, true);
251 }
252 });
253
254 // operate on entering links:
255 var entering = link.enter()
256 .append('line')
257 .call(calcPosition)
258 .attr({
259 x1: function (d) { return d.get('position').x1; },
260 y1: function (d) { return d.get('position').y1; },
261 x2: function (d) { return d.get('position').x2; },
262 y2: function (d) { return d.get('position').y2; },
263 stroke: linkConfig['light'].inColor,
264 'stroke-width': linkConfig.inWidth
265 });
266
267 entering.each(t2d3.linkEntering);
268
269 // operate on both existing and new links:
270 //link.each(...)
271
272 // add labels for how many links are in a thick line
273 // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
274
275 // apply or remove labels
276 // t2d3.applyLinkLabels();
277
278 // operate on exiting links:
279 link.exit()
280 .attr('stroke-dasharray', '3 3')
281 .attr('stroke', linkConfig['light'].outColor)
282 .style('opacity', 0.5)
283 .transition()
284 .duration(1500)
285 .attr({
286 'stroke-dasharray': '3 12',
287 'stroke-width': linkConfig.outWidth
288 })
289 .style('opacity', 0.0)
290 .remove();
291 }
292
293 function calcPosition() {
294 var lines = this,
295 linkSrcId,
296 linkNums = [];
297
298 lines.each(function (d) {
299 if (d.get('type') === 'hostLink') {
300 d.set('position', getDefaultPos(d));
301 }
302 });
303
304 function normalizeLinkSrc(link) {
305 // ensure source device is consistent across set of links
306 // temporary measure until link modeling is refactored
307 if (!linkSrcId) {
308 linkSrcId = link.source.id;
309 return false;
310 }
311
312 return link.source.id !== linkSrcId;
313 }
314
315 lines.each(function (d) {
316 d.set('position', getDefaultPos(d));
317 });
318 }
319
320 function getDefaultPos(link) {
321
322 return {
323 x1: link.get('source').x,
324 y1: link.get('source').y,
325 x2: link.get('target').x,
326 y2: link.get('target').y
327 };
328 }
329
330 function setDimensions() {
331 if (force) {
332 force.size(t2vs.getDimensions());
333 }
334 }
335
336
337 function start() {
338 force.start();
339 }
340
Steven Burrows9edc7e02016-08-29 11:52:07 +0100341 // Mouse Events
342 function mouseMoveHandler() {
343 var mp = getLogicalMousePosition(this),
344 link = computeNearestLink(mp);
345
346
347 if (highlightedLink) {
348 highlightedLink.unenhance();
349 highlightedLink = null;
350 }
351
352 if (link) {
353 link.enhance();
354 highlightedLink = link;
355 }
356 }
357
358 // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
359
360 function getLogicalMousePosition(container) {
361 var m = d3.mouse(container),
362 sc = uplink.zoomer().scale(),
363 tr = uplink.zoomer().translate(),
364 mx = (m[0] - tr[0]) / sc,
365 my = (m[1] - tr[1]) / sc;
366 return {x: mx, y: my};
367 }
368
369
370 function sq(x) { return x * x; }
371
372 function mdist(p, m) {
373 return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
374 }
375
376 function prox(dist) {
377 return dist / uplink.zoomer().scale();
378 }
379
380 function computeNearestNode(mouse) {
381 var proximity = prox(30),
382 nearest = null,
383 minDist,
384 regionNodes = t2rs.regionNodes();
385
386 if (regionNodes.length) {
387 minDist = proximity * 2;
388
389 regionNodes.forEach(function (d) {
390 var dist;
391
392 if (!api.showHosts() && d.class === 'host') {
393 return; // skip hidden hosts
394 }
395
396 dist = mdist({x: d.x, y: d.y}, mouse);
397 if (dist < minDist && dist < proximity) {
398 minDist = dist;
399 nearest = d;
400 }
401 });
402 }
403 return nearest;
404 }
405
406
407 function computeNearestLink(mouse) {
408 var proximity = prox(30),
409 nearest = null,
410 minDist,
411 regionLinks = t2rs.regionLinks();
412
413 function pdrop(line, mouse) {
414
415 var x1 = line.x1,
416 y1 = line.y1,
417 x2 = line.x2,
418 y2 = line.y2,
419 x3 = mouse.x,
420 y3 = mouse.y,
421 k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
422 (sq(y2-y1) + sq(x2-x1)),
423 x4 = x3 - k * (y2-y1),
424 y4 = y3 + k * (x2-x1);
425 return {x:x4, y:y4};
426 }
427
428 function lineHit(line, p, m) {
429 if (p.x < line.x1 && p.x < line.x2) return false;
430 if (p.x > line.x1 && p.x > line.x2) return false;
431 if (p.y < line.y1 && p.y < line.y2) return false;
432 if (p.y > line.y1 && p.y > line.y2) return false;
433 // line intersects, but are we close enough?
434 return mdist(p, m) <= proximity;
435 }
436
437 if (regionLinks.length) {
438 minDist = proximity * 2;
439
440 regionLinks.forEach(function (d) {
441 // if (!api.showHosts() && d.type() === 'hostLink') {
442 // return; // skip hidden host links
443 // }
444
445 var line = d.get('position'),
446 point = pdrop(line, mouse),
447 hit = lineHit(line, point, mouse),
448 dist;
449
450 if (hit) {
451 dist = mdist(point, mouse);
452 if (dist < minDist) {
453 minDist = dist;
454 nearest = d;
455 }
456 }
457 });
458 }
459 return nearest;
460 }
461
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100462 angular.module('ovTopo2')
463 .factory('Topo2LayoutService',
464 [
465 '$log', 'SvgUtilService', 'Topo2RegionService',
Steven Burrows9edc7e02016-08-29 11:52:07 +0100466 'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService',
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100467
Steven Burrows9edc7e02016-08-29 11:52:07 +0100468 function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100469
470 $log = _$log_;
471 t2rs = _t2rs_;
472 t2d3 = _t2d3_;
473 t2vs = _t2vs_;
Steven Burrows9edc7e02016-08-29 11:52:07 +0100474 t2ss = _t2ss_;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100475 sus = _sus_;
476
477 return {
478 init: init,
479 update: update,
480 start: start,
481
482 setDimensions: setDimensions
483 }
484 }
485 ]
486 );
487})();