blob: c23bc7edc2d5c39022540156c48a2cd01726cd2b [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 Burrowsdfa52b02016-09-02 13:50:43 +010027 var uplink, linkG, linkLabelG, nodeG;
28 var link, node;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010029
Steven Burrowsdfa52b02016-09-02 13:50:43 +010030 var highlightedLink;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010031
32 // default settings for force layout
33 var defaultSettings = {
34 gravity: 0.4,
35 friction: 0.7,
36 charge: {
37 // note: key is node.class
38 device: -8000,
39 host: -5000,
40 _def_: -12000
41 },
42 linkDistance: {
43 // note: key is link.type
44 direct: 100,
45 optical: 120,
46 hostLink: 3,
47 _def_: 50
48 },
49 linkStrength: {
50 // note: key is link.type
51 // range: {0.0 ... 1.0}
Steven Burrowsdfa52b02016-09-02 13:50:43 +010052 direct: 1.0,
53 optical: 1.0,
54 hostLink: 1.0,
Steven Burrowsec1f45c2016-08-08 16:14:41 +010055 _def_: 1.0
56 }
57 };
58
59 // configuration
60 var linkConfig = {
61 light: {
62 baseColor: '#939598',
63 inColor: '#66f',
64 outColor: '#f00'
65 },
66 dark: {
67 // TODO : theme
68 baseColor: '#939598',
69 inColor: '#66f',
70 outColor: '#f00'
71 },
72 inWidth: 12,
73 outWidth: 10
74 };
75
76 // internal state
77 var settings, // merged default settings and options
78 force, // force layout object
79 drag, // drag behavior handler
Steven Burrowsdfa52b02016-09-02 13:50:43 +010080 nodeLock = false; // whether nodes can be dragged or not (locked)
Steven Burrowsec1f45c2016-08-08 16:14:41 +010081
82 var tickStuff = {
83 nodeAttr: {
84 transform: function (d) {
85 var dx = isNaN(d.x) ? 0 : d.x,
86 dy = isNaN(d.y) ? 0 : d.y;
87 return sus.translate(dx, dy);
88 }
89 },
90 linkAttr: {
91 x1: function (d) { return d.get('position').x1; },
92 y1: function (d) { return d.get('position').y1; },
93 x2: function (d) { return d.get('position').x2; },
94 y2: function (d) { return d.get('position').y2; }
Steven Burrowsec1f45c2016-08-08 16:14:41 +010095 }
96 };
97
98 function init(_svg_, forceG, _uplink_, _dim_, opts) {
99
100 $log.debug("Initialising Topology Layout");
Steven Burrows9edc7e02016-08-29 11:52:07 +0100101 uplink = _uplink_;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100102 settings = angular.extend({}, defaultSettings, opts);
103
104 linkG = forceG.append('g').attr('id', 'topo-links');
105 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100106 forceG.append('g').attr('id', 'topo-numLinkLabels');
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100107 nodeG = forceG.append('g').attr('id', 'topo-nodes');
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100108 forceG.append('g').attr('id', 'topo-portLabels');
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100109
110 link = linkG.selectAll('.link');
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100111 linkLabelG.selectAll('.linkLabel');
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100112 node = nodeG.selectAll('.node');
113
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100114 _svg_.on('mousemove', mouseMoveHandler);
115 }
116
117 function createForceLayout() {
118
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100119 force = d3.layout.force()
120 .size(t2vs.getDimensions())
121 .nodes(t2rs.regionNodes())
122 .links(t2rs.regionLinks())
123 .gravity(settings.gravity)
124 .friction(settings.friction)
125 .charge(settings.charge._def_)
126 .linkDistance(settings.linkDistance._def_)
127 .linkStrength(settings.linkStrength._def_)
128 .on('tick', tick);
Steven Burrows9edc7e02016-08-29 11:52:07 +0100129
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100130 drag = sus.createDragBehavior(force,
131 t2ss.selectObject, atDragEnd, dragEnabled, clickEnabled);
Steven Burrows9edc7e02016-08-29 11:52:07 +0100132
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100133 start();
134 update();
Steven Burrows9edc7e02016-08-29 11:52:07 +0100135 }
136
137 function zoomingOrPanning(ev) {
138 return ev.metaKey || ev.altKey;
139 }
140
141 function atDragEnd(d) {
142 // once we've finished moving, pin the node in position
143 d.fixed = true;
144 d3.select(this).classed('fixed', true);
145 // TODO: sendUpdateMeta(d);
146 t2ss.clickConsumed(true);
147 }
148
149 // predicate that indicates when dragging is active
150 function dragEnabled() {
151 var ev = d3.event.sourceEvent;
152 // nodeLock means we aren't allowing nodes to be dragged...
153 return !nodeLock && !zoomingOrPanning(ev);
154 }
155
156 // predicate that indicates when clicking is active
157 function clickEnabled() {
158 return true;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100159 }
160
161 function tick() {
162 // guard against null (which can happen when our view pages out)...
163 if (node && node.size()) {
164 node.attr(tickStuff.nodeAttr);
165 }
166 if (link && link.size()) {
167 link.call(calcPosition)
168 .attr(tickStuff.linkAttr);
169 // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
170 }
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100171 }
172
173 function update() {
174 _updateNodes();
175 _updateLinks();
176 }
177
178 function _updateNodes() {
179
180 var regionNodes = t2rs.regionNodes();
181
182 // select all the nodes in the layout:
183 node = nodeG.selectAll('.node')
184 .data(regionNodes, function (d) { return d.get('id'); });
185
186 var entering = node.enter()
187 .append('g')
188 .attr({
189 id: function (d) { return sus.safeId(d.get('id')); },
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100190 class: function (d) { return d.svgClassName(); },
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100191 transform: function (d) {
192 // Need to guard against NaN here ??
193 return sus.translate(d.node.x, d.node.y);
194 },
195 opacity: 0
196 })
Steven Burrows9edc7e02016-08-29 11:52:07 +0100197 .call(drag)
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100198 // .on('mouseover', tss.nodeMouseOver)
199 // .on('mouseout', tss.nodeMouseOut)
200 .transition()
201 .attr('opacity', 1);
202
Steven Burrows6deb4ce2016-08-26 16:06:23 +0100203 entering.filter('.device').each(t2d3.nodeEnter);
204 entering.filter('.sub-region').each(t2d3.nodeEnter);
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100205 entering.filter('.host').each(t2d3.hostEnter);
206
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100207 // operate on exiting nodes:
208 // Note that the node is removed after 2 seconds.
209 // Sub element animations should be shorter than 2 seconds.
210 var exiting = node.exit()
211 .transition()
212 .duration(2000)
213 .style('opacity', 0)
214 .remove();
215
216 // exiting node specifics:
217 // exiting.filter('.host').each(t2d3.hostExit);
218 exiting.filter('.device').each(t2d3.nodeExit);
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100219 }
220
221 function _updateLinks() {
222
223 // var th = ts.theme();
224 var regionLinks = t2rs.regionLinks();
225
226 link = linkG.selectAll('.link')
227 .data(regionLinks, function (d) { return d.get('key'); });
228
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100229 // operate on entering links:
230 var entering = link.enter()
231 .append('line')
232 .call(calcPosition)
233 .attr({
234 x1: function (d) { return d.get('position').x1; },
235 y1: function (d) { return d.get('position').y1; },
236 x2: function (d) { return d.get('position').x2; },
237 y2: function (d) { return d.get('position').y2; },
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100238 stroke: linkConfig.light.inColor,
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100239 'stroke-width': linkConfig.inWidth
240 });
241
242 entering.each(t2d3.linkEntering);
243
244 // operate on both existing and new links:
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100245 // link.each(...)
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100246
247 // add labels for how many links are in a thick line
248 // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
249
250 // apply or remove labels
251 // t2d3.applyLinkLabels();
252
253 // operate on exiting links:
254 link.exit()
255 .attr('stroke-dasharray', '3 3')
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100256 .attr('stroke', linkConfig.light.outColor)
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100257 .style('opacity', 0.5)
258 .transition()
259 .duration(1500)
260 .attr({
261 'stroke-dasharray': '3 12',
262 'stroke-width': linkConfig.outWidth
263 })
264 .style('opacity', 0.0)
265 .remove();
266 }
267
268 function calcPosition() {
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100269 var lines = this;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100270
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100271 lines.each(function (d) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100272 if (d.get('type') === 'hostLink') {
273 d.set('position', getDefaultPos(d));
274 }
275 });
276
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100277 lines.each(function (d) {
278 d.set('position', getDefaultPos(d));
279 });
280 }
281
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100282 function getDefaultPos(link) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100283
284 return {
285 x1: link.get('source').x,
286 y1: link.get('source').y,
287 x2: link.get('target').x,
288 y2: link.get('target').y
289 };
290 }
291
292 function setDimensions() {
293 if (force) {
294 force.size(t2vs.getDimensions());
295 }
296 }
297
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100298 function start() {
299 force.start();
300 }
301
Steven Burrows9edc7e02016-08-29 11:52:07 +0100302 // Mouse Events
303 function mouseMoveHandler() {
304 var mp = getLogicalMousePosition(this),
305 link = computeNearestLink(mp);
306
Steven Burrows9edc7e02016-08-29 11:52:07 +0100307 if (highlightedLink) {
308 highlightedLink.unenhance();
309 highlightedLink = null;
310 }
311
312 if (link) {
313 link.enhance();
314 highlightedLink = link;
315 }
316 }
317
318 // ======== ALGORITHM TO FIND LINK CLOSEST TO MOUSE ========
319
320 function getLogicalMousePosition(container) {
321 var m = d3.mouse(container),
322 sc = uplink.zoomer().scale(),
323 tr = uplink.zoomer().translate(),
324 mx = (m[0] - tr[0]) / sc,
325 my = (m[1] - tr[1]) / sc;
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100326 return { x: mx, y: my };
Steven Burrows9edc7e02016-08-29 11:52:07 +0100327 }
328
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100329 function sq(x) {
330 return x * x;
331 }
Steven Burrows9edc7e02016-08-29 11:52:07 +0100332
333 function mdist(p, m) {
334 return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
335 }
336
337 function prox(dist) {
338 return dist / uplink.zoomer().scale();
339 }
340
Steven Burrows9edc7e02016-08-29 11:52:07 +0100341 function computeNearestLink(mouse) {
342 var proximity = prox(30),
343 nearest = null,
344 minDist,
345 regionLinks = t2rs.regionLinks();
346
347 function pdrop(line, mouse) {
348
349 var x1 = line.x1,
350 y1 = line.y1,
351 x2 = line.x2,
352 y2 = line.y2,
353 x3 = mouse.x,
354 y3 = mouse.y,
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100355 k = ((y2 - y1) * (x3 - x1) - (x2 - x1) * (y3 - y1)) /
356 (sq(y2 - y1) + sq(x2 - x1)),
357 x4 = x3 - k * (y2 - y1),
358 y4 = y3 + k * (x2 - x1);
359 return { x: x4, y: y4 };
Steven Burrows9edc7e02016-08-29 11:52:07 +0100360 }
361
362 function lineHit(line, p, m) {
363 if (p.x < line.x1 && p.x < line.x2) return false;
364 if (p.x > line.x1 && p.x > line.x2) return false;
365 if (p.y < line.y1 && p.y < line.y2) return false;
366 if (p.y > line.y1 && p.y > line.y2) return false;
367 // line intersects, but are we close enough?
368 return mdist(p, m) <= proximity;
369 }
370
371 if (regionLinks.length) {
372 minDist = proximity * 2;
373
374 regionLinks.forEach(function (d) {
375 // if (!api.showHosts() && d.type() === 'hostLink') {
376 // return; // skip hidden host links
377 // }
378
379 var line = d.get('position'),
380 point = pdrop(line, mouse),
381 hit = lineHit(line, point, mouse),
382 dist;
383
384 if (hit) {
385 dist = mdist(point, mouse);
386 if (dist < minDist) {
387 minDist = dist;
388 nearest = d;
389 }
390 }
391 });
392 }
393 return nearest;
394 }
395
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100396 angular.module('ovTopo2')
397 .factory('Topo2LayoutService',
398 [
399 '$log', 'SvgUtilService', 'Topo2RegionService',
Steven Burrows9edc7e02016-08-29 11:52:07 +0100400 'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService',
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100401
Steven Burrows9edc7e02016-08-29 11:52:07 +0100402 function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100403
404 $log = _$log_;
405 t2rs = _t2rs_;
406 t2d3 = _t2d3_;
407 t2vs = _t2vs_;
Steven Burrows9edc7e02016-08-29 11:52:07 +0100408 t2ss = _t2ss_;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100409 sus = _sus_;
410
411 return {
412 init: init,
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100413 createForceLayout: createForceLayout,
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100414 update: update,
415 start: start,
416
417 setDimensions: setDimensions
Steven Burrowsdfa52b02016-09-02 13:50:43 +0100418 };
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100419 }
420 ]
421 );
422})();