blob: 32190125a89cf5667f6751cdf13e73481685dea3 [file] [log] [blame]
Simon Hunta4242de2015-02-24 17:11:55 -08001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Simon Hunta4242de2015-02-24 17:11:55 -08003 *
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 D3 Module.
19 Functions for manipulating the D3 visualizations of the Topology
20 */
21
22(function () {
23 'use strict';
24
25 // injected refs
Thomas Vachuska0af26912016-03-21 21:37:30 -070026 var $log, fs, sus, is, ts, ps, ttbs;
Simon Hunta4242de2015-02-24 17:11:55 -080027
28 // api to topoForce
29 var api;
30 /*
31 node() // get ref to D3 selection of nodes
32 link() // get ref to D3 selection of links
33 linkLabel() // get ref to D3 selection of link labels
34 instVisible() // true if instances panel is visible
35 posNode() // position node
36 showHosts() // true if hosts are to be shown
37 restyleLinkElement() // update link styles based on backing data
38 updateLinkLabelModel() // update backing data for link labels
39 */
40
41 // configuration
Simon Hunta5487ad2016-06-16 13:10:41 -070042 var devIconDim = 36,
43 labelPad = 4,
44 hostRadius = 14,
45 badgeConfig = {
Simon Hunt5674db92015-10-22 16:12:48 -070046 radius: 12,
Simon Hunt004fc2c2015-10-23 11:55:58 -070047 yoff: 5,
48 gdelta: 10
Simon Hunta5487ad2016-06-16 13:10:41 -070049 },
50 halfDevIcon = devIconDim / 2,
51 devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
52 hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
53 status = {
54 i: 'badgeInfo',
55 w: 'badgeWarn',
56 e: 'badgeError'
Simon Huntf44d7262016-06-14 14:46:56 -070057 };
58
Simon Hunt1eee51d2016-02-26 19:12:13 -080059 // NOTE: this type of hack should go away once we have implemented
60 // the server-side UiModel code.
61 // {virtual -> cord} is for the E-CORD demo at ONS 2016
62 var remappedDeviceTypes = {
63 virtual: 'cord'
64 };
65
66 function mapDeviceTypeToGlyph(type) {
67 return remappedDeviceTypes[type] || type || 'unknown';
68 }
69
Simon Hunt5674db92015-10-22 16:12:48 -070070 function badgeStatus(badge) {
71 return status[badge.status] || status.i;
72 }
73
Simon Hunta4242de2015-02-24 17:11:55 -080074 // internal state
75 var deviceLabelIndex = 0,
76 hostLabelIndex = 0;
77
Simon Hunta5487ad2016-06-16 13:10:41 -070078 // note: these are the device icon colors without affinity (no master)
Simon Hunta4242de2015-02-24 17:11:55 -080079 var dColTheme = {
80 light: {
Simon Huntf44d7262016-06-14 14:46:56 -070081 online: '#444444',
82 offline: '#cccccc'
Simon Hunta4242de2015-02-24 17:11:55 -080083 },
84 dark: {
Simon Huntf44d7262016-06-14 14:46:56 -070085 // TODO: theme
86 online: '#444444',
87 offline: '#cccccc'
Simon Hunta4242de2015-02-24 17:11:55 -080088 }
89 };
90
Simon Huntf44d7262016-06-14 14:46:56 -070091 function devGlyphColor(d) {
92 var o = d.online,
93 id = d.master,
94 otag = o ? 'online' : 'offline';
95 return o ? sus.cat7().getColor(id, 0, ts.theme())
96 : dColTheme[ts.theme()][otag];
Simon Hunta4242de2015-02-24 17:11:55 -080097 }
98
99 function setDeviceColor(d) {
Simon Huntf44d7262016-06-14 14:46:56 -0700100 d.el.select('use')
101 .style('fill', devGlyphColor(d));
Simon Hunta4242de2015-02-24 17:11:55 -0800102 }
103
Simon Hunta4242de2015-02-24 17:11:55 -0800104 function incDevLabIndex() {
Steven Burrows37549ee2016-09-21 14:41:39 +0100105 setDevLabIndex(device3ndex+1);
Bri Prebilic Cole9cf1a8d2015-04-21 13:15:29 -0700106 switch(deviceLabelIndex) {
107 case 0: return 'Hide device labels';
108 case 1: return 'Show friendly device labels';
109 case 2: return 'Show device ID labels';
110 }
Simon Hunta4242de2015-02-24 17:11:55 -0800111 }
112
Thomas Vachuska0af26912016-03-21 21:37:30 -0700113 function setDevLabIndex(mode) {
114 deviceLabelIndex = mode % 3;
115 var p = ps.getPrefs('topo_prefs', ttbs.defaultPrefs);
116 p.dlbls = deviceLabelIndex;
117 ps.setPrefs('topo_prefs', p);
118 }
119
Simon Hunta4242de2015-02-24 17:11:55 -0800120 function hostLabel(d) {
121 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
122 return d.labels[idx];
123 }
Simon Huntf44d7262016-06-14 14:46:56 -0700124
Simon Hunta4242de2015-02-24 17:11:55 -0800125 function deviceLabel(d) {
126 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
127 return d.labels[idx];
128 }
Simon Huntf44d7262016-06-14 14:46:56 -0700129
Simon Hunta4242de2015-02-24 17:11:55 -0800130 function trimLabel(label) {
131 return (label && label.trim()) || '';
132 }
133
Simon Huntf44d7262016-06-14 14:46:56 -0700134 function computeLabelWidth(n) {
135 var text = n.select('text'),
136 box = text.node().getBBox();
137 return box.width + labelPad * 2;
138 }
139
140 function iconBox(dim, labelWidth) {
Simon Hunta4242de2015-02-24 17:11:55 -0800141 return {
Simon Huntf44d7262016-06-14 14:46:56 -0700142 x: -dim/2,
143 y: -dim/2,
144 width: dim + labelWidth,
145 height: dim
146 }
Simon Hunta4242de2015-02-24 17:11:55 -0800147 }
148
Simon Hunt5674db92015-10-22 16:12:48 -0700149 function updateDeviceRendering(d) {
Simon Huntf44d7262016-06-14 14:46:56 -0700150 var node = d.el,
151 bdg = d.badge,
152 label = trimLabel(deviceLabel(d)),
153 labelWidth;
Simon Hunta4242de2015-02-24 17:11:55 -0800154
Simon Huntf44d7262016-06-14 14:46:56 -0700155 node.select('text').text(label);
156 labelWidth = label ? computeLabelWidth(node) : 0;
Simon Hunta4242de2015-02-24 17:11:55 -0800157
158 node.select('rect')
159 .transition()
Simon Huntf44d7262016-06-14 14:46:56 -0700160 .attr(iconBox(devIconDim, labelWidth));
Simon Hunta4242de2015-02-24 17:11:55 -0800161
Simon Hunt5674db92015-10-22 16:12:48 -0700162 if (bdg) {
Simon Hunta5487ad2016-06-16 13:10:41 -0700163 renderBadge(node, bdg, devBadgeOff);
Simon Hunte9343f32015-10-21 18:07:46 -0700164 }
165 }
166
Andrea Campanella52125412015-12-03 14:50:40 -0800167 function updateHostRendering(d) {
168 var node = d.el,
Simon Huntc2bfe332015-12-04 11:01:24 -0800169 bdg = d.badge;
Andrea Campanella52125412015-12-03 14:50:40 -0800170
171 updateHostLabel(d);
Andrea Campanella52125412015-12-03 14:50:40 -0800172
Andrea Campanella52125412015-12-03 14:50:40 -0800173 if (bdg) {
Simon Hunta5487ad2016-06-16 13:10:41 -0700174 renderBadge(node, bdg, hostBadgeOff);
Simon Huntc2bfe332015-12-04 11:01:24 -0800175 }
176 }
Andrea Campanella52125412015-12-03 14:50:40 -0800177
Simon Huntc2bfe332015-12-04 11:01:24 -0800178 function renderBadge(node, bdg, boff) {
179 var bsel,
180 bcr = badgeConfig.radius,
181 bcgd = badgeConfig.gdelta;
Andrea Campanella52125412015-12-03 14:50:40 -0800182
Simon Huntc2bfe332015-12-04 11:01:24 -0800183 node.select('g.badge').remove();
Andrea Campanella52125412015-12-03 14:50:40 -0800184
Simon Huntc2bfe332015-12-04 11:01:24 -0800185 bsel = node.append('g')
186 .classed('badge', true)
187 .classed(badgeStatus(bdg), true)
188 .attr('transform', sus.translate(boff.dx, boff.dy));
Andrea Campanella52125412015-12-03 14:50:40 -0800189
Simon Huntc2bfe332015-12-04 11:01:24 -0800190 bsel.append('circle')
191 .attr('r', bcr);
192
193 if (bdg.txt) {
194 bsel.append('text')
195 .attr('dy', badgeConfig.yoff)
196 .attr('text-anchor', 'middle')
197 .text(bdg.txt);
198 } else if (bdg.gid) {
199 bsel.append('use')
200 .attr({
201 width: bcgd * 2,
202 height: bcgd * 2,
203 transform: sus.translate(-bcgd, -bcgd),
204 'xlink:href': '#' + bdg.gid
205 });
Andrea Campanella52125412015-12-03 14:50:40 -0800206 }
207 }
208
Simon Hunta4242de2015-02-24 17:11:55 -0800209 function updateHostLabel(d) {
210 var label = trimLabel(hostLabel(d));
211 d.el.select('text').text(label);
212 }
213
214 function updateDeviceColors(d) {
215 if (d) {
216 setDeviceColor(d);
217 } else {
218 api.node().filter('.device').each(function (d) {
219 setDeviceColor(d);
220 });
221 }
222 }
223
224
225 // ==========================
226 // updateNodes - subfunctions
227
228 function deviceExisting(d) {
229 var node = d.el;
230 node.classed('online', d.online);
Simon Hunt5674db92015-10-22 16:12:48 -0700231 updateDeviceRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800232 api.posNode(d, true);
233 }
234
235 function hostExisting(d) {
Andrea Campanella52125412015-12-03 14:50:40 -0800236 updateHostRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800237 api.posNode(d, true);
238 }
239
240 function deviceEnter(d) {
241 var node = d3.select(this),
Simon Hunt1eee51d2016-02-26 19:12:13 -0800242 glyphId = mapDeviceTypeToGlyph(d.type),
Simon Hunta4242de2015-02-24 17:11:55 -0800243 label = trimLabel(deviceLabel(d)),
Simon Huntf44d7262016-06-14 14:46:56 -0700244 rect, text, glyph, labelWidth;
Simon Hunta4242de2015-02-24 17:11:55 -0800245
246 d.el = node;
247
Simon Huntf44d7262016-06-14 14:46:56 -0700248 rect = node.append('rect');
Simon Hunta4242de2015-02-24 17:11:55 -0800249
Simon Huntf44d7262016-06-14 14:46:56 -0700250 text = node.append('text').text(label)
251 .attr('text-anchor', 'left')
252 .attr('y', '0.3em')
Simon Hunta5487ad2016-06-16 13:10:41 -0700253 .attr('x', halfDevIcon + labelPad);
Simon Hunta4242de2015-02-24 17:11:55 -0800254
Simon Huntf44d7262016-06-14 14:46:56 -0700255 glyph = is.addDeviceIcon(node, glyphId, devIconDim);
Simon Hunta4242de2015-02-24 17:11:55 -0800256
Simon Huntf44d7262016-06-14 14:46:56 -0700257 labelWidth = label ? computeLabelWidth(node) : 0;
258
259 rect.attr(iconBox(devIconDim, labelWidth));
260 glyph.attr(iconBox(devIconDim, 0));
261
Simon Hunta5487ad2016-06-16 13:10:41 -0700262 node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
Simon Hunta4242de2015-02-24 17:11:55 -0800263 }
264
265 function hostEnter(d) {
266 var node = d3.select(this),
267 gid = d.type || 'unknown',
Simon Hunta5487ad2016-06-16 13:10:41 -0700268 textDy = hostRadius + 10;
Simon Hunta4242de2015-02-24 17:11:55 -0800269
270 d.el = node;
271 sus.visible(node, api.showHosts());
272
Simon Hunta5487ad2016-06-16 13:10:41 -0700273 is.addHostIcon(node, hostRadius, gid);
Simon Hunta4242de2015-02-24 17:11:55 -0800274
275 node.append('text')
276 .text(hostLabel)
277 .attr('dy', textDy)
278 .attr('text-anchor', 'middle');
279 }
280
281 function hostExit(d) {
282 var node = d.el;
283 node.select('use')
284 .style('opacity', 0.5)
285 .transition()
286 .duration(800)
287 .style('opacity', 0);
288
289 node.select('text')
290 .style('opacity', 0.5)
291 .transition()
292 .duration(800)
293 .style('opacity', 0);
294
295 node.select('circle')
296 .style('stroke-fill', '#555')
297 .style('fill', '#888')
298 .style('opacity', 0.5)
299 .transition()
300 .duration(1500)
301 .attr('r', 0);
302 }
303
304 function deviceExit(d) {
305 var node = d.el;
306 node.select('use')
307 .style('opacity', 0.5)
308 .transition()
309 .duration(800)
310 .style('opacity', 0);
311
312 node.selectAll('rect')
313 .style('stroke-fill', '#555')
314 .style('fill', '#888')
315 .style('opacity', 0.5);
316 }
317
318
319 // ==========================
320 // updateLinks - subfunctions
321
Simon Hunta4242de2015-02-24 17:11:55 -0800322 function linkEntering(d) {
Steven Burrowsec1f45c2016-08-08 16:14:41 +0100323
Simon Hunta4242de2015-02-24 17:11:55 -0800324 var link = d3.select(this);
325 d.el = link;
326 api.restyleLinkElement(d);
327 if (d.type() === 'hostLink') {
328 sus.visible(link, api.showHosts());
329 }
330 }
331
332 var linkLabelOffset = '0.3em';
333
334 function applyLinkLabels() {
335 var entering;
336
337 api.updateLinkLabelModel();
338
339 // for elements already existing, we need to update the text
340 // and adjust the rectangle size to fit
341 api.linkLabel().each(function (d) {
342 var el = d3.select(this),
343 rect = el.select('rect'),
344 text = el.select('text');
345 text.text(d.label);
346 rect.attr(rectAroundText(el));
347 });
348
349 entering = api.linkLabel().enter().append('g')
350 .classed('linkLabel', true)
351 .attr('id', function (d) { return d.id; });
352
353 entering.each(function (d) {
354 var el = d3.select(this),
355 rect,
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700356 text;
Simon Hunta4242de2015-02-24 17:11:55 -0800357
358 if (d.ldata.type() === 'hostLink') {
359 el.classed('hostLinkLabel', true);
360 sus.visible(el, api.showHosts());
361 }
362
363 d.el = el;
364 rect = el.append('rect');
365 text = el.append('text').text(d.label);
366 rect.attr(rectAroundText(el));
367 text.attr('dy', linkLabelOffset);
368
Carmelo Casconed01eda62016-08-02 10:19:15 -0700369 el.attr('transform', transformLabel(d.ldata.position, d.key));
Simon Hunta4242de2015-02-24 17:11:55 -0800370 });
371
372 // Remove any labels that are no longer required.
373 api.linkLabel().exit().remove();
374 }
375
376 function rectAroundText(el) {
377 var text = el.select('text'),
378 box = text.node().getBBox();
379
380 // translate the bbox so that it is centered on [x,y]
381 box.x = -box.width / 2;
382 box.y = -box.height / 2;
383
384 // add padding
385 box.x -= 1;
386 box.width += 2;
387 return box;
388 }
389
Carmelo Casconed01eda62016-08-02 10:19:15 -0700390 function generateLabelFunction() {
391 var labels = [];
392 var xGap = 15;
393 var yGap = 17;
394
395 return function(newId, newX, newY) {
396
397 var idx = -1;
398
399 labels.forEach(function(l, i) {
400 if (l.id === newId) {
401 idx = i;
402 return;
403 }
404 var minX = l.x - xGap;
405 var maxX = l.x + xGap;
406 var minY = l.y - yGap;
407 var maxY = l.y + yGap;
408
409 if (newX > minX && newX < maxX && newY > minY && newY < maxY) {
410 // labels are overlapped
411 newX = newX - xGap;
412 newY = newY - yGap;
413 }
414 });
415
416 if (idx === -1) {
417 labels.push({id: newId, x: newX, y: newY});
418 }
419 else {
420 labels[idx] = {id: newId, x: newX, y: newY};
421 }
422
423 return {x: newX, y: newY};
424 }
425 }
426
427 var getLabelPosNoOverlap = generateLabelFunction();
428
429 function transformLabel(p, id) {
Simon Hunta4242de2015-02-24 17:11:55 -0800430 var dx = p.x2 - p.x1,
431 dy = p.y2 - p.y1,
432 xMid = dx/2 + p.x1,
433 yMid = dy/2 + p.y1;
Carmelo Casconed01eda62016-08-02 10:19:15 -0700434
435 if (id) {
436 var pos = getLabelPosNoOverlap(id, xMid, yMid);
437 return sus.translate(pos.x, pos.y);
438 }
439
Simon Hunta4242de2015-02-24 17:11:55 -0800440 return sus.translate(xMid, yMid);
441 }
442
Simon Hunt1a5301e2015-02-25 15:31:25 -0800443 function applyPortLabels(data, portLabelG) {
444 var entering = portLabelG.selectAll('.portLabel')
445 .data(data).enter().append('g')
446 .classed('portLabel', true)
447 .attr('id', function (d) { return d.id; });
448
449 entering.each(function (d) {
450 var el = d3.select(this),
451 rect = el.append('rect'),
452 text = el.append('text').text(d.num);
453
454 rect.attr(rectAroundText(el));
455 text.attr('dy', linkLabelOffset);
Simon Hunt969b3c92015-02-25 18:11:31 -0800456 el.attr('transform', sus.translate(d.x, d.y));
Simon Hunt1a5301e2015-02-25 15:31:25 -0800457 });
458 }
459
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700460 function labelPoint(linkPos) {
461 var lengthUpLine = 1 / 3,
462 dx = linkPos.x2 - linkPos.x1,
463 dy = linkPos.y2 - linkPos.y1,
464 movedX = dx * lengthUpLine,
465 movedY = dy * lengthUpLine;
466
467 return {
468 x: movedX,
469 y: movedY
470 };
471 }
472
473 function calcGroupPos(linkPos) {
474 var moved = labelPoint(linkPos);
475 return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
476 }
477
478 // calculates where on the link that the hash line for 5+ label appears
479 function hashAttrs(linkPos) {
480 var hashLength = 25,
481 halfLength = hashLength / 2,
482 dx = linkPos.x2 - linkPos.x1,
483 dy = linkPos.y2 - linkPos.y1,
484 length = Math.sqrt((dx * dx) + (dy * dy)),
485 moveAmtX = (dx / length) * halfLength,
486 moveAmtY = (dy / length) * halfLength,
487 mid = labelPoint(linkPos),
488 angle = Math.atan(dy / dx) + 45;
489
490 return {
491 x1: mid.x - moveAmtX,
492 y1: mid.y - moveAmtY,
493 x2: mid.x + moveAmtX,
494 y2: mid.y + moveAmtY,
495 stroke: api.linkConfig()[ts.theme()].baseColor,
496 transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
497 };
498 }
499
500 function textLabelPos(linkPos) {
501 var point = labelPoint(linkPos),
502 dist = 20;
503 return {
504 x: point.x + dist,
505 y: point.y + dist
506 };
507 }
508
509 function applyNumLinkLabels(data, lblsG) {
510 var labels = lblsG.selectAll('g.numLinkLabel')
511 .data(data, function (d) { return 'pair-' + d.id; }),
512 entering;
513
514 // update existing labels
515 labels.each(function (d) {
516 var el = d3.select(this);
517
518 el.attr({
519 transform: function (d) { return calcGroupPos(d.linkCoords); }
520 });
521 el.select('line')
522 .attr(hashAttrs(d.linkCoords));
523 el.select('text')
524 .attr(textLabelPos(d.linkCoords))
525 .text(d.num);
526 });
527
528 // add new labels
529 entering = labels
530 .enter()
531 .append('g')
532 .attr({
533 transform: function (d) { return calcGroupPos(d.linkCoords); },
534 id: function (d) { return 'pair-' + d.id; }
535 })
536 .classed('numLinkLabel', true);
537
538 entering.each(function (d) {
539 var el = d3.select(this);
540
541 el.append('line')
542 .classed('numLinkHash', true)
543 .attr(hashAttrs(d.linkCoords));
544 el.append('text')
545 .classed('numLinkText', true)
546 .attr(textLabelPos(d.linkCoords))
547 .text(d.num);
548 });
549
550 // remove old labels
551 labels.exit().remove();
552 }
553
Simon Hunta4242de2015-02-24 17:11:55 -0800554 // ==========================
555 // Module definition
556
557 angular.module('ovTopo')
558 .factory('TopoD3Service',
559 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Thomas Vachuska0af26912016-03-21 21:37:30 -0700560 'PrefsService', 'TopoToolbarService',
Simon Hunta4242de2015-02-24 17:11:55 -0800561
Thomas Vachuska0af26912016-03-21 21:37:30 -0700562 function (_$log_, _fs_, _sus_, _is_, _ts_, _ps_, _ttbs_) {
Simon Hunta4242de2015-02-24 17:11:55 -0800563 $log = _$log_;
564 fs = _fs_;
565 sus = _sus_;
566 is = _is_;
567 ts = _ts_;
Thomas Vachuska0af26912016-03-21 21:37:30 -0700568 ps = _ps_;
569 ttbs = _ttbs_;
Simon Hunta4242de2015-02-24 17:11:55 -0800570
Simon Hunta4242de2015-02-24 17:11:55 -0800571 function initD3(_api_) {
572 api = _api_;
573 }
574
575 function destroyD3() { }
576
577 return {
578 initD3: initD3,
579 destroyD3: destroyD3,
580
581 incDevLabIndex: incDevLabIndex,
Thomas Vachuska0af26912016-03-21 21:37:30 -0700582 setDevLabIndex: setDevLabIndex,
Simon Hunta4242de2015-02-24 17:11:55 -0800583 hostLabel: hostLabel,
584 deviceLabel: deviceLabel,
585 trimLabel: trimLabel,
586
Simon Hunt5674db92015-10-22 16:12:48 -0700587 updateDeviceLabel: updateDeviceRendering,
Simon Hunta4242de2015-02-24 17:11:55 -0800588 updateHostLabel: updateHostLabel,
589 updateDeviceColors: updateDeviceColors,
590
591 deviceExisting: deviceExisting,
592 hostExisting: hostExisting,
593 deviceEnter: deviceEnter,
594 hostEnter: hostEnter,
595 hostExit: hostExit,
596 deviceExit: deviceExit,
597
Simon Hunta4242de2015-02-24 17:11:55 -0800598 linkEntering: linkEntering,
599 applyLinkLabels: applyLinkLabels,
Simon Hunt1a5301e2015-02-25 15:31:25 -0800600 transformLabel: transformLabel,
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700601 applyPortLabels: applyPortLabels,
602 applyNumLinkLabels: applyNumLinkLabels
Simon Hunta4242de2015-02-24 17:11:55 -0800603 };
604 }]);
605}());