blob: 98493938714fc8a1c5bd74b911c3bdcd212d4029 [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 Burrowsaf96a212016-12-28 12:57:02 +000025 var instance;
Steven Burrowsec1f45c2016-08-08 16:14:41 +010026
Steven Burrowsec1f45c2016-08-08 16:14:41 +010027 // default settings for force layout
28 var defaultSettings = {
29 gravity: 0.4,
30 friction: 0.7,
31 charge: {
32 // note: key is node.class
33 device: -8000,
Steven Burrows583f4be2016-11-04 14:06:50 +010034 host: -20000,
35 region: -5000,
Steven Burrowsec1f45c2016-08-08 16:14:41 +010036 _def_: -12000
37 },
38 linkDistance: {
39 // note: key is link.type
40 direct: 100,
41 optical: 120,
Steven Burrows583f4be2016-11-04 14:06:50 +010042 UiEdgeLink: 30,
Steven Burrowsec1f45c2016-08-08 16:14:41 +010043 _def_: 50
44 },
45 linkStrength: {
46 // note: key is link.type
47 // range: {0.0 ... 1.0}
Steven Burrowsdfa52b02016-09-02 13:50:43 +010048 direct: 1.0,
49 optical: 1.0,
Steven Burrows583f4be2016-11-04 14:06:50 +010050 UiEdgeLink: 15.0,
Steven Burrowsec1f45c2016-08-08 16:14:41 +010051 _def_: 1.0
52 }
53 };
54
55 // configuration
56 var linkConfig = {
57 light: {
58 baseColor: '#939598',
59 inColor: '#66f',
60 outColor: '#f00'
61 },
62 dark: {
63 // TODO : theme
64 baseColor: '#939598',
65 inColor: '#66f',
66 outColor: '#f00'
67 },
68 inWidth: 12,
69 outWidth: 10
70 };
71
72 // internal state
Steven Burrowsaf96a212016-12-28 12:57:02 +000073 var nodeLock = false; // whether nodes can be dragged or not (locked)
Steven Burrows9edc7e02016-08-29 11:52:07 +010074
Steven Burrowsa3fca812016-10-14 15:11:04 -050075 // predicate that indicates when clicking is active
76 function clickEnabled() {
77 return true;
78 }
79
Steven Burrowsaf96a212016-12-28 12:57:02 +000080 function getDefaultPosition(link) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +010081 return {
82 x1: link.get('source').x,
83 y1: link.get('source').y,
84 x2: link.get('target').x,
85 y2: link.get('target').y
86 };
87 }
88
Steven Burrowsec1f45c2016-08-08 16:14:41 +010089 angular.module('ovTopo2')
90 .factory('Topo2LayoutService',
91 [
Steven Burrowse7cc3082016-09-27 11:24:58 -070092 '$log', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService',
Steven Burrowsaf96a212016-12-28 12:57:02 +000093 'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService', 'Topo2ZoomService',
94 'Topo2ViewController',
95 function ($log, wss, sus, t2rs, t2d3, t2vs, t2ss, t2zs,
96 ViewController) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +010097
Steven Burrowsaf96a212016-12-28 12:57:02 +000098 var Layout = ViewController.extend({
99 initialize: function (svg, forceG, uplink, dim, zoomer, opts) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100100
Steven Burrowsaf96a212016-12-28 12:57:02 +0000101 $log.debug('initialize Layout');
102 instance = this;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100103
Steven Burrowsaf96a212016-12-28 12:57:02 +0000104 this.svg = svg;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100105
Steven Burrowsaf96a212016-12-28 12:57:02 +0000106 // Append all the SVG Group elements to the forceG object
107 this.createForceElements();
108
109 this.uplink = uplink;
110 this.dim = dim;
111 this.zoomer = zoomer;
112
113 this.settings = angular.extend({}, defaultSettings, opts);
114
115 this.link = this.elements.linkG.selectAll('.link');
116 this.elements.linkLabelG.selectAll('.linkLabel');
117 this.node = this.elements.nodeG.selectAll('.node');
118 },
119 createForceElements: function () {
120
121 this.prevForce = this.forceG;
122
123 this.forceG = d3.select('#topo-zoomlayer')
124 .append('g').attr('class', 'topo-force');
125
126 this.elements = {
127 linkG: this.addElement(this.forceG, 'topo-links'),
128 linkLabelG: this.addElement(this.forceG, 'topo-linkLabels'),
129 numLinksLabels: this.addElement(this.forceG, 'topo-numLinkLabels'),
130 nodeG: this.addElement(this.forceG, 'topo-nodes'),
131 portLabels: this.addElement(this.forceG, 'topo-portLabels')
132 };
133 },
134 addElement: function (parent, className) {
135 return parent.append('g').attr('class', className);
136 },
137 settingOrDefault: function (settingName, node) {
138 var nodeType = node.get('nodeType');
139 return this.settings[settingName][nodeType] || this.settings[settingName]._def_;
140 },
141 createForceLayout: function () {
142 var _this = this,
143 regionLinks = t2rs.regionLinks(),
144 regionNodes = t2rs.regionNodes();
145
146 this.force = d3.layout.force()
147 .size(t2vs.getDimensions())
148 .gravity(this.settings.gravity)
149 .friction(this.settings.friction)
150 .charge(this.settingOrDefault.bind(this, 'charge'))
151 .linkDistance(this.settingOrDefault.bind(this, 'linkDistance'))
152 .linkStrength(this.settingOrDefault.bind(this, 'linkStrength'))
153 .nodes(regionNodes)
154 .links(regionLinks)
155 .on("tick", this.tick.bind(this))
156 .on("start", function () {
157
158 // TODO: Find a better way to do this
159 setTimeout(function () {
160 _this.centerLayout();
161 }, 500);
162 })
163 .start();
164
165 this.link = this.elements.linkG.selectAll('.link')
166 .data(regionLinks, function (d) { return d.get('key'); });
167
168 this.node = this.elements.nodeG.selectAll('.node')
169 .data(regionNodes, function (d) { return d.get('id'); });
170
171 this.drag = sus.createDragBehavior(this.force,
172 t2ss.selectObject,
173 this.atDragEnd,
174 this.dragEnabled.bind(this),
175 clickEnabled
176 );
177
178 this.update();
179 },
180 centerLayout: function () {
181 d3.select('#topo-zoomlayer').attr('data-layout', t2rs.model.get('id'));
182
183 var zoomer = d3.select('#topo-zoomlayer').node().getBBox(),
184 layoutBBox = this.forceG.node().getBBox(),
185 scale = (zoomer.height - 150) / layoutBBox.height,
186 x = (zoomer.width / 2) - ((layoutBBox.x + layoutBBox.width / 2) * scale),
187 y = (zoomer.height / 2) - ((layoutBBox.y + layoutBBox.height / 2) * scale);
188
189 t2zs.panAndZoom([x, y], scale, 1000);
190 },
191 tick: function () {
192 this.link
193 .attr("x1", function (d) { return d.source.x; })
194 .attr("y1", function (d) { return d.source.y; })
195 .attr("x2", function (d) { return d.target.x; })
196 .attr("y2", function (d) { return d.target.y; });
197
198 this.node
199 .attr({
200 transform: function (d) {
201 var dx = isNaN(d.x) ? 0 : d.x,
202 dy = isNaN(d.y) ? 0 : d.y;
203 return sus.translate(dx, dy);
204 }
205 });
206 },
207
208 start: function () {
209 this.force.start();
210 },
211 update: function () {
212 this.updateNodes();
213 this.updateLinks();
214 },
215 updateNodes: function () {
216 var regionNodes = t2rs.regionNodes();
217
218 // select all the nodes in the layout:
219 this.node = this.elements.nodeG.selectAll('.node')
220 .data(regionNodes, function (d) { return d.get('id'); });
221
222 var entering = this.node.enter()
223 .append('g')
224 .attr({
225 id: function (d) { return sus.safeId(d.get('id')); },
226 class: function (d) { return d.svgClassName(); },
227 transform: function (d) {
228 // Need to guard against NaN here ??
229 return sus.translate(d.node.x, d.node.y);
230 },
231 opacity: 0
232 })
233 .call(this.drag)
234 .transition()
235 .attr('opacity', 1);
236
237 entering.filter('.device').each(t2d3.nodeEnter);
238 entering.filter('.sub-region').each(t2d3.nodeEnter);
239 entering.filter('.host').each(t2d3.hostEnter);
240
241 // operate on exiting nodes:
242 // Note that the node is removed after 2 seconds.
243 // Sub element animations should be shorter than 2 seconds.
244 // var exiting = this.node.exit()
245 // .transition()
246 // .duration(300)
247 // .style('opacity', 0)
248 // .remove();
249
250 // exiting node specifics:
251 // exiting.filter('.host').each(t2d3.hostExit);
252 // exiting.filter('.device').each(t2d3.nodeExit);
253 },
254 updateLinks: function () {
255
256 var regionLinks = t2rs.regionLinks();
257
258 this.link = this.elements.linkG.selectAll('.link')
259 .data(regionLinks, function (d) { return d.get('key'); });
260
261 // operate on entering links:
262 var entering = this.link.enter()
263 .append('line')
264 .call(this.calcPosition)
265 .attr({
266 x1: function (d) { return d.get('position').x1; },
267 y1: function (d) { return d.get('position').y1; },
268 x2: function (d) { return d.get('position').x2; },
269 y2: function (d) { return d.get('position').y2; },
270 stroke: linkConfig.light.inColor,
271 'stroke-width': linkConfig.inWidth
272 });
273
274 entering.each(t2d3.linkEntering);
275
276 // operate on exiting links:
277 this.link.exit()
278 .style('opacity', 1)
279 .transition()
280 .duration(300)
281 .style('opacity', 0.0)
282 .remove();
283 },
284 calcPosition: function () {
285 var lines = this;
286
287 lines.each(function (d) {
288 if (d.get('type') === 'hostLink') {
289 d.set('position', getDefaultPosition(d));
290 }
291 });
292
293 lines.each(function (d) {
294 d.set('position', getDefaultPosition(d));
295 });
296 },
297 sendUpdateMeta: function (d, clearPos) {
298 var metaUi = {},
299 ll;
300
301 // if we are not clearing the position data (unpinning),
302 // attach the x, y, (and equivalent longitude, latitude)...
303 if (!clearPos) {
304 ll = d.lngLatFromCoord([d.x, d.y]);
305 metaUi = {
306 x: d.x,
307 y: d.y,
308 equivLoc: {
309 lng: ll[0],
310 lat: ll[1]
311 }
312 };
313 }
314 d.metaUi = metaUi;
315 wss.sendEvent('updateMeta2', {
316 id: d.get('id'),
317 class: d.get('class'),
318 memento: metaUi
319 });
320 },
321 setDimensions: function () {
322 if (this.force) {
323 this.force.size(t2vs.getDimensions());
324 }
325 },
326 dragEnabled: function () {
327 var ev = d3.event.sourceEvent;
328 // nodeLock means we aren't allowing nodes to be dragged...
329 return !nodeLock && !this.zoomingOrPanning(ev);
330 },
331 zoomingOrPanning: function (ev) {
332 return ev.metaKey || ev.altKey;
333 },
334 atDragEnd: function (d) {
335 // once we've finished moving, pin the node in position
336 d.fixed = true;
337 d3.select(this).classed('fixed', true);
338 instance.sendUpdateMeta(d);
339 $log.debug(d);
340 t2ss.clickConsumed(true);
341 },
342 transitionDownRegion: function () {
343
344 this.prevForce.transition()
345 .duration(1500)
346 .style('opacity', 0)
347 .remove();
348
349 this.forceG
350 .style('opacity', 0)
351 .transition()
352 .delay(500)
353 .duration(500)
354 .style('opacity', 1);
355 },
356 transitionUpRegion: function () {
357 this.prevForce.transition()
358 .duration(1000)
359 .style('opacity', 0)
360 .remove();
361
362 this.forceG
363 .style('opacity', 0)
364 .transition()
365 .delay(500)
366 .duration(500)
367 .style('opacity', 1);
368 }
369 });
370
371 function getInstance(svg, forceG, uplink, dim, zoomer, opts) {
372 return instance || new Layout(svg, forceG, uplink, dim, zoomer, opts);
373 }
374
375 return getInstance;
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100376 }
377 ]
378 );
379})();