wired sxp-mapper features to gbp distro
[groupbasedpolicy.git] / groupbasedpolicy-ui / module / src / main / resources / gbp / js / joint.clean.build.js
1 /*! JointJS v0.9.3 - JavaScript diagramming library  2015-05-22 
2
3
4 This Source Code Form is subject to the terms of the Mozilla Public
5 License, v. 2.0. If a copy of the MPL was not distributed with this
6 file, You can obtain one at http://mozilla.org/MPL/2.0/.
7  */
8 (function(root, factory) {
9
10     if (typeof define === 'function' && define.amd) {
11
12         // For AMD.
13
14         define(['app/gbp/js/geometry', 'app/gbp/js/vectorizer', 'jquery', 'app/gbp/js/lodash.min', 'app/gbp/js/backbone-min'], function(g, V, $, _, Backbone) {
15
16             Backbone.$ = $;
17
18             return factory(root, Backbone, _, $, g, V);
19         });
20
21     } else if (typeof exports !== 'undefined') {
22
23         // For Node.js or CommonJS.
24
25         var Backbone = require('backbone');
26         var _ = require('lodash');
27         var $ = Backbone.$ = require('jquery');
28         var g = require('./geometry');
29         var V = require('./vectorizer');
30
31         module.exports = factory(root, Backbone, _, $, g, V);
32
33     } else {
34
35         // As a browser global.
36
37         var Backbone = root.Backbone;
38         var _ = root._;
39         var $ = Backbone.$ = root.jQuery || root.$;
40         var g = root.g;
41         var V = root.V;
42
43         root.joint = factory(root, Backbone, _, $, g, V);
44
45     }
46
47 }(this, function(root, Backbone, _, $, g, V) {
48
49 /*! JointJS v0.9.3 - JavaScript diagramming library  2015-05-22 
50
51
52 This Source Code Form is subject to the terms of the Mozilla Public
53 License, v. 2.0. If a copy of the MPL was not distributed with this
54 file, You can obtain one at http://mozilla.org/MPL/2.0/.
55  */
56 //      JointJS library.
57 //      (c) 2011-2013 client IO
58
59 // Global namespace.
60
61 var joint = {
62
63     version: '0.9.3',
64
65     // `joint.dia` namespace.
66     dia: {},
67
68     // `joint.ui` namespace.
69     ui: {},
70
71     // `joint.layout` namespace.
72     layout: {},
73
74     // `joint.shapes` namespace.
75     shapes: {},
76
77     // `joint.format` namespace.
78     format: {},
79
80     // `joint.connectors` namespace.
81     connectors: {},
82
83     // `joint.routers` namespace.
84     routers: {},
85
86     util: {
87
88         // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/.
89         hashCode: function(str) {
90
91             var hash = 0;
92             if (str.length == 0) return hash;
93             for (var i = 0; i < str.length; i++) {
94                 var c = str.charCodeAt(i);
95                 hash = ((hash << 5) - hash) + c;
96                 hash = hash & hash; // Convert to 32bit integer
97             }
98             return hash;
99         },
100
101         getByPath: function(obj, path, delim) {
102
103             delim = delim || '.';
104             var keys = path.split(delim);
105             var key;
106
107             while (keys.length) {
108                 key = keys.shift();
109                 if (Object(obj) === obj && key in obj) {
110                     obj = obj[key];
111                 } else {
112                     return undefined;
113                 }
114             }
115             return obj;
116         },
117
118         setByPath: function(obj, path, value, delim) {
119
120             delim = delim || '.';
121
122             var keys = path.split(delim);
123             var diver = obj;
124             var i = 0;
125
126             if (path.indexOf(delim) > -1) {
127
128                 for (var len = keys.length; i < len - 1; i++) {
129                     // diver creates an empty object if there is no nested object under such a key.
130                     // This means that one can populate an empty nested object with setByPath().
131                     diver = diver[keys[i]] || (diver[keys[i]] = {});
132                 }
133                 diver[keys[len - 1]] = value;
134             } else {
135                 obj[path] = value;
136             }
137             return obj;
138         },
139
140         unsetByPath: function(obj, path, delim) {
141
142             delim = delim || '.';
143
144             // index of the last delimiter
145             var i = path.lastIndexOf(delim);
146
147             if (i > -1) {
148
149                 // unsetting a nested attribute
150                 var parent = joint.util.getByPath(obj, path.substr(0, i), delim);
151
152                 if (parent) {
153                     delete parent[path.slice(i + 1)];
154                 }
155
156             } else {
157
158                 // unsetting a primitive attribute
159                 delete obj[path];
160             }
161
162             return obj;
163         },
164
165         flattenObject: function(obj, delim, stop) {
166
167             delim = delim || '.';
168             var ret = {};
169
170             for (var key in obj) {
171
172                 if (!obj.hasOwnProperty(key)) continue;
173
174                 var shouldGoDeeper = typeof obj[key] === 'object';
175                 if (shouldGoDeeper && stop && stop(obj[key])) {
176                     shouldGoDeeper = false;
177                 }
178
179                 if (shouldGoDeeper) {
180
181                     var flatObject = this.flattenObject(obj[key], delim, stop);
182
183                     for (var flatKey in flatObject) {
184                         if (!flatObject.hasOwnProperty(flatKey)) continue;
185                         ret[key + delim + flatKey] = flatObject[flatKey];
186                     }
187
188                 } else {
189
190                     ret[key] = obj[key];
191                 }
192             }
193
194             return ret;
195         },
196
197         uuid: function() {
198
199             // credit: http://stackoverflow.com/posts/2117523/revisions
200
201             return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
202                 var r = Math.random() * 16|0;
203                 var v = c == 'x' ? r : (r&0x3|0x8);
204                 return v.toString(16);
205             });
206         },
207
208         // Generate global unique id for obj and store it as a property of the object.
209         guid: function(obj) {
210
211             this.guid.id = this.guid.id || 1;
212             obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id);
213             return obj.id;
214         },
215
216         // Copy all the properties to the first argument from the following arguments.
217         // All the properties will be overwritten by the properties from the following
218         // arguments. Inherited properties are ignored.
219         mixin: function() {
220
221             var target = arguments[0];
222
223             for (var i = 1, l = arguments.length; i < l; i++) {
224
225                 var extension = arguments[i];
226
227                 // Only functions and objects can be mixined.
228
229                 if ((Object(extension) !== extension) &&
230                     !_.isFunction(extension) &&
231                     (extension === null || extension === undefined)) {
232
233                     continue;
234                 }
235
236                 _.each(extension, function(copy, key) {
237
238                     if (this.mixin.deep && (Object(copy) === copy)) {
239
240                         if (!target[key]) {
241
242                             target[key] = _.isArray(copy) ? [] : {};
243                         }
244
245                         this.mixin(target[key], copy);
246                         return;
247                     }
248
249                     if (target[key] !== copy) {
250
251                         if (!this.mixin.supplement || !target.hasOwnProperty(key)) {
252
253                             target[key] = copy;
254                         }
255
256                     }
257
258                 }, this);
259             }
260
261             return target;
262         },
263
264         // Copy all properties to the first argument from the following
265         // arguments only in case if they don't exists in the first argument.
266         // All the function propererties in the first argument will get
267         // additional property base pointing to the extenders same named
268         // property function's call method.
269         supplement: function() {
270
271             this.mixin.supplement = true;
272             var ret = this.mixin.apply(this, arguments);
273             this.mixin.supplement = false;
274             return ret;
275         },
276
277         // Same as `mixin()` but deep version.
278         deepMixin: function() {
279
280             this.mixin.deep = true;
281             var ret = this.mixin.apply(this, arguments);
282             this.mixin.deep = false;
283             return ret;
284         },
285
286         // Same as `supplement()` but deep version.
287         deepSupplement: function() {
288
289             this.mixin.deep = this.mixin.supplement = true;
290             var ret = this.mixin.apply(this, arguments);
291             this.mixin.deep = this.mixin.supplement = false;
292             return ret;
293         },
294
295         normalizeEvent: function(evt) {
296
297             return (evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches.length) ? evt.originalEvent.changedTouches[0] : evt;
298         },
299
300         nextFrame:(function() {
301
302             var raf;
303             var client = typeof window != 'undefined';
304
305             if (client) {
306
307                 raf = window.requestAnimationFrame     ||
308                     window.webkitRequestAnimationFrame ||
309                     window.mozRequestAnimationFrame    ||
310                     window.oRequestAnimationFrame      ||
311                     window.msRequestAnimationFrame;
312             }
313
314             if (!raf) {
315
316                 var lastTime = 0;
317
318                 raf = function(callback) {
319
320                     var currTime = new Date().getTime();
321                     var timeToCall = Math.max(0, 16 - (currTime - lastTime));
322                     var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
323
324                     lastTime = currTime + timeToCall;
325
326                     return id;
327                 };
328             }
329
330             return client ? _.bind(raf, window) : raf;
331
332         })(),
333
334         cancelFrame: (function() {
335
336             var caf;
337             var client = typeof window != 'undefined';
338
339             if (client) {
340
341                 caf = window.cancelAnimationFrame            ||
342                     window.webkitCancelAnimationFrame        ||
343                     window.webkitCancelRequestAnimationFrame ||
344                     window.msCancelAnimationFrame            ||
345                     window.msCancelRequestAnimationFrame     ||
346                     window.oCancelAnimationFrame             ||
347                     window.oCancelRequestAnimationFrame      ||
348                     window.mozCancelAnimationFrame           ||
349                     window.mozCancelRequestAnimationFrame;
350             }
351
352             caf = caf || clearTimeout;
353
354             return client ? _.bind(caf, window) : caf;
355
356         })(),
357
358         shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) {
359
360             var bbox;
361             var spot;
362
363             if (!magnet) {
364
365                 // There is no magnet, try to make the best guess what is the
366                 // wrapping SVG element. This is because we want this "smart"
367                 // connection points to work out of the box without the
368                 // programmer to put magnet marks to any of the subelements.
369                 // For example, we want the functoin to work on basic.Path elements
370                 // without any special treatment of such elements.
371                 // The code below guesses the wrapping element based on
372                 // one simple assumption. The wrapping elemnet is the
373                 // first child of the scalable group if such a group exists
374                 // or the first child of the rotatable group if not.
375                 // This makese sense because usually the wrapping element
376                 // is below any other sub element in the shapes.
377                 var scalable = view.$('.scalable')[0];
378                 var rotatable = view.$('.rotatable')[0];
379
380                 if (scalable && scalable.firstChild) {
381
382                     magnet = scalable.firstChild;
383
384                 } else if (rotatable && rotatable.firstChild) {
385
386                     magnet = rotatable.firstChild;
387                 }
388             }
389
390             if (magnet) {
391
392                 spot = V(magnet).findIntersection(reference, linkView.paper.viewport);
393                 if (!spot) {
394                     bbox = g.rect(V(magnet).bbox(false, linkView.paper.viewport));
395                 }
396
397             } else {
398
399                 bbox = view.model.getBBox();
400                 spot = bbox.intersectionWithLineFromCenterToPoint(reference);
401             }
402             return spot || bbox.center();
403         },
404
405         breakText: function(text, size, styles, opt) {
406
407             opt = opt || {};
408
409             var width = size.width;
410             var height = size.height;
411
412             var svgDocument = opt.svgDocument || V('svg').node;
413             var textElement = V('<text><tspan></tspan></text>').attr(styles || {}).node;
414             var textSpan = textElement.firstChild;
415             var textNode = document.createTextNode('');
416
417             textSpan.appendChild(textNode);
418
419             svgDocument.appendChild(textElement);
420
421             if (!opt.svgDocument) {
422
423                 document.body.appendChild(svgDocument);
424             }
425
426             var words = text.split(' ');
427             var full = [];
428             var lines = [];
429             var p;
430
431             for (var i = 0, l = 0, len = words.length; i < len; i++) {
432
433                 var word = words[i];
434
435                 textNode.data = lines[l] ? lines[l] + ' ' + word : word;
436
437                 if (textSpan.getComputedTextLength() <= width) {
438
439                     // the current line fits
440                     lines[l] = textNode.data;
441
442                     if (p) {
443                         // We were partitioning. Put rest of the word onto next line
444                         full[l++] = true;
445
446                         // cancel partitioning
447                         p = 0;
448                     }
449
450                 } else {
451
452                     if (!lines[l] || p) {
453
454                         var partition = !!p;
455
456                         p = word.length - 1;
457
458                         if (partition || !p) {
459
460                             // word has only one character.
461                             if (!p) {
462
463                                 if (!lines[l]) {
464
465                                     // we won't fit this text within our rect
466                                     lines = [];
467
468                                     break;
469                                 }
470
471                                 // partitioning didn't help on the non-empty line
472                                 // try again, but this time start with a new line
473
474                                 // cancel partitions created
475                                 words.splice(i, 2, word + words[i + 1]);
476
477                                 // adjust word length
478                                 len--;
479
480                                 full[l++] = true;
481                                 i--;
482
483                                 continue;
484                             }
485
486                             // move last letter to the beginning of the next word
487                             words[i] = word.substring(0, p);
488                             words[i + 1] = word.substring(p) + words[i + 1];
489
490                         } else {
491
492                             // We initiate partitioning
493                             // split the long word into two words
494                             words.splice(i, 1, word.substring(0, p), word.substring(p));
495
496                             // adjust words length
497                             len++;
498
499                             if (l && !full[l - 1]) {
500                                 // if the previous line is not full, try to fit max part of
501                                 // the current word there
502                                 l--;
503                             }
504                         }
505
506                         i--;
507
508                         continue;
509                     }
510
511                     l++;
512                     i--;
513                 }
514
515                 // if size.height is defined we have to check whether the height of the entire
516                 // text exceeds the rect height
517                 if (typeof height !== 'undefined') {
518
519                     // get line height as text height / 0.8 (as text height is approx. 0.8em
520                     // and line height is 1em. See vectorizer.text())
521                     var lh = lh || textElement.getBBox().height * 1.25;
522
523                     if (lh * lines.length > height) {
524
525                         // remove overflowing lines
526                         lines.splice(Math.floor(height / lh));
527
528                         break;
529                     }
530                 }
531             }
532
533             if (opt.svgDocument) {
534
535                 // svg document was provided, remove the text element only
536                 svgDocument.removeChild(textElement);
537
538             } else {
539
540                 // clean svg document
541                 document.body.removeChild(svgDocument);
542             }
543
544             return lines.join('\n');
545         },
546
547         imageToDataUri: function(url, callback) {
548
549             if (!url || url.substr(0, 'data:'.length) === 'data:') {
550                 // No need to convert to data uri if it is already in data uri.
551
552                 // This not only convenient but desired. For example,
553                 // IE throws a security error if data:image/svg+xml is used to render
554                 // an image to the canvas and an attempt is made to read out data uri.
555                 // Now if our image is already in data uri, there is no need to render it to the canvas
556                 // and so we can bypass this error.
557
558                 // Keep the async nature of the function.
559                 return setTimeout(function() { callback(null, url); }, 0);
560             }
561
562             var canvas = document.createElement('canvas');
563             var img = document.createElement('img');
564
565             img.onload = function() {
566
567                 var ctx = canvas.getContext('2d');
568
569                 canvas.width = img.width;
570                 canvas.height = img.height;
571
572                 ctx.drawImage(img, 0, 0);
573
574                 try {
575
576                     // Guess the type of the image from the url suffix.
577                     var suffix = (url.split('.').pop()) || 'png';
578                     // A little correction for JPEGs. There is no image/jpg mime type but image/jpeg.
579                     var type = 'image/' + (suffix === 'jpg') ? 'jpeg' : suffix;
580                     var dataUri = canvas.toDataURL(type);
581
582                 } catch (e) {
583
584                     if (/\.svg$/.test(url)) {
585                         // IE throws a security error if we try to render an SVG into the canvas.
586                         // Luckily for us, we don't need canvas at all to convert
587                         // SVG to data uri. We can just use AJAX to load the SVG string
588                         // and construct the data uri ourselves.
589                         var xhr = window.XMLHttpRequest ? new XMLHttpRequest : new ActiveXObject('Microsoft.XMLHTTP');
590                         xhr.open('GET', url, false);
591                         xhr.send(null);
592                         var svg = xhr.responseText;
593
594                         return callback(null, 'data:image/svg+xml,' + encodeURIComponent(svg));
595                     }
596
597                     console.error(img.src, 'fails to convert', e);
598                 }
599
600                 callback(null, dataUri);
601             };
602
603             img.ononerror = function() {
604
605                 callback(new Error('Failed to load image.'));
606             };
607
608             img.src = url;
609         },
610
611         timing: {
612
613             linear: function(t) {
614                 return t;
615             },
616
617             quad: function(t) {
618                 return t * t;
619             },
620
621             cubic: function(t) {
622                 return t * t * t;
623             },
624
625             inout: function(t) {
626                 if (t <= 0) return 0;
627                 if (t >= 1) return 1;
628                 var t2 = t * t;
629                 var t3 = t2 * t;
630                 return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75);
631             },
632
633             exponential: function(t) {
634                 return Math.pow(2, 10 * (t - 1));
635             },
636
637             bounce: function(t) {
638                 for (var a = 0, b = 1; 1; a += b, b /= 2) {
639                     if (t >= (7 - 4 * a) / 11) {
640                         var q = (11 - 6 * a - 11 * t) / 4;
641                         return -q * q + b * b;
642                     }
643                 }
644             },
645
646             reverse: function(f) {
647                 return function(t) {
648                     return 1 - f(1 - t);
649                 };
650             },
651
652             reflect: function(f) {
653                 return function(t) {
654                     return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t)));
655                 };
656             },
657
658             clamp: function(f, n, x) {
659                 n = n || 0;
660                 x = x || 1;
661                 return function(t) {
662                     var r = f(t);
663                     return r < n ? n : r > x ? x : r;
664                 };
665             },
666
667             back: function(s) {
668                 if (!s) s = 1.70158;
669                 return function(t) {
670                     return t * t * ((s + 1) * t - s);
671                 };
672             },
673
674             elastic: function(x) {
675                 if (!x) x = 1.5;
676                 return function(t) {
677                     return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t);
678                 };
679             }
680         },
681
682         interpolate: {
683
684             number: function(a, b) {
685                 var d = b - a;
686                 return function(t) { return a + d * t; };
687             },
688
689             object: function(a, b) {
690                 var s = _.keys(a);
691                 return function(t) {
692                     var i, p;
693                     var r = {};
694                     for (i = s.length - 1; i != -1; i--) {
695                         p = s[i];
696                         r[p] = a[p] + (b[p] - a[p]) * t;
697                     }
698                     return r;
699                 };
700             },
701
702             hexColor: function(a, b) {
703
704                 var ca = parseInt(a.slice(1), 16);
705                 var cb = parseInt(b.slice(1), 16);
706                 var ra = ca & 0x0000ff;
707                 var rd = (cb & 0x0000ff) - ra;
708                 var ga = ca & 0x00ff00;
709                 var gd = (cb & 0x00ff00) - ga;
710                 var ba = ca & 0xff0000;
711                 var bd = (cb & 0xff0000) - ba;
712
713                 return function(t) {
714
715                     var r = (ra + rd * t) & 0x000000ff;
716                     var g = (ga + gd * t) & 0x0000ff00;
717                     var b = (ba + bd * t) & 0x00ff0000;
718
719                     return '#' + (1 << 24 | r | g | b ).toString(16).slice(1);
720                 };
721             },
722
723             unit: function(a, b) {
724
725                 var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/;
726                 var ma = r.exec(a);
727                 var mb = r.exec(b);
728                 var p = mb[1].indexOf('.');
729                 var f = p > 0 ? mb[1].length - p - 1 : 0;
730                 a = +ma[1];
731                 var d = +mb[1] - a;
732                 var u = ma[2];
733
734                 return function(t) {
735                     return (a + d * t).toFixed(f) + u;
736                 };
737             }
738         },
739
740         // SVG filters.
741         filter: {
742
743             // `x` ... horizontal blur
744             // `y` ... vertical blur (optional)
745             blur: function(args) {
746
747                 var x = _.isFinite(args.x) ? args.x : 2;
748
749                 return _.template('<filter><feGaussianBlur stdDeviation="${stdDeviation}"/></filter>', {
750                     stdDeviation: _.isFinite(args.y) ? [x, args.y] : x
751                 });
752             },
753
754             // `dx` ... horizontal shift
755             // `dy` ... vertical shift
756             // `blur` ... blur
757             // `color` ... color
758             // `opacity` ... opacity
759             dropShadow: function(args) {
760
761                 var tpl = 'SVGFEDropShadowElement' in window
762                     ? '<filter><feDropShadow stdDeviation="${blur}" dx="${dx}" dy="${dy}" flood-color="${color}" flood-opacity="${opacity}"/></filter>'
763                     : '<filter><feGaussianBlur in="SourceAlpha" stdDeviation="${blur}"/><feOffset dx="${dx}" dy="${dy}" result="offsetblur"/><feFlood flood-color="${color}"/><feComposite in2="offsetblur" operator="in"/><feComponentTransfer><feFuncA type="linear" slope="${opacity}"/></feComponentTransfer><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
764
765                 return _.template(tpl, {
766                     dx: args.dx || 0,
767                     dy: args.dy || 0,
768                     opacity: _.isFinite(args.opacity) ? args.opacity : 1,
769                     color: args.color || 'black',
770                     blur: _.isFinite(args.blur) ? args.blur : 4
771                 });
772             },
773
774             // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged.
775             grayscale: function(args) {
776
777                 var amount = _.isFinite(args.amount) ? args.amount : 1;
778
779                 return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 0 ${d} ${e} ${f} 0 0 ${g} ${b} ${h} 0 0 0 0 0 1 0"/></filter>', {
780                     a: 0.2126 + 0.7874 * (1 - amount),
781                     b: 0.7152 - 0.7152 * (1 - amount),
782                     c: 0.0722 - 0.0722 * (1 - amount),
783                     d: 0.2126 - 0.2126 * (1 - amount),
784                     e: 0.7152 + 0.2848 * (1 - amount),
785                     f: 0.0722 - 0.0722 * (1 - amount),
786                     g: 0.2126 - 0.2126 * (1 - amount),
787                     h: 0.0722 + 0.9278 * (1 - amount)
788                 });
789             },
790
791             // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged.
792             sepia: function(args) {
793
794                 var amount = _.isFinite(args.amount) ? args.amount : 1;
795
796                 return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 0 ${d} ${e} ${f} 0 0 ${g} ${h} ${i} 0 0 0 0 0 1 0"/></filter>', {
797                     a: 0.393 + 0.607 * (1 - amount),
798                     b: 0.769 - 0.769 * (1 - amount),
799                     c: 0.189 - 0.189 * (1 - amount),
800                     d: 0.349 - 0.349 * (1 - amount),
801                     e: 0.686 + 0.314 * (1 - amount),
802                     f: 0.168 - 0.168 * (1 - amount),
803                     g: 0.272 - 0.272 * (1 - amount),
804                     h: 0.534 - 0.534 * (1 - amount),
805                     i: 0.131 + 0.869 * (1 - amount)
806                 });
807             },
808
809             // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged.
810             saturate: function(args) {
811
812                 var amount = _.isFinite(args.amount) ? args.amount : 1;
813
814                 return _.template('<filter><feColorMatrix type="saturate" values="${amount}"/></filter>', {
815                     amount: 1 - amount
816                 });
817             },
818
819             // `angle` ...  the number of degrees around the color circle the input samples will be adjusted.
820             hueRotate: function(args) {
821
822                 return _.template('<filter><feColorMatrix type="hueRotate" values="${angle}"/></filter>', {
823                     angle: args.angle || 0
824                 });
825             },
826
827             // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged.
828             invert: function(args) {
829
830                 var amount = _.isFinite(args.amount) ? args.amount : 1;
831
832                 return _.template('<filter><feComponentTransfer><feFuncR type="table" tableValues="${amount} ${amount2}"/><feFuncG type="table" tableValues="${amount} ${amount2}"/><feFuncB type="table" tableValues="${amount} ${amount2}"/></feComponentTransfer></filter>', {
833                     amount: amount,
834                     amount2: 1 - amount
835                 });
836             },
837
838             // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged.
839             brightness: function(args) {
840
841                 return _.template('<filter><feComponentTransfer><feFuncR type="linear" slope="${amount}"/><feFuncG type="linear" slope="${amount}"/><feFuncB type="linear" slope="${amount}"/></feComponentTransfer></filter>', {
842                     amount: _.isFinite(args.amount) ? args.amount : 1
843                 });
844             },
845
846             // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged.
847             contrast: function(args) {
848
849                 var amount = _.isFinite(args.amount) ? args.amount : 1;
850
851                 return _.template('<filter><feComponentTransfer><feFuncR type="linear" slope="${amount}" intercept="${amount2}"/><feFuncG type="linear" slope="${amount}" intercept="${amount2}"/><feFuncB type="linear" slope="${amount}" intercept="${amount2}"/></feComponentTransfer></filter>', {
852                     amount: amount,
853                     amount2: .5 - amount / 2
854                 });
855             }
856         },
857
858         format: {
859
860             // Formatting numbers via the Python Format Specification Mini-language.
861             // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language.
862             // Heavilly inspired by the D3.js library implementation.
863             number: function(specifier, value, locale) {
864
865                 locale = locale || {
866
867                     currency: ['$', ''],
868                     decimal: '.',
869                     thousands: ',',
870                     grouping: [3]
871                 };
872
873                 // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language.
874                 // [[fill]align][sign][symbol][0][width][,][.precision][type]
875                 var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i;
876
877                 var match = re.exec(specifier);
878                 var fill = match[1] || ' ';
879                 var align = match[2] || '>';
880                 var sign = match[3] || '';
881                 var symbol = match[4] || '';
882                 var zfill = match[5];
883                 var width = +match[6];
884                 var comma = match[7];
885                 var precision = match[8];
886                 var type = match[9];
887                 var scale = 1;
888                 var prefix = '';
889                 var suffix = '';
890                 var integer = false;
891
892                 if (precision) precision = +precision.substring(1);
893
894                 if (zfill || fill === '0' && align === '=') {
895                     zfill = fill = '0';
896                     align = '=';
897                     if (comma) width -= Math.floor((width - 1) / 4);
898                 }
899
900                 switch (type) {
901                     case 'n': comma = true; type = 'g'; break;
902                     case '%': scale = 100; suffix = '%'; type = 'f'; break;
903                     case 'p': scale = 100; suffix = '%'; type = 'r'; break;
904                     case 'b':
905                     case 'o':
906                     case 'x':
907                     case 'X': if (symbol === '#') prefix = '0' + type.toLowerCase();
908                     case 'c':
909                     case 'd': integer = true; precision = 0; break;
910                     case 's': scale = -1; type = 'r'; break;
911                 }
912
913                 if (symbol === '$') {
914                     prefix = locale.currency[0];
915                     suffix = locale.currency[1];
916                 }
917
918                 // If no precision is specified for `'r'`, fallback to general notation.
919                 if (type == 'r' && !precision) type = 'g';
920
921                 // Ensure that the requested precision is in the supported range.
922                 if (precision != null) {
923                     if (type == 'g') precision = Math.max(1, Math.min(21, precision));
924                     else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision));
925                 }
926
927                 var zcomma = zfill && comma;
928
929                 // Return the empty string for floats formatted as ints.
930                 if (integer && (value % 1)) return '';
931
932                 // Convert negative to positive, and record the sign prefix.
933                 var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign;
934
935                 var fullSuffix = suffix;
936
937                 // Apply the scale, computing it from the value's exponent for si format.
938                 // Preserve the existing suffix, if any, such as the currency symbol.
939                 if (scale < 0) {
940                     var unit = this.prefix(value, precision);
941                     value = unit.scale(value);
942                     fullSuffix = unit.symbol + suffix;
943                 } else {
944                     value *= scale;
945                 }
946
947                 // Convert to the desired precision.
948                 value = this.convert(type, value, precision);
949
950                 // Break the value into the integer part (before) and decimal part (after).
951                 var i = value.lastIndexOf('.');
952                 var before = i < 0 ? value : value.substring(0, i);
953                 var after = i < 0 ? '' : locale.decimal + value.substring(i + 1);
954
955                 function formatGroup(value) {
956
957                     var i = value.length;
958                     var t = [];
959                     var j = 0;
960                     var g = locale.grouping[0];
961                     while (i > 0 && g > 0) {
962                         t.push(value.substring(i -= g, i + g));
963                         g = locale.grouping[j = (j + 1) % locale.grouping.length];
964                     }
965                     return t.reverse().join(locale.thousands);
966                 }
967
968                 // If the fill character is not `'0'`, grouping is applied before padding.
969                 if (!zfill && comma && locale.grouping) {
970
971                     before = formatGroup(before);
972                 }
973
974                 var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length);
975                 var padding = length < width ? new Array(length = width - length + 1).join(fill) : '';
976
977                 // If the fill character is `'0'`, grouping is applied after padding.
978                 if (zcomma) before = formatGroup(padding + before);
979
980                 // Apply prefix.
981                 negative += prefix;
982
983                 // Rejoin integer and decimal parts.
984                 value = before + after;
985
986                 return (align === '<' ? negative + value + padding
987                         : align === '>' ? padding + negative + value
988                         : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length)
989                         : negative + (zcomma ? value : padding + value)) + fullSuffix;
990             },
991
992             // Formatting string via the Python Format string.
993             // See https://docs.python.org/2/library/string.html#format-string-syntax)
994             string: function(formatString, value) {
995
996                 var fieldDelimiterIndex;
997                 var fieldDelimiter = '{';
998                 var endPlaceholder = false;
999                 var formattedStringArray = [];
1000
1001                 while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) {
1002
1003                     var pieceFormatedString, formatSpec, fieldName;
1004
1005                     pieceFormatedString = formatString.slice(0, fieldDelimiterIndex);
1006
1007                     if (endPlaceholder) {
1008                         formatSpec = pieceFormatedString.split(':');
1009                         fieldName = formatSpec.shift().split('.');
1010                         pieceFormatedString = value;
1011
1012                         for (var i = 0; i < fieldName.length; i++)
1013                             pieceFormatedString = pieceFormatedString[fieldName[i]];
1014
1015                         if (formatSpec.length)
1016                             pieceFormatedString = this.number(formatSpec, pieceFormatedString);
1017                     }
1018
1019                     formattedStringArray.push(pieceFormatedString);
1020
1021                     formatString = formatString.slice(fieldDelimiterIndex + 1);
1022                     fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{';
1023                 }
1024                 formattedStringArray.push(formatString);
1025
1026                 return formattedStringArray.join('');
1027             },
1028
1029             convert: function(type, value, precision) {
1030
1031                 switch (type) {
1032                     case 'b': return value.toString(2);
1033                     case 'c': return String.fromCharCode(value);
1034                     case 'o': return value.toString(8);
1035                     case 'x': return value.toString(16);
1036                     case 'X': return value.toString(16).toUpperCase();
1037                     case 'g': return value.toPrecision(precision);
1038                     case 'e': return value.toExponential(precision);
1039                     case 'f': return value.toFixed(precision);
1040                     case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision))));
1041                     default: return value + '';
1042                 }
1043             },
1044
1045             round: function(value, precision) {
1046
1047                 return precision
1048                     ? Math.round(value * (precision = Math.pow(10, precision))) / precision
1049                     : Math.round(value);
1050             },
1051
1052             precision: function(value, precision) {
1053
1054                 return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1);
1055             },
1056
1057             prefix: function(value, precision) {
1058
1059                 var prefixes = _.map(['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], function(d, i) {
1060                     var k = Math.pow(10, abs(8 - i) * 3);
1061                     return {
1062                         scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; },
1063                         symbol: d
1064                     };
1065                 });
1066
1067                 var i = 0;
1068                 if (value) {
1069                     if (value < 0) value *= -1;
1070                     if (precision) value = this.round(value, this.precision(value, precision));
1071                     i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10);
1072                     i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3));
1073                 }
1074                 return prefixes[8 + i / 3];
1075             }
1076         }
1077     }
1078 };
1079
1080 //      JointJS, the JavaScript diagramming library.
1081 //      (c) 2011-2013 client IO
1082
1083 joint.dia.GraphCells = Backbone.Collection.extend({
1084
1085     initialize: function() {
1086
1087         // Backbone automatically doesn't trigger re-sort if models attributes are changed later when
1088         // they're already in the collection. Therefore, we're triggering sort manually here.
1089         this.on('change:z', this.sort, this);
1090     },
1091
1092     model: function(attrs, options) {
1093
1094         if (attrs.type === 'link') {
1095
1096             return new joint.dia.Link(attrs, options);
1097         }
1098
1099         var module = attrs.type.split('.')[0];
1100         var entity = attrs.type.split('.')[1];
1101
1102         if (joint.shapes[module] && joint.shapes[module][entity]) {
1103
1104             return new joint.shapes[module][entity](attrs, options);
1105         }
1106
1107         return new joint.dia.Element(attrs, options);
1108     },
1109
1110     // `comparator` makes it easy to sort cells based on their `z` index.
1111     comparator: function(model) {
1112
1113         return model.get('z') || 0;
1114     },
1115
1116     // Get all inbound and outbound links connected to the cell `model`.
1117     getConnectedLinks: function(model, opt) {
1118
1119         opt = opt || {};
1120
1121         if (_.isUndefined(opt.inbound) && _.isUndefined(opt.outbound)) {
1122             opt.inbound = opt.outbound = true;
1123         }
1124
1125         var links = this.filter(function(cell) {
1126
1127             var source = cell.get('source');
1128             var target = cell.get('target');
1129
1130             return (source && source.id === model.id && opt.outbound) ||
1131                 (target && target.id === model.id  && opt.inbound);
1132         });
1133
1134         // option 'deep' returns all links that are connected to any of the descendent cell
1135         // and are not descendents itself
1136         if (opt.deep) {
1137
1138             var embeddedCells = model.getEmbeddedCells({ deep: true });
1139
1140             _.each(this.difference(links, embeddedCells), function(cell) {
1141
1142                 if (opt.outbound) {
1143
1144                     var source = cell.get('source');
1145
1146                     if (source && source.id && _.find(embeddedCells, { id: source.id })) {
1147                         links.push(cell);
1148                         return; // prevent a loop link to be pushed twice
1149                     }
1150                 }
1151
1152                 if (opt.inbound) {
1153
1154                     var target = cell.get('target');
1155
1156                     if (target && target.id && _.find(embeddedCells, { id: target.id })) {
1157                         links.push(cell);
1158                     }
1159                 }
1160             });
1161         }
1162
1163         return links;
1164     },
1165
1166     getCommonAncestor: function(/* cells */) {
1167
1168         var cellsAncestors = _.map(arguments, function(cell) {
1169
1170             var ancestors = [cell.id];
1171             var parentId = cell.get('parent');
1172
1173             while (parentId) {
1174
1175                 ancestors.push(parentId);
1176                 parentId = this.get(parentId).get('parent');
1177             }
1178
1179             return ancestors;
1180
1181         }, this);
1182
1183         cellsAncestors = _.sortBy(cellsAncestors, 'length');
1184
1185         var commonAncestor = _.find(cellsAncestors.shift(), function(ancestor) {
1186
1187             return _.every(cellsAncestors, function(cellAncestors) {
1188                 return _.contains(cellAncestors, ancestor);
1189             });
1190         });
1191
1192         return this.get(commonAncestor);
1193     },
1194
1195     // Return the bounding box of all cells in array provided. If no array
1196     // provided returns bounding box of all cells. Links are being ignored.
1197     getBBox: function(cells) {
1198
1199         cells = cells || this.models;
1200
1201         var origin = { x: Infinity, y: Infinity };
1202         var corner = { x: -Infinity, y: -Infinity };
1203
1204         _.each(cells, function(cell) {
1205
1206             // Links has no bounding box defined on the model.
1207             if (cell.isLink()) return;
1208
1209             var bbox = cell.getBBox();
1210             origin.x = Math.min(origin.x, bbox.x);
1211             origin.y = Math.min(origin.y, bbox.y);
1212             corner.x = Math.max(corner.x, bbox.x + bbox.width);
1213             corner.y = Math.max(corner.y, bbox.y + bbox.height);
1214         });
1215
1216         return g.rect(origin.x, origin.y, corner.x - origin.x, corner.y - origin.y);
1217     }
1218 });
1219
1220
1221 joint.dia.Graph = Backbone.Model.extend({
1222
1223     initialize: function(attrs, opt) {
1224
1225         // Passing `cellModel` function in the options object to graph allows for
1226         // setting models based on attribute objects. This is especially handy
1227         // when processing JSON graphs that are in a different than JointJS format.
1228         this.set('cells', new joint.dia.GraphCells([], { model: opt && opt.cellModel }));
1229
1230         // Make all the events fired in the `cells` collection available.
1231         // to the outside world.
1232         this.get('cells').on('all', this.trigger, this);
1233
1234         this.get('cells').on('remove', this.removeCell, this);
1235     },
1236
1237     toJSON: function() {
1238
1239         // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections.
1240         // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely.
1241         var json = Backbone.Model.prototype.toJSON.apply(this, arguments);
1242         json.cells = this.get('cells').toJSON();
1243         return json;
1244     },
1245
1246     fromJSON: function(json, opt) {
1247
1248         if (!json.cells) {
1249
1250             throw new Error('Graph JSON must contain cells array.');
1251         }
1252
1253         this.set(_.omit(json, 'cells'), opt);
1254         this.resetCells(json.cells, opt);
1255     },
1256
1257     clear: function(opt) {
1258
1259         this.trigger('batch:start');
1260         this.get('cells').remove(this.get('cells').models, opt);
1261         this.trigger('batch:stop');
1262     },
1263
1264     _prepareCell: function(cell) {
1265
1266         if (cell instanceof Backbone.Model && _.isUndefined(cell.get('z'))) {
1267
1268             cell.set('z', this.maxZIndex() + 1, { silent: true });
1269
1270         } else if (_.isUndefined(cell.z)) {
1271
1272             cell.z = this.maxZIndex() + 1;
1273         }
1274
1275         return cell;
1276     },
1277
1278     maxZIndex: function() {
1279
1280         var lastCell = this.get('cells').last();
1281         return lastCell ? (lastCell.get('z') || 0) : 0;
1282     },
1283
1284     addCell: function(cell, options) {
1285
1286         if (_.isArray(cell)) {
1287
1288             return this.addCells(cell, options);
1289         }
1290
1291         this.get('cells').add(this._prepareCell(cell), options || {});
1292
1293         return this;
1294     },
1295
1296     addCells: function(cells, options) {
1297
1298         options = options || {};
1299         options.position = cells.length;
1300
1301         _.each(cells, function(cell) {
1302             options.position--;
1303             this.addCell(cell, options);
1304         }, this);
1305
1306         return this;
1307     },
1308
1309     // When adding a lot of cells, it is much more efficient to
1310     // reset the entire cells collection in one go.
1311     // Useful for bulk operations and optimizations.
1312     resetCells: function(cells, opt) {
1313
1314         this.get('cells').reset(_.map(cells, this._prepareCell, this), opt);
1315
1316         return this;
1317     },
1318
1319     removeCell: function(cell, collection, options) {
1320
1321         // Applications might provide a `disconnectLinks` option set to `true` in order to
1322         // disconnect links when a cell is removed rather then removing them. The default
1323         // is to remove all the associated links.
1324         if (options && options.disconnectLinks) {
1325
1326             this.disconnectLinks(cell, options);
1327
1328         } else {
1329
1330             this.removeLinks(cell, options);
1331         }
1332
1333         // Silently remove the cell from the cells collection. Silently, because
1334         // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is
1335         // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events
1336         // would be triggered on the graph model.
1337         this.get('cells').remove(cell, { silent: true });
1338     },
1339
1340     // Get a cell by `id`.
1341     getCell: function(id) {
1342
1343         return this.get('cells').get(id);
1344     },
1345
1346     getElements: function() {
1347
1348         return this.get('cells').filter(function(cell) {
1349
1350             return cell instanceof joint.dia.Element;
1351         });
1352     },
1353
1354     getLinks: function() {
1355
1356         return this.get('cells').filter(function(cell) {
1357
1358             return cell instanceof joint.dia.Link;
1359         });
1360     },
1361
1362     // Get all inbound and outbound links connected to the cell `model`.
1363     getConnectedLinks: function(model, opt) {
1364
1365         return this.get('cells').getConnectedLinks(model, opt);
1366     },
1367
1368     getNeighbors: function(el) {
1369
1370         var links = this.getConnectedLinks(el);
1371         var neighbors = [];
1372         var cells = this.get('cells');
1373
1374         _.each(links, function(link) {
1375
1376             var source = link.get('source');
1377             var target = link.get('target');
1378
1379             // Discard if it is a point.
1380             if (!source.x) {
1381                 var sourceElement = cells.get(source.id);
1382                 if (sourceElement !== el) {
1383
1384                     neighbors.push(sourceElement);
1385                 }
1386             }
1387             if (!target.x) {
1388                 var targetElement = cells.get(target.id);
1389                 if (targetElement !== el) {
1390
1391                     neighbors.push(targetElement);
1392                 }
1393             }
1394         });
1395
1396         return neighbors;
1397     },
1398
1399     // Disconnect links connected to the cell `model`.
1400     disconnectLinks: function(model, options) {
1401
1402         _.each(this.getConnectedLinks(model), function(link) {
1403
1404             link.set(link.get('source').id === model.id ? 'source' : 'target', g.point(0, 0), options);
1405         });
1406     },
1407
1408     // Remove links connected to the cell `model` completely.
1409     removeLinks: function(model, options) {
1410
1411         _.invoke(this.getConnectedLinks(model), 'remove', options);
1412     },
1413
1414     // Find all views at given point
1415     findModelsFromPoint: function(p) {
1416
1417         return _.filter(this.getElements(), function(el) {
1418             return el.getBBox().containsPoint(p);
1419         });
1420     },
1421
1422     // Find all views in given area
1423     findModelsInArea: function(r) {
1424
1425         return _.filter(this.getElements(), function(el) {
1426             return el.getBBox().intersect(r);
1427         });
1428     },
1429
1430     // Return the bounding box of all `elements`.
1431     getBBox: function(/* elements */) {
1432
1433         var collection = this.get('cells');
1434         return collection.getBBox.apply(collection, arguments);
1435     },
1436
1437     getCommonAncestor: function(/* cells */) {
1438
1439         var collection = this.get('cells');
1440         return collection.getCommonAncestor.apply(collection, arguments);
1441     }
1442 });
1443
1444 //      JointJS.
1445 //      (c) 2011-2013 client IO
1446
1447 // joint.dia.Cell base model.
1448 // --------------------------
1449
1450 joint.dia.Cell = Backbone.Model.extend({
1451
1452     // This is the same as Backbone.Model with the only difference that is uses _.merge
1453     // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes.
1454     constructor: function(attributes, options) {
1455
1456         var defaults;
1457         var attrs = attributes || {};
1458         this.cid = _.uniqueId('c');
1459         this.attributes = {};
1460         if (options && options.collection) this.collection = options.collection;
1461         if (options && options.parse) attrs = this.parse(attrs, options) || {};
1462         if (defaults = _.result(this, 'defaults')) {
1463             //<custom code>
1464             // Replaced the call to _.defaults with _.merge.
1465             attrs = _.merge({}, defaults, attrs);
1466             //</custom code>
1467         }
1468         this.set(attrs, options);
1469         this.changed = {};
1470         this.initialize.apply(this, arguments);
1471     },
1472
1473     toJSON: function() {
1474
1475         var defaultAttrs = this.constructor.prototype.defaults.attrs || {};
1476         var attrs = this.attributes.attrs;
1477         var finalAttrs = {};
1478
1479         // Loop through all the attributes and
1480         // omit the default attributes as they are implicitly reconstructable by the cell 'type'.
1481         _.each(attrs, function(attr, selector) {
1482
1483             var defaultAttr = defaultAttrs[selector];
1484
1485             _.each(attr, function(value, name) {
1486
1487                 // attr is mainly flat though it might have one more level (consider the `style` attribute).
1488                 // Check if the `value` is object and if yes, go one level deep.
1489                 if (_.isObject(value) && !_.isArray(value)) {
1490
1491                     _.each(value, function(value2, name2) {
1492
1493                         if (!defaultAttr || !defaultAttr[name] || !_.isEqual(defaultAttr[name][name2], value2)) {
1494
1495                             finalAttrs[selector] = finalAttrs[selector] || {};
1496                             (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2;
1497                         }
1498                     });
1499
1500                 } else if (!defaultAttr || !_.isEqual(defaultAttr[name], value)) {
1501                     // `value` is not an object, default attribute for such a selector does not exist
1502                     // or it is different than the attribute value set on the model.
1503
1504                     finalAttrs[selector] = finalAttrs[selector] || {};
1505                     finalAttrs[selector][name] = value;
1506                 }
1507             });
1508         });
1509
1510         var attributes = _.cloneDeep(_.omit(this.attributes, 'attrs'));
1511         //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs')));
1512         attributes.attrs = finalAttrs;
1513
1514         return attributes;
1515     },
1516
1517     initialize: function(options) {
1518
1519         if (!options || !options.id) {
1520
1521             this.set('id', joint.util.uuid(), { silent: true });
1522         }
1523
1524         this._transitionIds = {};
1525
1526         // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes.
1527         this.processPorts();
1528         this.on('change:attrs', this.processPorts, this);
1529     },
1530
1531     processPorts: function() {
1532
1533         // Whenever `attrs` changes, we extract ports from the `attrs` object and store it
1534         // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source`
1535         // set to that port, we remove those links as well (to follow the same behaviour as
1536         // with a removed element).
1537
1538         var previousPorts = this.ports;
1539
1540         // Collect ports from the `attrs` object.
1541         var ports = {};
1542         _.each(this.get('attrs'), function(attrs, selector) {
1543
1544             if (attrs && attrs.port) {
1545
1546                 // `port` can either be directly an `id` or an object containing an `id` (and potentially other data).
1547                 if (!_.isUndefined(attrs.port.id)) {
1548                     ports[attrs.port.id] = attrs.port;
1549                 } else {
1550                     ports[attrs.port] = { id: attrs.port };
1551                 }
1552             }
1553         });
1554
1555         // Collect ports that have been removed (compared to the previous ports) - if any.
1556         // Use hash table for quick lookup.
1557         var removedPorts = {};
1558         _.each(previousPorts, function(port, id) {
1559
1560             if (!ports[id]) removedPorts[id] = true;
1561         });
1562
1563         // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports.
1564         if (this.collection && !_.isEmpty(removedPorts)) {
1565
1566             var inboundLinks = this.collection.getConnectedLinks(this, { inbound: true });
1567             _.each(inboundLinks, function(link) {
1568
1569                 if (removedPorts[link.get('target').port]) link.remove();
1570             });
1571
1572             var outboundLinks = this.collection.getConnectedLinks(this, { outbound: true });
1573             _.each(outboundLinks, function(link) {
1574
1575                 if (removedPorts[link.get('source').port]) link.remove();
1576             });
1577         }
1578
1579         // Update the `ports` object.
1580         this.ports = ports;
1581     },
1582
1583     remove: function(opt) {
1584
1585         opt = opt || {};
1586
1587         var collection = this.collection;
1588
1589         if (collection) {
1590             collection.trigger('batch:start', { batchName: 'remove' });
1591         }
1592
1593         // First, unembed this cell from its parent cell if there is one.
1594         var parentCellId = this.get('parent');
1595         if (parentCellId) {
1596
1597             var parentCell = this.collection && this.collection.get(parentCellId);
1598             parentCell.unembed(this);
1599         }
1600
1601         _.invoke(this.getEmbeddedCells(), 'remove', opt);
1602
1603         this.trigger('remove', this, this.collection, opt);
1604
1605         if (collection) {
1606             collection.trigger('batch:stop', { batchName: 'remove' });
1607         }
1608
1609         return this;
1610     },
1611
1612     toFront: function(opt) {
1613
1614         if (this.collection) {
1615
1616             opt = opt || {};
1617
1618             var z = (this.collection.last().get('z') || 0) + 1;
1619
1620             this.trigger('batch:start', { batchName: 'to-front' }).set('z', z, opt);
1621
1622             if (opt.deep) {
1623
1624                 var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true });
1625                 _.each(cells, function(cell) { cell.set('z', ++z, opt); });
1626
1627             }
1628
1629             this.trigger('batch:stop', { batchName: 'to-front' });
1630         }
1631
1632         return this;
1633     },
1634
1635     toBack: function(opt) {
1636
1637         if (this.collection) {
1638
1639             opt = opt || {};
1640
1641             var z = (this.collection.first().get('z') || 0) - 1;
1642
1643             this.trigger('batch:start', { batchName: 'to-back' });
1644
1645             if (opt.deep) {
1646
1647                 var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true });
1648                 _.eachRight(cells, function(cell) { cell.set('z', z--, opt); });
1649             }
1650
1651             this.set('z', z, opt).trigger('batch:stop', { batchName: 'to-back' });
1652         }
1653
1654         return this;
1655     },
1656
1657     embed: function(cell, opt) {
1658
1659         if (this == cell || this.isEmbeddedIn(cell)) {
1660
1661             throw new Error('Recursive embedding not allowed.');
1662
1663         } else {
1664
1665             this.trigger('batch:start', { batchName: 'embed' });
1666
1667             var embeds = _.clone(this.get('embeds') || []);
1668
1669             // We keep all element ids after links ids.
1670             embeds[cell.isLink() ? 'unshift' : 'push'](cell.id);
1671
1672             cell.set('parent', this.id, opt);
1673             this.set('embeds', _.uniq(embeds), opt);
1674
1675             this.trigger('batch:stop', { batchName: 'embed' });
1676         }
1677
1678         return this;
1679     },
1680
1681     unembed: function(cell, opt) {
1682
1683         this.trigger('batch:start', { batchName: 'unembed' });
1684
1685         cell.unset('parent', opt);
1686         this.set('embeds', _.without(this.get('embeds'), cell.id), opt);
1687
1688         this.trigger('batch:stop', { batchName: 'unembed' });
1689
1690         return this;
1691     },
1692
1693     // Return an array of ancestor cells.
1694     // The array is ordered from the parent of the cell
1695     // to the most distant ancestor.
1696     getAncestors: function() {
1697
1698         var ancestors = [];
1699         var parentId = this.get('parent');
1700
1701         if (this.collection === undefined)
1702             return ancestors;
1703
1704         while (parentId !== undefined) {
1705             var parent = this.collection.get(parentId);
1706             if (parent !== undefined) {
1707                 ancestors.push(parent);
1708                 parentId = parent.get('parent');
1709             } else {
1710                 break;
1711             }
1712         }
1713
1714         return ancestors;
1715     },
1716
1717     getEmbeddedCells: function(opt) {
1718
1719         opt = opt || {};
1720
1721         // Cell models can only be retrieved when this element is part of a collection.
1722         // There is no way this element knows about other cells otherwise.
1723         // This also means that calling e.g. `translate()` on an element with embeds before
1724         // adding it to a graph does not translate its embeds.
1725         if (this.collection) {
1726
1727             var cells;
1728
1729             if (opt.deep) {
1730
1731                 if (opt.breadthFirst) {
1732
1733                     // breadthFirst algorithm
1734                     cells = [];
1735                     var queue = this.getEmbeddedCells();
1736
1737                     while (queue.length > 0) {
1738
1739                         var parent = queue.shift();
1740                         cells.push(parent);
1741                         queue.push.apply(queue, parent.getEmbeddedCells());
1742                     }
1743
1744                 } else {
1745
1746                     // depthFirst algorithm
1747                     cells = this.getEmbeddedCells();
1748                     _.each(cells, function(cell) {
1749                         cells.push.apply(cells, cell.getEmbeddedCells(opt));
1750                     });
1751                 }
1752
1753             } else {
1754
1755                 cells = _.map(this.get('embeds'), this.collection.get, this.collection);
1756             }
1757
1758             return cells;
1759         }
1760         return [];
1761     },
1762
1763     isEmbeddedIn: function(cell, opt) {
1764
1765         var cellId = _.isString(cell) ? cell : cell.id;
1766         var parentId = this.get('parent');
1767
1768         opt = _.defaults({ deep: true }, opt);
1769
1770         // See getEmbeddedCells().
1771         if (this.collection && opt.deep) {
1772
1773             while (parentId) {
1774                 if (parentId == cellId) {
1775                     return true;
1776                 }
1777                 parentId = this.collection.get(parentId).get('parent');
1778             }
1779
1780             return false;
1781
1782         } else {
1783
1784             // When this cell is not part of a collection check
1785             // at least whether it's a direct child of given cell.
1786             return parentId == cellId;
1787         }
1788     },
1789
1790     clone: function(opt) {
1791
1792         opt = opt || {};
1793
1794         var clone = Backbone.Model.prototype.clone.apply(this, arguments);
1795
1796         // We don't want the clone to have the same ID as the original.
1797         clone.set('id', joint.util.uuid(), { silent: true });
1798         clone.set('embeds', '');
1799
1800         if (!opt.deep) return clone;
1801
1802         // The rest of the `clone()` method deals with embeds. If `deep` option is set to `true`,
1803         // the return value is an array of all the embedded clones created.
1804
1805         var embeds = _.sortBy(this.getEmbeddedCells(), function(cell) {
1806             // Sort embeds that links come before elements.
1807             return cell instanceof joint.dia.Element;
1808         });
1809
1810         var clones = [clone];
1811
1812         // This mapping stores cloned links under the `id`s of they originals.
1813         // This prevents cloning a link more then once. Consider a link 'self loop' for example.
1814         var linkCloneMapping = {};
1815
1816         _.each(embeds, function(embed) {
1817
1818             var embedClones = embed.clone({ deep: true });
1819
1820             // Embed the first clone returned from `clone({ deep: true })` above. The first
1821             // cell is always the clone of the cell that called the `clone()` method, i.e. clone of `embed` in this case.
1822             clone.embed(embedClones[0]);
1823
1824             _.each(embedClones, function(embedClone) {
1825
1826                 if (embedClone instanceof joint.dia.Link) {
1827
1828                     if (embedClone.get('source').id == this.id) {
1829
1830                         embedClone.prop('source', { id: clone.id });
1831                     }
1832
1833                     if (embedClone.get('target').id == this.id) {
1834
1835                         embedClone.prop('target', { id: clone.id });
1836                     }
1837
1838                     linkCloneMapping[embed.id] = embedClone;
1839
1840                     // Skip links. Inbound/outbound links are not relevant for them.
1841                     return;
1842                 }
1843
1844                 clones.push(embedClone);
1845
1846                 // Collect all inbound links, clone them (if not done already) and set their target to the `embedClone.id`.
1847                 var inboundLinks = this.collection.getConnectedLinks(embed, { inbound: true });
1848
1849                 _.each(inboundLinks, function(link) {
1850
1851                     var linkClone = linkCloneMapping[link.id] || link.clone();
1852
1853                     // Make sure we don't clone a link more then once.
1854                     linkCloneMapping[link.id] = linkClone;
1855
1856                     linkClone.prop('target', { id: embedClone.id });
1857                 });
1858
1859                 // Collect all inbound links, clone them (if not done already) and set their source to the `embedClone.id`.
1860                 var outboundLinks = this.collection.getConnectedLinks(embed, { outbound: true });
1861
1862                 _.each(outboundLinks, function(link) {
1863
1864                     var linkClone = linkCloneMapping[link.id] || link.clone();
1865
1866                     // Make sure we don't clone a link more then once.
1867                     linkCloneMapping[link.id] = linkClone;
1868
1869                     linkClone.prop('source', { id: embedClone.id });
1870                 });
1871
1872             }, this);
1873
1874         }, this);
1875
1876         // Add link clones to the array of all the new clones.
1877         clones = clones.concat(_.values(linkCloneMapping));
1878
1879         return clones;
1880     },
1881
1882     // A convenient way to set nested properties.
1883     // This method merges the properties you'd like to set with the ones
1884     // stored in the cell and makes sure change events are properly triggered.
1885     // You can either set a nested property with one object
1886     // or use a property path.
1887     // The most simple use case is:
1888     // `cell.prop('name/first', 'John')` or
1889     // `cell.prop({ name: { first: 'John' } })`.
1890     // Nested arrays are supported too:
1891     // `cell.prop('series/0/data/0/degree', 50)` or
1892     // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`.
1893     prop: function(props, value, opt) {
1894
1895         var delim = '/';
1896
1897         if (_.isString(props)) {
1898             // Get/set an attribute by a special path syntax that delimits
1899             // nested objects by the colon character.
1900
1901             if (arguments.length > 1) {
1902
1903                 var path = props;
1904                 var pathArray = path.split('/');
1905                 var property = pathArray[0];
1906
1907                 opt = opt || {};
1908                 opt.propertyPath = path;
1909                 opt.propertyValue = value;
1910
1911                 if (pathArray.length == 1) {
1912                     // Property is not nested. We can simply use `set()`.
1913                     return this.set(property, value, opt);
1914                 }
1915
1916                 var update = {};
1917                 // Initialize the nested object. Subobjects are either arrays or objects.
1918                 // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created.
1919                 // Note that this imposes a limitation on object keys one can use with Inspector.
1920                 // Pure integer keys will cause issues and are therefore not allowed.
1921                 var initializer = update;
1922                 var prevProperty = property;
1923                 _.each(_.rest(pathArray), function(key) {
1924                     initializer = initializer[prevProperty] = (_.isFinite(Number(key)) ? [] : {});
1925                     prevProperty = key;
1926                 });
1927                 // Fill update with the `value` on `path`.
1928                 update = joint.util.setByPath(update, path, value, '/');
1929
1930                 var baseAttributes = _.merge({}, this.attributes);
1931                 // if rewrite mode enabled, we replace value referenced by path with
1932                 // the new one (we don't merge).
1933                 opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/');
1934
1935                 // Merge update with the model attributes.
1936                 var attributes = _.merge(baseAttributes, update);
1937                 // Finally, set the property to the updated attributes.
1938                 return this.set(property, attributes[property], opt);
1939
1940             } else {
1941
1942                 return joint.util.getByPath(this.attributes, props, delim);
1943             }
1944         }
1945
1946         return this.set(_.merge({}, this.attributes, props), value);
1947     },
1948
1949     // A convient way to unset nested properties
1950     removeProp: function(path, opt) {
1951
1952         // Once a property is removed from the `attrs` attribute
1953         // the cellView will recognize a `dirty` flag and rerender itself
1954         // in order to remove the attribute from SVG element.
1955         opt = opt || {};
1956         opt.dirty = true;
1957
1958         var pathArray = path.split('/');
1959
1960         if (pathArray.length === 1) {
1961             // A top level property
1962             return this.unset(path, opt);
1963         }
1964
1965         // A nested property
1966         var property = pathArray[0];
1967         var nestedPath = pathArray.slice(1).join('/');
1968         var propertyValue = _.merge({}, this.get(property));
1969
1970         joint.util.unsetByPath(propertyValue, nestedPath, '/');
1971
1972         return this.set(property, propertyValue, opt);
1973     },
1974
1975     // A convenient way to set nested attributes.
1976     attr: function(attrs, value, opt) {
1977
1978         var args = Array.prototype.slice.call(arguments);
1979
1980         if (_.isString(attrs)) {
1981             // Get/set an attribute by a special path syntax that delimits
1982             // nested objects by the colon character.
1983             args[0] = 'attrs/' + attrs;
1984
1985         } else {
1986
1987             args[0] = { 'attrs' : attrs };
1988         }
1989
1990         return this.prop.apply(this, args);
1991     },
1992
1993     // A convenient way to unset nested attributes
1994     removeAttr: function(path, opt) {
1995
1996         if (_.isArray(path)) {
1997             _.each(path, function(p) { this.removeAttr(p, opt); }, this);
1998             return this;
1999         }
2000
2001         return this.removeProp('attrs/' + path, opt);
2002     },
2003
2004     transition: function(path, value, opt, delim) {
2005
2006         delim = delim || '/';
2007
2008         var defaults = {
2009             duration: 100,
2010             delay: 10,
2011             timingFunction: joint.util.timing.linear,
2012             valueFunction: joint.util.interpolate.number
2013         };
2014
2015         opt = _.extend(defaults, opt);
2016
2017         var firstFrameTime = 0;
2018         var interpolatingFunction;
2019
2020         var setter = _.bind(function(runtime) {
2021
2022             var id, progress, propertyValue, status;
2023
2024             firstFrameTime = firstFrameTime || runtime;
2025             runtime -= firstFrameTime;
2026             progress = runtime / opt.duration;
2027
2028             if (progress < 1) {
2029                 this._transitionIds[path] = id = joint.util.nextFrame(setter);
2030             } else {
2031                 progress = 1;
2032                 delete this._transitionIds[path];
2033             }
2034
2035             propertyValue = interpolatingFunction(opt.timingFunction(progress));
2036
2037             opt.transitionId = id;
2038
2039             this.prop(path, propertyValue, opt);
2040
2041             if (!id) this.trigger('transition:end', this, path);
2042
2043         }, this);
2044
2045         var initiator = _.bind(function(callback) {
2046
2047             this.stopTransitions(path);
2048
2049             interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value);
2050
2051             this._transitionIds[path] = joint.util.nextFrame(callback);
2052
2053             this.trigger('transition:start', this, path);
2054
2055         }, this);
2056
2057         return _.delay(initiator, opt.delay, setter);
2058     },
2059
2060     getTransitions: function() {
2061         return _.keys(this._transitionIds);
2062     },
2063
2064     stopTransitions: function(path, delim) {
2065
2066         delim = delim || '/';
2067
2068         var pathArray = path && path.split(delim);
2069
2070         _(this._transitionIds).keys().filter(pathArray && function(key) {
2071
2072             return _.isEqual(pathArray, key.split(delim).slice(0, pathArray.length));
2073
2074         }).each(function(key) {
2075
2076             joint.util.cancelFrame(this._transitionIds[key]);
2077
2078             delete this._transitionIds[key];
2079
2080             this.trigger('transition:end', this, key);
2081
2082         }, this);
2083
2084         return this;
2085     },
2086
2087     // A shorcut making it easy to create constructs like the following:
2088     // `var el = (new joint.shapes.basic.Rect).addTo(graph)`.
2089     addTo: function(graph, opt) {
2090
2091         graph.addCell(this, opt);
2092         return this;
2093     },
2094
2095     // A shortcut for an equivalent call: `paper.findViewByModel(cell)`
2096     // making it easy to create constructs like the following:
2097     // `cell.findView(paper).highlight()`
2098     findView: function(paper) {
2099
2100         return paper.findViewByModel(this);
2101     },
2102
2103     isLink: function() {
2104
2105         return false;
2106     }
2107 });
2108
2109 // joint.dia.CellView base view and controller.
2110 // --------------------------------------------
2111
2112 // This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`.
2113
2114 joint.dia.CellView = Backbone.View.extend({
2115
2116     tagName: 'g',
2117
2118     attributes: function() {
2119
2120         return { 'model-id': this.model.id };
2121     },
2122
2123     constructor: function(options) {
2124
2125         this._configure(options);
2126         Backbone.View.apply(this, arguments);
2127     },
2128
2129     _configure: function(options) {
2130
2131         if (this.options) options = _.extend({}, _.result(this, 'options'), options);
2132         this.options = options;
2133         // Make sure a global unique id is assigned to this view. Store this id also to the properties object.
2134         // The global unique id makes sure that the same view can be rendered on e.g. different machines and
2135         // still be associated to the same object among all those clients. This is necessary for real-time
2136         // collaboration mechanism.
2137         this.options.id = this.options.id || joint.util.guid(this);
2138     },
2139
2140     initialize: function() {
2141
2142         _.bindAll(this, 'remove', 'update');
2143
2144         // Store reference to this to the <g> DOM element so that the view is accessible through the DOM tree.
2145         this.$el.data('view', this);
2146
2147         this.listenTo(this.model, 'remove', this.remove);
2148         this.listenTo(this.model, 'change:attrs', this.onChangeAttrs);
2149     },
2150
2151     onChangeAttrs: function(cell, attrs, opt) {
2152
2153         if (opt.dirty) {
2154
2155             // dirty flag could be set when a model attribute was removed and it needs to be cleared
2156             // also from the DOM element. See cell.removeAttr().
2157             return this.render();
2158         }
2159
2160         return this.update();
2161     },
2162
2163     // Override the Backbone `_ensureElement()` method in order to create a `<g>` node that wraps
2164     // all the nodes of the Cell view.
2165     _ensureElement: function() {
2166
2167         var el;
2168
2169         if (!this.el) {
2170
2171             var attrs = _.extend({ id: this.id }, _.result(this, 'attributes'));
2172             if (this.className) attrs['class'] = _.result(this, 'className');
2173             el = V(_.result(this, 'tagName'), attrs).node;
2174
2175         } else {
2176
2177             el = _.result(this, 'el');
2178         }
2179
2180         this.setElement(el, false);
2181     },
2182
2183     findBySelector: function(selector) {
2184
2185         // These are either descendants of `this.$el` of `this.$el` itself.
2186         // `.` is a special selector used to select the wrapping `<g>` element.
2187         var $selected = selector === '.' ? this.$el : this.$el.find(selector);
2188         return $selected;
2189     },
2190
2191     notify: function(evt) {
2192
2193         if (this.paper) {
2194
2195             var args = Array.prototype.slice.call(arguments, 1);
2196
2197             // Trigger the event on both the element itself and also on the paper.
2198             this.trigger.apply(this, [evt].concat(args));
2199
2200             // Paper event handlers receive the view object as the first argument.
2201             this.paper.trigger.apply(this.paper, [evt, this].concat(args));
2202         }
2203     },
2204
2205     getStrokeBBox: function(el) {
2206         // Return a bounding box rectangle that takes into account stroke.
2207         // Note that this is a naive and ad-hoc implementation that does not
2208         // works only in certain cases and should be replaced as soon as browsers will
2209         // start supporting the getStrokeBBox() SVG method.
2210         // @TODO any better solution is very welcome!
2211
2212         var isMagnet = !!el;
2213
2214         el = el || this.el;
2215         var bbox = V(el).bbox(false, this.paper.viewport);
2216
2217         var strokeWidth;
2218         if (isMagnet) {
2219
2220             strokeWidth = V(el).attr('stroke-width');
2221
2222         } else {
2223
2224             strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width');
2225         }
2226
2227         strokeWidth = parseFloat(strokeWidth) || 0;
2228
2229         return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth });
2230     },
2231
2232     getBBox: function() {
2233
2234         return V(this.el).bbox();
2235     },
2236
2237     highlight: function(el, opt) {
2238
2239         el = !el ? this.el : this.$(el)[0] || this.el;
2240
2241         // set partial flag if the highlighted element is not the entire view.
2242         opt = opt || {};
2243         opt.partial = el != this.el;
2244
2245         this.notify('cell:highlight', el, opt);
2246         return this;
2247     },
2248
2249     unhighlight: function(el, opt) {
2250
2251         el = !el ? this.el : this.$(el)[0] || this.el;
2252
2253         opt = opt || {};
2254         opt.partial = el != this.el;
2255
2256         this.notify('cell:unhighlight', el, opt);
2257         return this;
2258     },
2259
2260     // Find the closest element that has the `magnet` attribute set to `true`. If there was not such
2261     // an element found, return the root element of the cell view.
2262     findMagnet: function(el) {
2263
2264         var $el = this.$(el);
2265
2266         if ($el.length === 0 || $el[0] === this.el) {
2267
2268             // If the overall cell has set `magnet === false`, then return `undefined` to
2269             // announce there is no magnet found for this cell.
2270             // This is especially useful to set on cells that have 'ports'. In this case,
2271             // only the ports have set `magnet === true` and the overall element has `magnet === false`.
2272             var attrs = this.model.get('attrs') || {};
2273             if (attrs['.'] && attrs['.']['magnet'] === false) {
2274                 return undefined;
2275             }
2276
2277             return this.el;
2278         }
2279
2280         if ($el.attr('magnet')) {
2281
2282             return $el[0];
2283         }
2284
2285         return this.findMagnet($el.parent());
2286     },
2287
2288     // `selector` is a CSS selector or `'.'`. `filter` must be in the special JointJS filter format:
2289     // `{ name: <name of the filter>, args: { <arguments>, ... }`.
2290     // An example is: `{ filter: { name: 'blur', args: { radius: 5 } } }`.
2291     applyFilter: function(selector, filter) {
2292
2293         var $selected = this.findBySelector(selector);
2294
2295         // Generate a hash code from the stringified filter definition. This gives us
2296         // a unique filter ID for different definitions.
2297         var filterId = filter.name + this.paper.svg.id + joint.util.hashCode(JSON.stringify(filter));
2298
2299         // If the filter already exists in the document,
2300         // we're done and we can just use it (reference it using `url()`).
2301         // If not, create one.
2302         if (!this.paper.svg.getElementById(filterId)) {
2303
2304             var filterSVGString = joint.util.filter[filter.name] && joint.util.filter[filter.name](filter.args || {});
2305             if (!filterSVGString) {
2306                 throw new Error('Non-existing filter ' + filter.name);
2307             }
2308             var filterElement = V(filterSVGString);
2309             // Set the filter area to be 3x the bounding box of the cell
2310             // and center the filter around the cell.
2311             filterElement.attr({
2312                 filterUnits: 'objectBoundingBox',
2313                 x: -1, y: -1, width: 3, height: 3
2314             });
2315             if (filter.attrs) filterElement.attr(filter.attrs);
2316             filterElement.node.id = filterId;
2317             V(this.paper.svg).defs().append(filterElement);
2318         }
2319
2320         $selected.each(function() {
2321
2322             V(this).attr('filter', 'url(#' + filterId + ')');
2323         });
2324     },
2325
2326     // `selector` is a CSS selector or `'.'`. `attr` is either a `'fill'` or `'stroke'`.
2327     // `gradient` must be in the special JointJS gradient format:
2328     // `{ type: <linearGradient|radialGradient>, stops: [ { offset: <offset>, color: <color> }, ... ]`.
2329     // An example is: `{ fill: { type: 'linearGradient', stops: [ { offset: '10%', color: 'green' }, { offset: '50%', color: 'blue' } ] } }`.
2330     applyGradient: function(selector, attr, gradient) {
2331
2332         var $selected = this.findBySelector(selector);
2333
2334         // Generate a hash code from the stringified filter definition. This gives us
2335         // a unique filter ID for different definitions.
2336         var gradientId = gradient.type + this.paper.svg.id + joint.util.hashCode(JSON.stringify(gradient));
2337
2338         // If the gradient already exists in the document,
2339         // we're done and we can just use it (reference it using `url()`).
2340         // If not, create one.
2341         if (!this.paper.svg.getElementById(gradientId)) {
2342
2343             var gradientSVGString = [
2344                 '<' + gradient.type + '>',
2345                 _.map(gradient.stops, function(stop) {
2346                     return '<stop offset="' + stop.offset + '" stop-color="' + stop.color + '" stop-opacity="' + (_.isFinite(stop.opacity) ? stop.opacity : 1) + '" />';
2347                 }).join(''),
2348                 '</' + gradient.type + '>'
2349             ].join('');
2350
2351             var gradientElement = V(gradientSVGString);
2352             if (gradient.attrs) { gradientElement.attr(gradient.attrs); }
2353             gradientElement.node.id = gradientId;
2354             V(this.paper.svg).defs().append(gradientElement);
2355         }
2356
2357         $selected.each(function() {
2358
2359             V(this).attr(attr, 'url(#' + gradientId + ')');
2360         });
2361     },
2362
2363     // Construct a unique selector for the `el` element within this view.
2364     // `prevSelector` is being collected through the recursive call.
2365     // No value for `prevSelector` is expected when using this method.
2366     getSelector: function(el, prevSelector) {
2367
2368         if (el === this.el) {
2369             return prevSelector;
2370         }
2371
2372         var nthChild = V(el).index() + 1;
2373         var selector = el.tagName + ':nth-child(' + nthChild + ')';
2374
2375         if (prevSelector) {
2376             selector += ' > ' + prevSelector;
2377         }
2378
2379         return this.getSelector(el.parentNode, selector);
2380     },
2381
2382     // Interaction. The controller part.
2383     // ---------------------------------
2384
2385     // Interaction is handled by the paper and delegated to the view in interest.
2386     // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid.
2387     // If necessary, real coordinates can be obtained from the `evt` event object.
2388
2389     // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`,
2390     // i.e. `joint.dia.Element` and `joint.dia.Link`.
2391
2392     pointerdblclick: function(evt, x, y) {
2393
2394         this.notify('cell:pointerdblclick', evt, x, y);
2395     },
2396
2397     pointerclick: function(evt, x, y) {
2398
2399         this.notify('cell:pointerclick', evt, x, y);
2400     },
2401
2402     pointerdown: function(evt, x, y) {
2403
2404         if (this.model.collection) {
2405             this.model.trigger('batch:start', { batchName: 'pointer' });
2406             this._collection = this.model.collection;
2407         }
2408
2409         this.notify('cell:pointerdown', evt, x, y);
2410     },
2411
2412     pointermove: function(evt, x, y) {
2413
2414         this.notify('cell:pointermove', evt, x, y);
2415     },
2416
2417     pointerup: function(evt, x, y) {
2418
2419         this.notify('cell:pointerup', evt, x, y);
2420
2421         if (this._collection) {
2422             // we don't want to trigger event on model as model doesn't
2423             // need to be member of collection anymore (remove)
2424             this._collection.trigger('batch:stop', { batchName: 'pointer' });
2425             delete this._collection;
2426         }
2427     },
2428
2429     mouseover: function(evt) {
2430
2431         this.notify('cell:mouseover', evt);
2432     },
2433
2434     mouseout: function(evt) {
2435
2436         this.notify('cell:mouseout', evt);
2437     }
2438 });
2439
2440 //      JointJS library.
2441 //      (c) 2011-2013 client IO
2442
2443 // joint.dia.Element base model.
2444 // -----------------------------
2445
2446 joint.dia.Element = joint.dia.Cell.extend({
2447
2448     defaults: {
2449         position: { x: 0, y: 0 },
2450         size: { width: 1, height: 1 },
2451         angle: 0
2452     },
2453
2454     position: function(x, y, opt) {
2455
2456         var isSetter = _.isNumber(y);
2457
2458         opt = (isSetter ? opt : x) || {};
2459
2460         // option `parentRelative` for setting the position relative to the element's parent.
2461         if (opt.parentRelative) {
2462
2463             // Getting the parent's position requires the collection.
2464             // Cell.get('parent') helds cell id only.
2465             if (!this.collection) throw new Error('Element must be part of a collection.');
2466
2467             var parent = this.collection.get(this.get('parent'));
2468             var parentPosition = parent && !parent.isLink()
2469                 ? parent.get('position')
2470                 : { x: 0, y: 0 };
2471         }
2472
2473         if (isSetter) {
2474
2475             if (opt.parentRelative) {
2476                 x += parentPosition.x;
2477                 y += parentPosition.y;
2478             }
2479
2480             return this.set('position', { x: x, y: y }, opt);
2481
2482         } else { // Getter returns a geometry point.
2483
2484             var elementPosition = g.point(this.get('position'));
2485
2486             return opt.parentRelative
2487                 ? elementPosition.difference(parentPosition)
2488                 : elementPosition;
2489         }
2490     },
2491
2492     translate: function(tx, ty, opt) {
2493
2494         ty = ty || 0;
2495
2496         if (tx === 0 && ty === 0) {
2497             // Like nothing has happened.
2498             return this;
2499         }
2500
2501         opt = opt || {};
2502         // Pass the initiator of the translation.
2503         opt.translateBy = opt.translateBy || this.id;
2504         // To find out by how much an element was translated in event 'change:position' handlers.
2505         opt.tx = tx;
2506         opt.ty = ty;
2507
2508         var position = this.get('position') || { x: 0, y: 0 };
2509         var translatedPosition = { x: position.x + tx || 0, y: position.y + ty || 0 };
2510
2511         if (opt.transition) {
2512
2513             if (!_.isObject(opt.transition)) opt.transition = {};
2514
2515             this.transition('position', translatedPosition, _.extend({}, opt.transition, {
2516                 valueFunction: joint.util.interpolate.object
2517             }));
2518
2519         } else {
2520
2521             this.set('position', translatedPosition, opt);
2522
2523             // Recursively call `translate()` on all the embeds cells.
2524             _.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt);
2525         }
2526
2527         return this;
2528     },
2529
2530     resize: function(width, height, opt) {
2531
2532         this.trigger('batch:start', { batchName: 'resize' });
2533         this.set('size', { width: width, height: height }, opt);
2534         this.trigger('batch:stop', { batchName: 'resize' });
2535
2536         return this;
2537     },
2538
2539     fitEmbeds: function(opt) {
2540
2541         opt = opt || 0;
2542
2543         var collection = this.collection;
2544
2545         // Getting the children's size and position requires the collection.
2546         // Cell.get('embdes') helds an array of cell ids only.
2547         if (!collection) throw new Error('Element must be part of a collection.');
2548
2549         var embeddedCells = this.getEmbeddedCells();
2550
2551         if (embeddedCells.length > 0) {
2552
2553             this.trigger('batch:start', { batchName: 'fit-embeds' });
2554
2555             if (opt.deep) {
2556                 // Recursively apply fitEmbeds on all embeds first.
2557                 _.invoke(embeddedCells, 'fitEmbeds', opt);
2558             }
2559
2560             // Compute cell's size and position  based on the children bbox
2561             // and given padding.
2562             var bbox = collection.getBBox(embeddedCells);
2563             var padding = opt.padding || 0;
2564
2565             if (_.isNumber(padding)) {
2566                 padding = {
2567                     left: padding,
2568                     right: padding,
2569                     top: padding,
2570                     bottom: padding
2571                 };
2572             } else {
2573                 padding = {
2574                     left: padding.left || 0,
2575                     right: padding.right || 0,
2576                     top: padding.top || 0,
2577                     bottom: padding.bottom || 0
2578                 };
2579             }
2580
2581             // Apply padding computed above to the bbox.
2582             bbox.moveAndExpand({
2583                 x: - padding.left,
2584                 y: - padding.top,
2585                 width: padding.right + padding.left,
2586                 height: padding.bottom + padding.top
2587             });
2588
2589             // Set new element dimensions finally.
2590             this.set({
2591                 position: { x: bbox.x, y: bbox.y },
2592                 size: { width: bbox.width, height: bbox.height }
2593             }, opt);
2594
2595             this.trigger('batch:stop', { batchName: 'fit-embeds' });
2596         }
2597
2598         return this;
2599     },
2600
2601     // Rotate element by `angle` degrees, optionally around `origin` point.
2602     // If `origin` is not provided, it is considered to be the center of the element.
2603     // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not
2604     // the difference from the previous angle.
2605     rotate: function(angle, absolute, origin) {
2606
2607         if (origin) {
2608
2609             var center = this.getBBox().center();
2610             var size = this.get('size');
2611             var position = this.get('position');
2612             center.rotate(origin, this.get('angle') - angle);
2613             var dx = center.x - size.width / 2 - position.x;
2614             var dy = center.y - size.height / 2 - position.y;
2615             this.trigger('batch:start', { batchName: 'rotate' });
2616             this.translate(dx, dy);
2617             this.rotate(angle, absolute);
2618             this.trigger('batch:stop', { batchName: 'rotate' });
2619
2620         } else {
2621
2622             this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360);
2623         }
2624
2625         return this;
2626     },
2627
2628     getBBox: function() {
2629
2630         var position = this.get('position');
2631         var size = this.get('size');
2632
2633         return g.rect(position.x, position.y, size.width, size.height);
2634     }
2635 });
2636
2637 // joint.dia.Element base view and controller.
2638 // -------------------------------------------
2639
2640 joint.dia.ElementView = joint.dia.CellView.extend({
2641
2642     className: function() {
2643         return 'element ' + this.model.get('type').split('.').join(' ');
2644     },
2645
2646     initialize: function() {
2647
2648         _.bindAll(this, 'translate', 'resize', 'rotate');
2649
2650         joint.dia.CellView.prototype.initialize.apply(this, arguments);
2651
2652         this.listenTo(this.model, 'change:position', this.translate);
2653         this.listenTo(this.model, 'change:size', this.resize);
2654         this.listenTo(this.model, 'change:angle', this.rotate);
2655     },
2656
2657     // Default is to process the `attrs` object and set attributes on subelements based on the selectors.
2658     update: function(cell, renderingOnlyAttrs) {
2659
2660         var allAttrs = this.model.get('attrs');
2661
2662         var rotatable = V(this.$('.rotatable')[0]);
2663         if (rotatable) {
2664
2665             var rotation = rotatable.attr('transform');
2666             rotatable.attr('transform', '');
2667         }
2668
2669         var relativelyPositioned = [];
2670
2671         _.each(renderingOnlyAttrs || allAttrs, function(attrs, selector) {
2672
2673             // Elements that should be updated.
2674             var $selected = this.findBySelector(selector);
2675
2676             // No element matched by the `selector` was found. We're done then.
2677             if ($selected.length === 0) return;
2678
2679             // Special attributes are treated by JointJS, not by SVG.
2680             var specialAttributes = ['style', 'text', 'html', 'ref-x', 'ref-y', 'ref-dx', 'ref-dy', 'ref-width', 'ref-height', 'ref', 'x-alignment', 'y-alignment', 'port'];
2681
2682             // If the `filter` attribute is an object, it is in the special JointJS filter format and so
2683             // it becomes a special attribute and is treated separately.
2684             if (_.isObject(attrs.filter)) {
2685
2686                 specialAttributes.push('filter');
2687                 this.applyFilter(selector, attrs.filter);
2688             }
2689
2690             // If the `fill` or `stroke` attribute is an object, it is in the special JointJS gradient format and so
2691             // it becomes a special attribute and is treated separately.
2692             if (_.isObject(attrs.fill)) {
2693
2694                 specialAttributes.push('fill');
2695                 this.applyGradient(selector, 'fill', attrs.fill);
2696             }
2697             if (_.isObject(attrs.stroke)) {
2698
2699                 specialAttributes.push('stroke');
2700                 this.applyGradient(selector, 'stroke', attrs.stroke);
2701             }
2702
2703             // Make special case for `text` attribute. So that we can set text content of the `<text>` element
2704             // via the `attrs` object as well.
2705             // Note that it's important to set text before applying the rest of the final attributes.
2706             // Vectorizer `text()` method sets on the element its own attributes and it has to be possible
2707             // to rewrite them, if needed. (i.e display: 'none')
2708             if (!_.isUndefined(attrs.text)) {
2709
2710                 $selected.each(function() {
2711
2712                     V(this).text(attrs.text + '', { lineHeight: attrs.lineHeight, textPath: attrs.textPath });
2713                 });
2714                 specialAttributes.push('lineHeight', 'textPath');
2715             }
2716
2717             // Set regular attributes on the `$selected` subelement. Note that we cannot use the jQuery attr()
2718             // method as some of the attributes might be namespaced (e.g. xlink:href) which fails with jQuery attr().
2719             var finalAttributes = _.omit(attrs, specialAttributes);
2720
2721             $selected.each(function() {
2722
2723                 V(this).attr(finalAttributes);
2724             });
2725
2726             // `port` attribute contains the `id` of the port that the underlying magnet represents.
2727             if (attrs.port) {
2728
2729                 $selected.attr('port', _.isUndefined(attrs.port.id) ? attrs.port : attrs.port.id);
2730             }
2731
2732             // `style` attribute is special in the sense that it sets the CSS style of the subelement.
2733             if (attrs.style) {
2734
2735                 $selected.css(attrs.style);
2736             }
2737
2738             if (!_.isUndefined(attrs.html)) {
2739
2740                 $selected.each(function() {
2741
2742                     $(this).html(attrs.html + '');
2743                 });
2744             }
2745
2746             // Special `ref-x` and `ref-y` attributes make it possible to set both absolute or
2747             // relative positioning of subelements.
2748             if (!_.isUndefined(attrs['ref-x']) ||
2749                 !_.isUndefined(attrs['ref-y']) ||
2750                 !_.isUndefined(attrs['ref-dx']) ||
2751                 !_.isUndefined(attrs['ref-dy']) ||
2752                 !_.isUndefined(attrs['x-alignment']) ||
2753                 !_.isUndefined(attrs['y-alignment']) ||
2754                 !_.isUndefined(attrs['ref-width']) ||
2755                 !_.isUndefined(attrs['ref-height'])
2756                ) {
2757
2758                 _.each($selected, function(el, index, list) {
2759                     var $el = $(el);
2760                     // copy original list selector to the element
2761                     $el.selector = list.selector;
2762                     relativelyPositioned.push($el);
2763                 });
2764             }
2765
2766         }, this);
2767
2768         // We don't want the sub elements to affect the bounding box of the root element when
2769         // positioning the sub elements relatively to the bounding box.
2770         //_.invoke(relativelyPositioned, 'hide');
2771         //_.invoke(relativelyPositioned, 'show');
2772
2773         // Note that we're using the bounding box without transformation because we are already inside
2774         // a transformed coordinate system.
2775         var bbox = this.el.getBBox();
2776
2777         renderingOnlyAttrs = renderingOnlyAttrs || {};
2778
2779         _.each(relativelyPositioned, function($el) {
2780
2781             // if there was a special attribute affecting the position amongst renderingOnlyAttributes
2782             // we have to merge it with rest of the element's attributes as they are necessary
2783             // to update the position relatively (i.e `ref`)
2784             var renderingOnlyElAttrs = renderingOnlyAttrs[$el.selector];
2785             var elAttrs = renderingOnlyElAttrs
2786                 ? _.merge({}, allAttrs[$el.selector], renderingOnlyElAttrs)
2787             : allAttrs[$el.selector];
2788
2789             this.positionRelative($el, bbox, elAttrs);
2790
2791         }, this);
2792
2793         if (rotatable) {
2794
2795             rotatable.attr('transform', rotation || '');
2796         }
2797     },
2798
2799     positionRelative: function($el, bbox, elAttrs) {
2800
2801         var ref = elAttrs['ref'];
2802         var refX = parseFloat(elAttrs['ref-x']);
2803         var refY = parseFloat(elAttrs['ref-y']);
2804         var refDx = parseFloat(elAttrs['ref-dx']);
2805         var refDy = parseFloat(elAttrs['ref-dy']);
2806         var yAlignment = elAttrs['y-alignment'];
2807         var xAlignment = elAttrs['x-alignment'];
2808         var refWidth = parseFloat(elAttrs['ref-width']);
2809         var refHeight = parseFloat(elAttrs['ref-height']);
2810
2811         // `ref` is the selector of the reference element. If no `ref` is passed, reference
2812         // element is the root element.
2813
2814         var isScalable = _.contains(_.pluck(_.pluck($el.parents('g'), 'className'), 'baseVal'), 'scalable');
2815
2816         if (ref) {
2817
2818             // Get the bounding box of the reference element relative to the root `<g>` element.
2819             bbox = V(this.findBySelector(ref)[0]).bbox(false, this.el);
2820         }
2821
2822         var vel = V($el[0]);
2823
2824         // Remove the previous translate() from the transform attribute and translate the element
2825         // relative to the root bounding box following the `ref-x` and `ref-y` attributes.
2826         if (vel.attr('transform')) {
2827
2828             vel.attr('transform', vel.attr('transform').replace(/translate\([^)]*\)/g, '').trim() || '');
2829         }
2830
2831         function isDefined(x) {
2832             return _.isNumber(x) && !_.isNaN(x);
2833         }
2834
2835         // The final translation of the subelement.
2836         var tx = 0;
2837         var ty = 0;
2838
2839         // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to
2840         // the reference element size
2841         // val in 0..1         ref-width = 0.75 sets the width to 75% of the ref. el. width
2842         // val < 0 || val > 1  ref-height = -20 sets the height to the the ref. el. height shorter by 20
2843
2844         if (isDefined(refWidth)) {
2845
2846             if (refWidth >= 0 && refWidth <= 1) {
2847
2848                 vel.attr('width', refWidth * bbox.width);
2849
2850             } else {
2851
2852                 vel.attr('width', Math.max(refWidth + bbox.width, 0));
2853             }
2854         }
2855
2856         if (isDefined(refHeight)) {
2857
2858             if (refHeight >= 0 && refHeight <= 1) {
2859
2860                 vel.attr('height', refHeight * bbox.height);
2861
2862             } else {
2863
2864                 vel.attr('height', Math.max(refHeight + bbox.height, 0));
2865             }
2866         }
2867
2868         // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom
2869         // coordinate of the reference element.
2870         if (isDefined(refDx)) {
2871
2872             if (isScalable) {
2873
2874                 // Compensate for the scale grid in case the elemnt is in the scalable group.
2875                 var scale = V(this.$('.scalable')[0]).scale();
2876                 tx = bbox.x + bbox.width + refDx / scale.sx;
2877
2878             } else {
2879
2880                 tx = bbox.x + bbox.width + refDx;
2881             }
2882         }
2883         if (isDefined(refDy)) {
2884
2885             if (isScalable) {
2886
2887                 // Compensate for the scale grid in case the elemnt is in the scalable group.
2888                 var scale = V(this.$('.scalable')[0]).scale();
2889                 ty = bbox.y + bbox.height + refDy / scale.sy;
2890             } else {
2891
2892                 ty = bbox.y + bbox.height + refDy;
2893             }
2894         }
2895
2896         // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width
2897         // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box
2898         // otherwise, `refX` is the left coordinate of the bounding box
2899         // Analogical rules apply for `refY`.
2900         if (isDefined(refX)) {
2901
2902             if (refX > 0 && refX < 1) {
2903
2904                 tx = bbox.x + bbox.width * refX;
2905
2906             } else if (isScalable) {
2907
2908                 // Compensate for the scale grid in case the elemnt is in the scalable group.
2909                 var scale = V(this.$('.scalable')[0]).scale();
2910                 tx = bbox.x + refX / scale.sx;
2911
2912             } else {
2913
2914                 tx = bbox.x + refX;
2915             }
2916         }
2917         if (isDefined(refY)) {
2918
2919             if (refY > 0 && refY < 1) {
2920
2921                 ty = bbox.y + bbox.height * refY;
2922
2923             } else if (isScalable) {
2924
2925                 // Compensate for the scale grid in case the elemnt is in the scalable group.
2926                 var scale = V(this.$('.scalable')[0]).scale();
2927                 ty = bbox.y + refY / scale.sy;
2928
2929             } else {
2930
2931                 ty = bbox.y + refY;
2932             }
2933         }
2934
2935         var velbbox = vel.bbox(false, this.paper.viewport);
2936         // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate.
2937         if (yAlignment === 'middle') {
2938
2939             ty -= velbbox.height / 2;
2940
2941         } else if (isDefined(yAlignment)) {
2942
2943             ty += (yAlignment > -1 && yAlignment < 1) ?  velbbox.height * yAlignment : yAlignment;
2944         }
2945
2946         // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate.
2947         if (xAlignment === 'middle') {
2948
2949             tx -= velbbox.width / 2;
2950
2951         } else if (isDefined(xAlignment)) {
2952
2953             tx += (xAlignment > -1 && xAlignment < 1) ?  velbbox.width * xAlignment : xAlignment;
2954         }
2955
2956         vel.translate(tx, ty);
2957     },
2958
2959     // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the
2960     // default markup is not desirable.
2961     renderMarkup: function() {
2962
2963         var markup = this.model.markup || this.model.get('markup');
2964
2965         if (markup) {
2966
2967             var nodes = V(markup);
2968             V(this.el).append(nodes);
2969
2970         } else {
2971
2972             throw new Error('properties.markup is missing while the default render() implementation is used.');
2973         }
2974     },
2975
2976     render: function() {
2977
2978         this.$el.empty();
2979
2980         this.renderMarkup();
2981
2982         this.update();
2983
2984         this.resize();
2985         this.rotate();
2986         this.translate();
2987
2988         return this;
2989     },
2990
2991     // Scale the whole `<g>` group. Note the difference between `scale()` and `resize()` here.
2992     // `resize()` doesn't scale the whole `<g>` group but rather adjusts the `box.sx`/`box.sy` only.
2993     // `update()` is then responsible for scaling only those elements that have the `follow-scale`
2994     // attribute set to `true`. This is desirable in elements that have e.g. a `<text>` subelement
2995     // that is not supposed to be scaled together with a surrounding `<rect>` element that IS supposed
2996     // be be scaled.
2997     scale: function(sx, sy) {
2998
2999         // TODO: take into account the origin coordinates `ox` and `oy`.
3000         V(this.el).scale(sx, sy);
3001     },
3002
3003     resize: function() {
3004
3005         var size = this.model.get('size') || { width: 1, height: 1 };
3006         var angle = this.model.get('angle') || 0;
3007
3008         var scalable = V(this.$('.scalable')[0]);
3009         if (!scalable) {
3010             // If there is no scalable elements, than there is nothing to resize.
3011             return;
3012         }
3013         var scalableBbox = scalable.bbox(true);
3014         // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making
3015         // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`.
3016         scalable.attr('transform', 'scale(' + (size.width / (scalableBbox.width || 1)) + ',' + (size.height / (scalableBbox.height || 1)) + ')');
3017
3018         // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height`
3019         // Order of transformations is significant but we want to reconstruct the object always in the order:
3020         // resize(), rotate(), translate() no matter of how the object was transformed. For that to work,
3021         // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the
3022         // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation
3023         // around the center of the resized object (which is a different origin then the origin of the previous rotation)
3024         // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was.
3025
3026         // Cancel the rotation but now around a different origin, which is the center of the scaled object.
3027         var rotatable = V(this.$('.rotatable')[0]);
3028         var rotation = rotatable && rotatable.attr('transform');
3029         if (rotation && rotation !== 'null') {
3030
3031             rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')');
3032             var rotatableBbox = scalable.bbox(false, this.paper.viewport);
3033
3034             // Store new x, y and perform rotate() again against the new rotation origin.
3035             this.model.set('position', { x: rotatableBbox.x, y: rotatableBbox.y });
3036             this.rotate();
3037         }
3038
3039         // Update must always be called on non-rotated element. Otherwise, relative positioning
3040         // would work with wrong (rotated) bounding boxes.
3041         this.update();
3042     },
3043
3044     translate: function(model, changes, opt) {
3045
3046         var position = this.model.get('position') || { x: 0, y: 0 };
3047
3048         V(this.el).attr('transform', 'translate(' + position.x + ',' + position.y + ')');
3049     },
3050
3051     rotate: function() {
3052
3053         var rotatable = V(this.$('.rotatable')[0]);
3054         if (!rotatable) {
3055             // If there is no rotatable elements, then there is nothing to rotate.
3056             return;
3057         }
3058
3059         var angle = this.model.get('angle') || 0;
3060         var size = this.model.get('size') || { width: 1, height: 1 };
3061
3062         var ox = size.width / 2;
3063         var oy = size.height / 2;
3064
3065
3066         rotatable.attr('transform', 'rotate(' + angle + ',' + ox + ',' + oy + ')');
3067     },
3068
3069     getBBox: function(opt) {
3070
3071         if (opt && opt.useModelGeometry) {
3072             var noTransformationBBox = this.model.getBBox().bbox(this.model.get('angle'));
3073             var transformationMatrix = this.paper.viewport.getCTM();
3074             return V.transformRect(noTransformationBBox, transformationMatrix);
3075         }
3076
3077         return joint.dia.CellView.prototype.getBBox.apply(this, arguments);
3078     },
3079
3080     // Embedding mode methods
3081     // ----------------------
3082
3083     findParentsByKey: function(key) {
3084
3085         var bbox = this.model.getBBox();
3086
3087         return key == 'bbox'
3088             ? this.paper.model.findModelsInArea(bbox)
3089             : this.paper.model.findModelsFromPoint(bbox[key]());
3090     },
3091
3092     prepareEmbedding: function() {
3093
3094         // Bring the model to the front with all his embeds.
3095         this.model.toFront({ deep: true, ui: true });
3096
3097         // Move to front also all the inbound and outbound links that are connected
3098         // to any of the element descendant. If we bring to front only embedded elements,
3099         // links connected to them would stay in the background.
3100         _.invoke(this.paper.model.getConnectedLinks(this.model, { deep: true }), 'toFront', { ui: true });
3101
3102         // Before we start looking for suitable parent we remove the current one.
3103         var parentId = this.model.get('parent');
3104         parentId && this.paper.model.getCell(parentId).unembed(this.model, { ui: true });
3105     },
3106
3107     processEmbedding: function(opt) {
3108
3109         opt = opt || this.paper.options;
3110
3111         var candidates = this.findParentsByKey(opt.findParentBy);
3112
3113         // don't account element itself or any of its descendents
3114         candidates = _.reject(candidates, function(el) {
3115             return this.model.id == el.id || el.isEmbeddedIn(this.model);
3116         }, this);
3117
3118         if (opt.frontParentOnly) {
3119             // pick the element with the highest `z` index
3120             candidates = candidates.slice(-1);
3121         }
3122
3123         var newCandidateView = null;
3124         var prevCandidateView = this._candidateEmbedView;
3125
3126         // iterate over all candidates starting from the last one (has the highest z-index).
3127         for (var i = candidates.length - 1; i >= 0; i--) {
3128
3129             var candidate = candidates[i];
3130
3131             if (prevCandidateView && prevCandidateView.model.id == candidate.id) {
3132
3133                 // candidate remains the same
3134                 newCandidateView = prevCandidateView;
3135                 break;
3136
3137             } else {
3138
3139                 var view = candidate.findView(this.paper);
3140                 if (opt.validateEmbedding.call(this.paper, this, view)) {
3141
3142                     // flip to the new candidate
3143                     newCandidateView = view;
3144                     break;
3145                 }
3146             }
3147         }
3148
3149         if (newCandidateView && newCandidateView != prevCandidateView) {
3150             // A new candidate view found. Highlight the new one.
3151             prevCandidateView && prevCandidateView.unhighlight(null, { embedding: true });
3152             this._candidateEmbedView = newCandidateView.highlight(null, { embedding: true });
3153         }
3154
3155         if (!newCandidateView && prevCandidateView) {
3156             // No candidate view found. Unhighlight the previous candidate.
3157             prevCandidateView.unhighlight(null, { embedding: true });
3158             delete this._candidateEmbedView;
3159         }
3160     },
3161
3162     finalizeEmbedding: function() {
3163
3164         var candidateView = this._candidateEmbedView;
3165
3166         if (candidateView) {
3167
3168             // We finished embedding. Candidate view is chosen to become the parent of the model.
3169             candidateView.model.embed(this.model, { ui: true });
3170             candidateView.unhighlight(null, { embedding: true });
3171
3172             delete this._candidateEmbedView;
3173         }
3174
3175         _.invoke(this.paper.model.getConnectedLinks(this.model, { deep: true }), 'reparent', { ui: true });
3176     },
3177
3178     // Interaction. The controller part.
3179     // ---------------------------------
3180
3181     pointerdown: function(evt, x, y) {
3182
3183         // target is a valid magnet start linking
3184         if (evt.target.getAttribute('magnet') && this.paper.options.validateMagnet.call(this.paper, this, evt.target)) {
3185
3186             this.model.trigger('batch:start', { batchName: 'add-link' });
3187
3188             var link = this.paper.getDefaultLink(this, evt.target);
3189             link.set({
3190                 source: {
3191                     id: this.model.id,
3192                     selector: this.getSelector(evt.target),
3193                     port: $(evt.target).attr('port')
3194                 },
3195                 target: { x: x, y: y }
3196             });
3197
3198             this.paper.model.addCell(link);
3199
3200             this._linkView = this.paper.findViewByModel(link);
3201             this._linkView.pointerdown(evt, x, y);
3202             this._linkView.startArrowheadMove('target');
3203
3204         } else {
3205             this._dx = x;
3206             this._dy = y;
3207
3208             joint.dia.CellView.prototype.pointerdown.apply(this, arguments);
3209             this.notify('element:pointerdown', evt, x, y);
3210         }
3211     },
3212
3213     pointermove: function(evt, x, y) {
3214
3215         if (this._linkView) {
3216
3217             // let the linkview deal with this event
3218             this._linkView.pointermove(evt, x, y);
3219
3220         } else {
3221
3222             var grid = this.paper.options.gridSize;
3223             var interactive = _.isFunction(this.options.interactive)
3224                 ? this.options.interactive(this, 'pointermove')
3225                 : this.options.interactive;
3226
3227             if (interactive !== false) {
3228
3229                 var position = this.model.get('position');
3230
3231                 // Make sure the new element's position always snaps to the current grid after
3232                 // translate as the previous one could be calculated with a different grid size.
3233                 this.model.translate(
3234                     g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - this._dx, grid),
3235                     g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - this._dy, grid)
3236                 );
3237
3238                 if (this.paper.options.embeddingMode) {
3239
3240                     if (!this._inProcessOfEmbedding) {
3241                         // Prepare the element for embedding only if the pointer moves.
3242                         // We don't want to do unnecessary action with the element
3243                         // if an user only clicks/dblclicks on it.
3244                         this.prepareEmbedding();
3245                         this._inProcessOfEmbedding = true;
3246                     }
3247
3248                     this.processEmbedding();
3249                 }
3250             }
3251
3252             this._dx = g.snapToGrid(x, grid);
3253             this._dy = g.snapToGrid(y, grid);
3254
3255
3256             joint.dia.CellView.prototype.pointermove.apply(this, arguments);
3257             this.notify('element:pointermove', evt, x, y);
3258         }
3259     },
3260
3261     pointerup: function(evt, x, y) {
3262
3263         if (this._linkView) {
3264
3265             // let the linkview deal with this event
3266             this._linkView.pointerup(evt, x, y);
3267             delete this._linkView;
3268
3269             this.model.trigger('batch:stop', { batchName: 'add-link' });
3270
3271         } else {
3272
3273             if (this._inProcessOfEmbedding) {
3274                 this.finalizeEmbedding();
3275                 this._inProcessOfEmbedding = false;
3276             }
3277
3278             this.notify('element:pointerup', evt, x, y);
3279             joint.dia.CellView.prototype.pointerup.apply(this, arguments);
3280
3281         }
3282     }
3283
3284 });
3285
3286 //      JointJS diagramming library.
3287 //      (c) 2011-2013 client IO
3288
3289 // joint.dia.Link base model.
3290 // --------------------------
3291 joint.dia.Link = joint.dia.Cell.extend({
3292
3293     // The default markup for links.
3294     markup: [
3295         '<path class="connection" stroke="black"/>',
3296         '<path class="marker-source" fill="black" stroke="black" />',
3297         '<path class="marker-target" fill="black" stroke="black" />',
3298         '<path class="connection-wrap"/>',
3299         '<g class="labels"/>',
3300         '<g class="marker-vertices"/>',
3301         '<g class="marker-arrowheads"/>',
3302         '<g class="link-tools"/>'
3303     ].join(''),
3304
3305     labelMarkup: [
3306         '<g class="label">',
3307         '<rect />',
3308         '<text />',
3309         '</g>'
3310     ].join(''),
3311
3312     toolMarkup: [
3313         '<g class="link-tool">',
3314         '<g class="tool-remove" event="remove">',
3315         '<circle r="11" />',
3316         '<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z"/>',
3317         '<title>Remove link.</title>',
3318         '</g>',
3319         '<g class="tool-options" event="link:options">',
3320         '<circle r="11" transform="translate(25)"/>',
3321         '<path fill="white" transform="scale(.55) translate(29, -16)" d="M31.229,17.736c0.064-0.571,0.104-1.148,0.104-1.736s-0.04-1.166-0.104-1.737l-4.377-1.557c-0.218-0.716-0.504-1.401-0.851-2.05l1.993-4.192c-0.725-0.91-1.549-1.734-2.458-2.459l-4.193,1.994c-0.647-0.347-1.334-0.632-2.049-0.849l-1.558-4.378C17.165,0.708,16.588,0.667,16,0.667s-1.166,0.041-1.737,0.105L12.707,5.15c-0.716,0.217-1.401,0.502-2.05,0.849L6.464,4.005C5.554,4.73,4.73,5.554,4.005,6.464l1.994,4.192c-0.347,0.648-0.632,1.334-0.849,2.05l-4.378,1.557C0.708,14.834,0.667,15.412,0.667,16s0.041,1.165,0.105,1.736l4.378,1.558c0.217,0.715,0.502,1.401,0.849,2.049l-1.994,4.193c0.725,0.909,1.549,1.733,2.459,2.458l4.192-1.993c0.648,0.347,1.334,0.633,2.05,0.851l1.557,4.377c0.571,0.064,1.148,0.104,1.737,0.104c0.588,0,1.165-0.04,1.736-0.104l1.558-4.377c0.715-0.218,1.399-0.504,2.049-0.851l4.193,1.993c0.909-0.725,1.733-1.549,2.458-2.458l-1.993-4.193c0.347-0.647,0.633-1.334,0.851-2.049L31.229,17.736zM16,20.871c-2.69,0-4.872-2.182-4.872-4.871c0-2.69,2.182-4.872,4.872-4.872c2.689,0,4.871,2.182,4.871,4.872C20.871,18.689,18.689,20.871,16,20.871z"/>',
3322         '<title>Link options.</title>',
3323         '</g>',
3324         '</g>'
3325     ].join(''),
3326
3327     // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`).
3328     // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for
3329     // dragging vertices (changin their position). The latter is used for removing vertices.
3330     vertexMarkup: [
3331         '<g class="marker-vertex-group" transform="translate(, )">',
3332         '<circle class="marker-vertex" idx="" r="10" />',
3333         '<path class="marker-vertex-remove-area" idx="" d="M16,5.333c-7.732,0-14,4.701-14,10.5c0,1.982,0.741,3.833,2.016,5.414L2,25.667l5.613-1.441c2.339,1.317,5.237,2.107,8.387,2.107c7.732,0,14-4.701,14-10.5C30,10.034,23.732,5.333,16,5.333z" transform="translate(5, -33)"/>',
3334         '<path class="marker-vertex-remove" idx="" transform="scale(.8) translate(9.5, -37)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z">',
3335         '<title>Remove vertex.</title>',
3336         '</path>',
3337         '</g>'
3338     ].join(''),
3339
3340     arrowheadMarkup: [
3341         '<g class="marker-arrowhead-group marker-arrowhead-group-">',
3342         '<path class="marker-arrowhead" end="" d="M 26 0 L 0 13 L 26 26 z" />',
3343         '</g>'
3344     ].join(''),
3345
3346     defaults: {
3347
3348         type: 'link',
3349         source: {},
3350         target: {}
3351     },
3352
3353     disconnect: function() {
3354
3355         return this.set({ source: g.point(0, 0), target: g.point(0, 0) });
3356     },
3357
3358     // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter.
3359     label: function(idx, value) {
3360
3361         idx = idx || 0;
3362
3363         var labels = this.get('labels') || [];
3364
3365         // Is it a getter?
3366         if (arguments.length === 0 || arguments.length === 1) {
3367
3368             return labels[idx];
3369         }
3370
3371         var newValue = _.merge({}, labels[idx], value);
3372
3373         var newLabels = labels.slice();
3374         newLabels[idx] = newValue;
3375
3376         return this.set({ labels: newLabels });
3377     },
3378
3379     translate: function(tx, ty, opt) {
3380
3381         var attrs = {};
3382         var source = this.get('source');
3383         var target = this.get('target');
3384         var vertices = this.get('vertices');
3385
3386         if (!source.id) {
3387             attrs.source = { x: source.x + tx, y: source.y + ty };
3388         }
3389
3390         if (!target.id) {
3391             attrs.target = { x: target.x + tx, y: target.y + ty };
3392         }
3393
3394         if (vertices && vertices.length) {
3395             attrs.vertices = _.map(vertices, function(vertex) {
3396                 return { x: vertex.x + tx, y: vertex.y + ty };
3397             });
3398         }
3399
3400         return this.set(attrs, opt);
3401     },
3402
3403     reparent: function(opt) {
3404
3405         var newParent;
3406
3407         if (this.collection) {
3408
3409             var source = this.collection.get(this.get('source').id);
3410             var target = this.collection.get(this.get('target').id);
3411             var prevParent = this.collection.get(this.get('parent'));
3412
3413             if (source && target) {
3414                 newParent = this.collection.getCommonAncestor(source, target);
3415             }
3416
3417             if (prevParent && (!newParent || newParent.id != prevParent.id)) {
3418                 // Unembed the link if source and target has no common ancestor
3419                 // or common ancestor changed
3420                 prevParent.unembed(this, opt);
3421             }
3422
3423             if (newParent) {
3424                 newParent.embed(this, opt);
3425             }
3426         }
3427
3428         return newParent;
3429     },
3430
3431     isLink: function() {
3432
3433         return true;
3434     },
3435
3436     hasLoop: function() {
3437
3438         var sourceId = this.get('source').id;
3439         var targetId = this.get('target').id;
3440
3441         return sourceId && targetId && sourceId == targetId;
3442     }
3443 });
3444
3445
3446 // joint.dia.Link base view and controller.
3447 // ----------------------------------------
3448
3449 joint.dia.LinkView = joint.dia.CellView.extend({
3450
3451     className: function() {
3452         return _.unique(this.model.get('type').split('.').concat('link')).join(' ');
3453     },
3454
3455     options: {
3456
3457         shortLinkLength: 100,
3458         doubleLinkTools: false,
3459         longLinkLength: 160,
3460         linkToolsOffset: 40,
3461         doubleLinkToolsOffset: 60,
3462         sampleInterval: 50
3463     },
3464
3465     initialize: function(options) {
3466
3467         joint.dia.CellView.prototype.initialize.apply(this, arguments);
3468
3469         // create methods in prototype, so they can be accessed from any instance and
3470         // don't need to be create over and over
3471         if (typeof this.constructor.prototype.watchSource !== 'function') {
3472             this.constructor.prototype.watchSource = this.createWatcher('source');
3473             this.constructor.prototype.watchTarget = this.createWatcher('target');
3474         }
3475
3476         // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to
3477         // `<g class="label">` nodes wrapped by Vectorizer. This allows for quick access to the
3478         // nodes in `updateLabelPosition()` in order to update the label positions.
3479         this._labelCache = {};
3480
3481         // keeps markers bboxes and positions again for quicker access
3482         this._markerCache = {};
3483
3484         // bind events
3485         this.startListening();
3486     },
3487
3488     startListening: function() {
3489
3490         this.listenTo(this.model, 'change:markup', this.render);
3491         this.listenTo(this.model, 'change:smooth change:manhattan change:router change:connector', this.update);
3492         this.listenTo(this.model, 'change:toolMarkup', function() {
3493             this.renderTools().updateToolsPosition();
3494         });
3495         this.listenTo(this.model, 'change:labels change:labelMarkup', function() {
3496             this.renderLabels().updateLabelPositions();
3497         });
3498         this.listenTo(this.model, 'change:vertices change:vertexMarkup', function(cell, changed, opt) {
3499             this.renderVertexMarkers();
3500             // If the vertices have been changed by a translation we do update only if the link was
3501             // only one translated. If the link was translated via another element which the link
3502             // is embedded in, this element will be translated as well and that triggers an update.
3503             // Note that all embeds in a model are sorted - first comes links, then elements.
3504             if (!opt.translateBy || (opt.translateBy == this.model.id || this.model.hasLoop())) {
3505                 this.update();
3506             }
3507         });
3508         this.listenTo(this.model, 'change:source', function(cell, source) {
3509             this.watchSource(cell, source).update();
3510         });
3511         this.listenTo(this.model, 'change:target', function(cell, target) {
3512             this.watchTarget(cell, target).update();
3513         });
3514     },
3515
3516     // Rendering
3517     //----------
3518
3519     render: function() {
3520
3521         this.$el.empty();
3522
3523         // A special markup can be given in the `properties.markup` property. This might be handy
3524         // if e.g. arrowhead markers should be `<image>` elements or any other element than `<path>`s.
3525         // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors
3526         // of elements with special meaning though. Therefore, those classes should be preserved in any
3527         // special markup passed in `properties.markup`.
3528         var children = V(this.model.get('markup') || this.model.markup);
3529
3530         // custom markup may contain only one children
3531         if (!_.isArray(children)) children = [children];
3532
3533         // Cache all children elements for quicker access.
3534         this._V = {}; // vectorized markup;
3535         _.each(children, function(child) {
3536             var c = child.attr('class');
3537             c && (this._V[$.camelCase(c)] = child);
3538         }, this);
3539
3540         // Only the connection path is mandatory
3541         if (!this._V.connection) throw new Error('link: no connection path in the markup');
3542
3543         // partial rendering
3544         this.renderTools();
3545         this.renderVertexMarkers();
3546         this.renderArrowheadMarkers();
3547
3548         V(this.el).append(children);
3549
3550         // rendering labels has to be run after the link is appended to DOM tree. (otherwise <Text> bbox
3551         // returns zero values)
3552         this.renderLabels();
3553
3554         // start watching the ends of the link for changes
3555         this.watchSource(this.model, this.model.get('source'))
3556             .watchTarget(this.model, this.model.get('target'))
3557             .update();
3558
3559         return this;
3560     },
3561
3562     renderLabels: function() {
3563
3564         if (!this._V.labels) return this;
3565
3566         this._labelCache = {};
3567         var $labels = $(this._V.labels.node).empty();
3568
3569         var labels = this.model.get('labels') || [];
3570         if (!labels.length) return this;
3571
3572         var labelTemplate = _.template(this.model.get('labelMarkup') || this.model.labelMarkup);
3573         // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from
3574         // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this
3575         // node to create a duplicate.
3576         var labelNodeInstance = V(labelTemplate());
3577
3578         var canLabelMove = this.can('labelMove');
3579
3580         _.each(labels, function(label, idx) {
3581
3582             var labelNode = labelNodeInstance.clone().node;
3583             V(labelNode).attr('label-idx', idx);
3584             if (canLabelMove) {
3585                 V(labelNode).attr('cursor', 'move');
3586             }
3587
3588             // Cache label nodes so that the `updateLabels()` can just update the label node positions.
3589             this._labelCache[idx] = V(labelNode);
3590
3591             var $text = $(labelNode).find('text');
3592             var $rect = $(labelNode).find('rect');
3593
3594             // Text attributes with the default `text-anchor` and font-size set.
3595             var textAttributes = _.extend({ 'text-anchor': 'middle', 'font-size': 14 }, joint.util.getByPath(label, 'attrs/text', '/'));
3596
3597             $text.attr(_.omit(textAttributes, 'text'));
3598
3599             if (!_.isUndefined(textAttributes.text)) {
3600
3601                 V($text[0]).text(textAttributes.text + '');
3602             }
3603
3604             // Note that we first need to append the `<text>` element to the DOM in order to
3605             // get its bounding box.
3606             $labels.append(labelNode);
3607
3608             // `y-alignment` - center the text element around its y coordinate.
3609             var textBbox = V($text[0]).bbox(true, $labels[0]);
3610             V($text[0]).translate(0, -textBbox.height / 2);
3611
3612             // Add default values.
3613             var rectAttributes = _.extend({
3614
3615                 fill: 'white',
3616                 rx: 3,
3617                 ry: 3
3618
3619             }, joint.util.getByPath(label, 'attrs/rect', '/'));
3620
3621             $rect.attr(_.extend(rectAttributes, {
3622                 x: textBbox.x,
3623                 y: textBbox.y - textBbox.height / 2,  // Take into account the y-alignment translation.
3624                 width: textBbox.width,
3625                 height: textBbox.height
3626             }));
3627
3628         }, this);
3629
3630         return this;
3631     },
3632
3633     renderTools: function() {
3634
3635         if (!this._V.linkTools) return this;
3636
3637         // Tools are a group of clickable elements that manipulate the whole link.
3638         // A good example of this is the remove tool that removes the whole link.
3639         // Tools appear after hovering the link close to the `source` element/point of the link
3640         // but are offset a bit so that they don't cover the `marker-arrowhead`.
3641
3642         var $tools = $(this._V.linkTools.node).empty();
3643         var toolTemplate = _.template(this.model.get('toolMarkup') || this.model.toolMarkup);
3644         var tool = V(toolTemplate());
3645
3646         $tools.append(tool.node);
3647
3648         // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly.
3649         this._toolCache = tool;
3650
3651         // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the
3652         // link as well but only if the link is longer than `longLinkLength`.
3653         if (this.options.doubleLinkTools) {
3654
3655             var tool2 = tool.clone();
3656             $tools.append(tool2.node);
3657             this._tool2Cache = tool2;
3658         }
3659
3660         return this;
3661     },
3662
3663     renderVertexMarkers: function() {
3664
3665         if (!this._V.markerVertices) return this;
3666
3667         var $markerVertices = $(this._V.markerVertices.node).empty();
3668
3669         // A special markup can be given in the `properties.vertexMarkup` property. This might be handy
3670         // if default styling (elements) are not desired. This makes it possible to use any
3671         // SVG elements for .marker-vertex and .marker-vertex-remove tools.
3672         var markupTemplate = _.template(this.model.get('vertexMarkup') || this.model.vertexMarkup);
3673
3674         _.each(this.model.get('vertices'), function(vertex, idx) {
3675
3676             $markerVertices.append(V(markupTemplate(_.extend({ idx: idx }, vertex))).node);
3677         });
3678
3679         return this;
3680     },
3681
3682     renderArrowheadMarkers: function() {
3683
3684         // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case.
3685         if (!this._V.markerArrowheads) return this;
3686
3687         var $markerArrowheads = $(this._V.markerArrowheads.node);
3688
3689         $markerArrowheads.empty();
3690
3691         // A special markup can be given in the `properties.vertexMarkup` property. This might be handy
3692         // if default styling (elements) are not desired. This makes it possible to use any
3693         // SVG elements for .marker-vertex and .marker-vertex-remove tools.
3694         var markupTemplate = _.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup);
3695
3696         this._V.sourceArrowhead = V(markupTemplate({ end: 'source' }));
3697         this._V.targetArrowhead = V(markupTemplate({ end: 'target' }));
3698
3699         $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node);
3700
3701         return this;
3702     },
3703
3704     // Updating
3705     //---------
3706
3707     // Default is to process the `attrs` object and set attributes on subelements based on the selectors.
3708     update: function() {
3709
3710         // Update attributes.
3711         _.each(this.model.get('attrs'), function(attrs, selector) {
3712
3713             var processedAttributes = [];
3714
3715             // If the `fill` or `stroke` attribute is an object, it is in the special JointJS gradient format and so
3716             // it becomes a special attribute and is treated separately.
3717             if (_.isObject(attrs.fill)) {
3718
3719                 this.applyGradient(selector, 'fill', attrs.fill);
3720                 processedAttributes.push('fill');
3721             }
3722
3723             if (_.isObject(attrs.stroke)) {
3724
3725                 this.applyGradient(selector, 'stroke', attrs.stroke);
3726                 processedAttributes.push('stroke');
3727             }
3728
3729             // If the `filter` attribute is an object, it is in the special JointJS filter format and so
3730             // it becomes a special attribute and is treated separately.
3731             if (_.isObject(attrs.filter)) {
3732
3733                 this.applyFilter(selector, attrs.filter);
3734                 processedAttributes.push('filter');
3735             }
3736
3737             // remove processed special attributes from attrs
3738             if (processedAttributes.length > 0) {
3739
3740                 processedAttributes.unshift(attrs);
3741                 attrs = _.omit.apply(_, processedAttributes);
3742             }
3743
3744             this.findBySelector(selector).attr(attrs);
3745
3746         }, this);
3747
3748         // Path finding
3749         var vertices = this.route = this.findRoute(this.model.get('vertices') || []);
3750
3751         // finds all the connection points taking new vertices into account
3752         this._findConnectionPoints(vertices);
3753
3754         var pathData = this.getPathData(vertices);
3755
3756         // The markup needs to contain a `.connection`
3757         this._V.connection.attr('d', pathData);
3758         this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData);
3759
3760         this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget);
3761
3762         //partials updates
3763         this.updateLabelPositions();
3764         this.updateToolsPosition();
3765         this.updateArrowheadMarkers();
3766
3767         delete this.options.perpendicular;
3768         // Mark that postponed update has been already executed.
3769         this.updatePostponed = false;
3770
3771         return this;
3772     },
3773
3774     _findConnectionPoints: function(vertices) {
3775
3776         // cache source and target points
3777         var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint;
3778
3779         var firstVertex = _.first(vertices);
3780
3781         sourcePoint = this.getConnectionPoint(
3782             'source', this.model.get('source'), firstVertex || this.model.get('target')
3783         ).round();
3784
3785         var lastVertex = _.last(vertices);
3786
3787         targetPoint = this.getConnectionPoint(
3788             'target', this.model.get('target'), lastVertex || sourcePoint
3789         ).round();
3790
3791         // Move the source point by the width of the marker taking into account
3792         // its scale around x-axis. Note that scale is the only transform that
3793         // makes sense to be set in `.marker-source` attributes object
3794         // as all other transforms (translate/rotate) will be replaced
3795         // by the `translateAndAutoOrient()` function.
3796         var cache = this._markerCache;
3797
3798         if (this._V.markerSource) {
3799
3800             cache.sourceBBox = cache.sourceBBox || this._V.markerSource.bbox(true);
3801
3802             sourceMarkerPoint = g.point(sourcePoint).move(
3803                 firstVertex || targetPoint,
3804                 cache.sourceBBox.width * this._V.markerSource.scale().sx * -1
3805             ).round();
3806         }
3807
3808         if (this._V.markerTarget) {
3809
3810             cache.targetBBox = cache.targetBBox || this._V.markerTarget.bbox(true);
3811
3812             targetMarkerPoint = g.point(targetPoint).move(
3813                 lastVertex || sourcePoint,
3814                 cache.targetBBox.width * this._V.markerTarget.scale().sx * -1
3815             ).round();
3816         }
3817
3818         // if there was no markup for the marker, use the connection point.
3819         cache.sourcePoint = sourceMarkerPoint || sourcePoint;
3820         cache.targetPoint = targetMarkerPoint || targetPoint;
3821
3822         // make connection points public
3823         this.sourcePoint = sourcePoint;
3824         this.targetPoint = targetPoint;
3825     },
3826
3827     updateLabelPositions: function() {
3828
3829         if (!this._V.labels) return this;
3830
3831         // This method assumes all the label nodes are stored in the `this._labelCache` hash table
3832         // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method.
3833
3834         var labels = this.model.get('labels') || [];
3835         if (!labels.length) return this;
3836
3837         var connectionElement = this._V.connection.node;
3838         var connectionLength = connectionElement.getTotalLength();
3839
3840         // Firefox returns connectionLength=NaN in odd cases (for bezier curves).
3841         // In that case we won't update labels at all.
3842         if (!_.isNaN(connectionLength)) {
3843
3844             var samples;
3845
3846             _.each(labels, function(label, idx) {
3847
3848                 var position = label.position;
3849                 var distance = _.isObject(position) ? position.distance : position;
3850                 var offset = _.isObject(position) ? position.offset : { x: 0, y: 0 };
3851
3852                 distance = (distance > connectionLength) ? connectionLength : distance; // sanity check
3853                 distance = (distance < 0) ? connectionLength + distance : distance;
3854                 distance = (distance > 1) ? distance : connectionLength * distance;
3855
3856                 var labelCoordinates = connectionElement.getPointAtLength(distance);
3857
3858                 if (_.isObject(offset)) {
3859
3860                     // Just offset the label by the x,y provided in the offset object.
3861                     labelCoordinates = g.point(labelCoordinates).offset(offset.x, offset.y);
3862
3863                 } else if (_.isNumber(offset)) {
3864
3865                     if (!samples) {
3866                         samples = this._samples || this._V.connection.sample(this.options.sampleInterval);
3867                     }
3868
3869                     // Offset the label by the amount provided in `offset` to an either
3870                     // side of the link.
3871
3872                     // 1. Find the closest sample & its left and right neighbours.
3873                     var minSqDistance = Infinity;
3874                     var closestSample;
3875                     var closestSampleIndex;
3876                     var p;
3877                     var sqDistance;
3878                     for (var i = 0, len = samples.length; i < len; i++) {
3879                         p = samples[i];
3880                         sqDistance = g.line(p, labelCoordinates).squaredLength();
3881                         if (sqDistance < minSqDistance) {
3882                             minSqDistance = sqDistance;
3883                             closestSample = p;
3884                             closestSampleIndex = i;
3885                         }
3886                     }
3887                     var prevSample = samples[closestSampleIndex - 1];
3888                     var nextSample = samples[closestSampleIndex + 1];
3889
3890                     // 2. Offset the label on the perpendicular line between
3891                     // the current label coordinate ("at `distance`") and
3892                     // the next sample.
3893                     var angle = 0;
3894                     if (nextSample) {
3895                         angle = g.point(labelCoordinates).theta(nextSample);
3896                     } else if (prevSample) {
3897                         angle = g.point(prevSample).theta(labelCoordinates);
3898                     }
3899                     labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90);
3900                 }
3901
3902                 this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')');
3903
3904             }, this);
3905         }
3906
3907         return this;
3908     },
3909
3910
3911     updateToolsPosition: function() {
3912
3913         if (!this._V.linkTools) return this;
3914
3915         // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker.
3916         // Note that the offset is hardcoded here. The offset should be always
3917         // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking
3918         // this up all the time would be slow.
3919
3920         var scale = '';
3921         var offset = this.options.linkToolsOffset;
3922         var connectionLength = this.getConnectionLength();
3923
3924         // Firefox returns connectionLength=NaN in odd cases (for bezier curves).
3925         // In that case we won't update tools position at all.
3926         if (!_.isNaN(connectionLength)) {
3927
3928             // If the link is too short, make the tools half the size and the offset twice as low.
3929             if (connectionLength < this.options.shortLinkLength) {
3930                 scale = 'scale(.5)';
3931                 offset /= 2;
3932             }
3933
3934             var toolPosition = this.getPointAtLength(offset);
3935
3936             this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale);
3937
3938             if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) {
3939
3940                 var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset;
3941
3942                 toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset);
3943                 this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale);
3944                 this._tool2Cache.attr('visibility', 'visible');
3945
3946             } else if (this.options.doubleLinkTools) {
3947
3948                 this._tool2Cache.attr('visibility', 'hidden');
3949             }
3950         }
3951
3952         return this;
3953     },
3954
3955
3956     updateArrowheadMarkers: function() {
3957
3958         if (!this._V.markerArrowheads) return this;
3959
3960         // getting bbox of an element with `display="none"` in IE9 ends up with access violation
3961         if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this;
3962
3963         var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1;
3964         this._V.sourceArrowhead.scale(sx);
3965         this._V.targetArrowhead.scale(sx);
3966
3967         this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead);
3968
3969         return this;
3970     },
3971
3972     // Returns a function observing changes on an end of the link. If a change happens and new end is a new model,
3973     // it stops listening on the previous one and starts listening to the new one.
3974     createWatcher: function(endType) {
3975
3976         // create handler for specific end type (source|target).
3977         var onModelChange = _.partial(this.onEndModelChange, endType);
3978
3979         function watchEndModel(link, end) {
3980
3981             end = end || {};
3982
3983             var endModel = null;
3984             var previousEnd = link.previous(endType) || {};
3985
3986             if (previousEnd.id) {
3987                 this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange);
3988             }
3989
3990             if (end.id) {
3991                 // If the observed model changes, it caches a new bbox and do the link update.
3992                 endModel = this.paper.getModelById(end.id);
3993                 this.listenTo(endModel, 'change', onModelChange);
3994             }
3995
3996             onModelChange.call(this, endModel, { cacheOnly: true });
3997
3998             return this;
3999         }
4000
4001         return watchEndModel;
4002     },
4003
4004     onEndModelChange: function(endType, endModel, opt) {
4005
4006         var doUpdate = !opt.cacheOnly;
4007         var end = this.model.get(endType) || {};
4008
4009         if (endModel) {
4010
4011             var selector = this.constructor.makeSelector(end);
4012             var oppositeEndType = endType == 'source' ? 'target' : 'source';
4013             var oppositeEnd = this.model.get(oppositeEndType) || {};
4014             var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd);
4015
4016             // Caching end models bounding boxes
4017             if (opt.isLoop && selector == oppositeSelector) {
4018
4019                 // Source and target elements are identical. We are handling `change` event for the
4020                 // second time now. There is no need to calculate bbox and find magnet element again.
4021                 // It was calculated already for opposite link end.
4022                 this[endType + 'BBox'] = this[oppositeEndType + 'BBox'];
4023                 this[endType + 'View'] = this[oppositeEndType + 'View'];
4024                 this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet'];
4025
4026             } else if (opt.translateBy) {
4027
4028                 var bbox = this[endType + 'BBox'];
4029                 bbox.x += opt.tx;
4030                 bbox.y += opt.ty;
4031
4032             } else {
4033
4034                 var view = this.paper.findViewByModel(end.id);
4035                 var magnetElement = view.el.querySelector(selector);
4036
4037                 this[endType + 'BBox'] = view.getStrokeBBox(magnetElement);
4038                 this[endType + 'View'] = view;
4039                 this[endType + 'Magnet'] = magnetElement;
4040             }
4041
4042             if (opt.isLoop && opt.translateBy &&
4043                 this.model.isEmbeddedIn(endModel) &&
4044                 !_.isEmpty(this.model.get('vertices'))) {
4045                 // If the link is embedded, has a loop and vertices and the end model
4046                 // has been translated, do not update yet. There are vertices still to be updated.
4047                 doUpdate = false;
4048             }
4049
4050             if (!this.updatePostponed && oppositeEnd.id) {
4051
4052                 var oppositeEndModel = this.paper.getModelById(oppositeEnd.id);
4053
4054                 // Passing `isLoop` flag via event option.
4055                 // Note that if we are listening to the same model for event 'change' twice.
4056                 // The same event will be handled by this method also twice.
4057                 opt.isLoop = end.id == oppositeEnd.id;
4058
4059                 if (opt.isLoop || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) {
4060
4061                     // Here are two options:
4062                     // - Source and target are connected to the same model (not necessary the same port)
4063                     // - both end models are translated by same ancestor. We know that opposte end
4064                     //   model will be translated in the moment as well.
4065                     // In both situations there will be more changes on model that will trigger an
4066                     // update. So there is no need to update the linkView yet.
4067                     this.updatePostponed = true;
4068                     doUpdate = false;
4069                 }
4070             }
4071
4072         } else {
4073
4074             // the link end is a point ~ rect 1x1
4075             this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1);
4076             this[endType + 'View'] = this[endType + 'Magnet'] = null;
4077         }
4078
4079         // keep track which end had been changed very last
4080         this.lastEndChange = endType;
4081
4082         doUpdate && this.update();
4083     },
4084
4085     _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) {
4086
4087         // Make the markers "point" to their sticky points being auto-oriented towards
4088         // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them.
4089         if (sourceArrow) {
4090             sourceArrow.translateAndAutoOrient(
4091                 this.sourcePoint,
4092                 _.first(this.route) || this.targetPoint,
4093                 this.paper.viewport
4094             );
4095         }
4096
4097         if (targetArrow) {
4098             targetArrow.translateAndAutoOrient(
4099                 this.targetPoint,
4100                 _.last(this.route) || this.sourcePoint,
4101                 this.paper.viewport
4102             );
4103         }
4104     },
4105
4106     removeVertex: function(idx) {
4107
4108         var vertices = _.clone(this.model.get('vertices'));
4109
4110         if (vertices && vertices.length) {
4111
4112             vertices.splice(idx, 1);
4113             this.model.set('vertices', vertices, { ui: true });
4114         }
4115
4116         return this;
4117     },
4118
4119     // This method ads a new vertex to the `vertices` array of `.connection`. This method
4120     // uses a heuristic to find the index at which the new `vertex` should be placed at assuming
4121     // the new vertex is somewhere on the path.
4122     addVertex: function(vertex) {
4123
4124         // As it is very hard to find a correct index of the newly created vertex,
4125         // a little heuristics is taking place here.
4126         // The heuristics checks if length of the newly created
4127         // path is lot more than length of the old path. If this is the case,
4128         // new vertex was probably put into a wrong index.
4129         // Try to put it into another index and repeat the heuristics again.
4130
4131         var vertices = (this.model.get('vertices') || []).slice();
4132         // Store the original vertices for a later revert if needed.
4133         var originalVertices = vertices.slice();
4134
4135         // A `<path>` element used to compute the length of the path during heuristics.
4136         var path = this._V.connection.node.cloneNode(false);
4137
4138         // Length of the original path.
4139         var originalPathLength = path.getTotalLength();
4140         // Current path length.
4141         var pathLength;
4142         // Tolerance determines the highest possible difference between the length
4143         // of the old and new path. The number has been chosen heuristically.
4144         var pathLengthTolerance = 20;
4145         // Total number of vertices including source and target points.
4146         var idx = vertices.length + 1;
4147
4148         // Loop through all possible indexes and check if the difference between
4149         // path lengths changes significantly. If not, the found index is
4150         // most probably the right one.
4151         while (idx--) {
4152
4153             vertices.splice(idx, 0, vertex);
4154             V(path).attr('d', this.getPathData(this.findRoute(vertices)));
4155
4156             pathLength = path.getTotalLength();
4157
4158             // Check if the path lengths changed significantly.
4159             if (pathLength - originalPathLength > pathLengthTolerance) {
4160
4161                 // Revert vertices to the original array. The path length has changed too much
4162                 // so that the index was not found yet.
4163                 vertices = originalVertices.slice();
4164
4165             } else {
4166
4167                 break;
4168             }
4169         }
4170
4171         if (idx === -1) {
4172             // If no suitable index was found for such a vertex, make the vertex the first one.
4173             idx = 0;
4174             vertices.splice(idx, 0, vertex);
4175         }
4176
4177         this.model.set('vertices', vertices, { ui: true });
4178
4179         return idx;
4180     },
4181
4182     // Send a token (an SVG element, usually a circle) along the connection path.
4183     // Example: `paper.findViewByModel(link).sendToken(V('circle', { r: 7, fill: 'green' }).node)`
4184     // `duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`.
4185     // `callback` is optional and is a function to be called once the token reaches the target.
4186     sendToken: function(token, duration, callback) {
4187
4188         duration = duration || 1000;
4189
4190         V(this.paper.viewport).append(token);
4191         V(token).animateAlongPath({ dur: duration + 'ms', repeatCount: 1 }, this._V.connection.node);
4192         _.delay(function() { V(token).remove(); callback && callback(); }, duration);
4193     },
4194
4195     findRoute: function(oldVertices) {
4196
4197         var router = this.model.get('router');
4198
4199         if (!router) {
4200
4201             if (this.model.get('manhattan')) {
4202                 // backwards compability
4203                 router = { name: 'orthogonal' };
4204             } else {
4205
4206                 return oldVertices;
4207             }
4208         }
4209
4210         var fn = joint.routers[router.name];
4211
4212         if (!_.isFunction(fn)) {
4213
4214             throw 'unknown router: ' + router.name;
4215         }
4216
4217         var newVertices = fn.call(this, oldVertices || [], router.args || {}, this);
4218
4219         return newVertices;
4220     },
4221
4222     // Return the `d` attribute value of the `<path>` element representing the link
4223     // between `source` and `target`.
4224     getPathData: function(vertices) {
4225
4226         var connector = this.model.get('connector');
4227
4228         if (!connector) {
4229
4230             // backwards compability
4231             connector = this.model.get('smooth') ? { name: 'smooth' } : { name: 'normal' };
4232         }
4233
4234         if (!_.isFunction(joint.connectors[connector.name])) {
4235
4236             throw 'unknown connector: ' + connector.name;
4237         }
4238
4239         var pathData = joint.connectors[connector.name].call(
4240             this,
4241             this._markerCache.sourcePoint, // Note that the value is translated by the size
4242             this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint)
4243             vertices || (this.model.get('vertices') || {}),
4244             connector.args || {}, // options
4245             this
4246         );
4247
4248         return pathData;
4249     },
4250
4251     // Find a point that is the start of the connection.
4252     // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection.
4253     // If the `selectorOrPoint` is an element however, we need to know a reference point (or element)
4254     // that the link leads to in order to determine the start of the connection on the original element.
4255     getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) {
4256
4257         var spot;
4258
4259         // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`.
4260         // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and
4261         // set the `source`/`target` later.
4262         _.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 });
4263         _.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 });
4264
4265         if (!selectorOrPoint.id) {
4266
4267             // If the source is a point, we don't need a reference point to find the sticky point of connection.
4268             spot = g.point(selectorOrPoint);
4269
4270         } else {
4271
4272             // If the source is an element, we need to find a point on the element boundary that is closest
4273             // to the reference point (or reference element).
4274             // Get the bounding box of the spot relative to the paper viewport. This is necessary
4275             // in order to follow paper viewport transformations (scale/rotate).
4276             // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`)
4277             // method, it exists since first render and are automatically updated
4278             var spotBbox = end === 'source' ? this.sourceBBox : this.targetBBox;
4279
4280             var reference;
4281
4282             if (!referenceSelectorOrPoint.id) {
4283
4284                 // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element.
4285                 reference = g.point(referenceSelectorOrPoint);
4286
4287             } else {
4288
4289                 // Reference was passed as an element, therefore we need to find a point on the reference
4290                 // element boundary closest to the source element.
4291                 // Get the bounding box of the spot relative to the paper viewport. This is necessary
4292                 // in order to follow paper viewport transformations (scale/rotate).
4293                 var referenceBbox = end === 'source' ? this.targetBBox : this.sourceBBox;
4294
4295                 reference = g.rect(referenceBbox).intersectionWithLineFromCenterToPoint(g.rect(spotBbox).center());
4296                 reference = reference || g.rect(referenceBbox).center();
4297             }
4298
4299             // If `perpendicularLinks` flag is set on the paper and there are vertices
4300             // on the link, then try to find a connection point that makes the link perpendicular
4301             // even though the link won't point to the center of the targeted object.
4302             if (this.paper.options.perpendicularLinks || this.options.perpendicular) {
4303
4304                 var horizontalLineRect = g.rect(0, reference.y, this.paper.options.width, 1);
4305                 var verticalLineRect = g.rect(reference.x, 0, 1, this.paper.options.height);
4306                 var nearestSide;
4307
4308                 if (horizontalLineRect.intersect(g.rect(spotBbox))) {
4309
4310                     nearestSide = g.rect(spotBbox).sideNearestToPoint(reference);
4311                     switch (nearestSide) {
4312                         case 'left':
4313                             spot = g.point(spotBbox.x, reference.y);
4314                             break;
4315                         case 'right':
4316                             spot = g.point(spotBbox.x + spotBbox.width, reference.y);
4317                             break;
4318                         default:
4319                             spot = g.rect(spotBbox).center();
4320                             break;
4321                     }
4322
4323                 } else if (verticalLineRect.intersect(g.rect(spotBbox))) {
4324
4325                     nearestSide = g.rect(spotBbox).sideNearestToPoint(reference);
4326                     switch (nearestSide) {
4327                         case 'top':
4328                             spot = g.point(reference.x, spotBbox.y);
4329                             break;
4330                         case 'bottom':
4331                             spot = g.point(reference.x, spotBbox.y + spotBbox.height);
4332                             break;
4333                         default:
4334                             spot = g.rect(spotBbox).center();
4335                             break;
4336                     }
4337
4338                 } else {
4339
4340                     // If there is no intersection horizontally or vertically with the object bounding box,
4341                     // then we fall back to the regular situation finding straight line (not perpendicular)
4342                     // between the object and the reference point.
4343
4344                     spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference);
4345                     spot = spot || g.rect(spotBbox).center();
4346                 }
4347
4348             } else if (this.paper.options.linkConnectionPoint) {
4349
4350                 var view = end === 'target' ? this.targetView : this.sourceView;
4351                 var magnet = end === 'target' ? this.targetMagnet : this.sourceMagnet;
4352
4353                 spot = this.paper.options.linkConnectionPoint(this, view, magnet, reference);
4354
4355             } else {
4356
4357                 spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference);
4358                 spot = spot || g.rect(spotBbox).center();
4359             }
4360         }
4361
4362         return spot;
4363     },
4364
4365     // Public API
4366     // ----------
4367
4368     getConnectionLength: function() {
4369
4370         return this._V.connection.node.getTotalLength();
4371     },
4372
4373     getPointAtLength: function(length) {
4374
4375         return this._V.connection.node.getPointAtLength(length);
4376     },
4377
4378     // Interaction. The controller part.
4379     // ---------------------------------
4380
4381     _beforeArrowheadMove: function() {
4382
4383         this._z = this.model.get('z');
4384         this.model.toFront();
4385
4386         // Let the pointer propagate throught the link view elements so that
4387         // the `evt.target` is another element under the pointer, not the link itself.
4388         this.el.style.pointerEvents = 'none';
4389
4390         if (this.paper.options.markAvailable) {
4391             this._markAvailableMagnets();
4392         }
4393     },
4394
4395     _afterArrowheadMove: function() {
4396
4397         if (this._z) {
4398             this.model.set('z', this._z, { ui: true });
4399             delete this._z;
4400         }
4401
4402         // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation.
4403         // Value `auto` doesn't work in IE9. We force to use `visiblePainted` instead.
4404         // See `https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events`.
4405         this.el.style.pointerEvents = 'visiblePainted';
4406
4407         if (this.paper.options.markAvailable) {
4408             this._unmarkAvailableMagnets();
4409         }
4410     },
4411
4412     _createValidateConnectionArgs: function(arrowhead) {
4413         // It makes sure the arguments for validateConnection have the following form:
4414         // (source view, source magnet, target view, target magnet and link view)
4415         var args = [];
4416
4417         args[4] = arrowhead;
4418         args[5] = this;
4419
4420         var oppositeArrowhead;
4421         var i = 0;
4422         var j = 0;
4423
4424         if (arrowhead === 'source') {
4425             i = 2;
4426             oppositeArrowhead = 'target';
4427         } else {
4428             j = 2;
4429             oppositeArrowhead = 'source';
4430         }
4431
4432         var end = this.model.get(oppositeArrowhead);
4433
4434         if (end.id) {
4435             args[i] = this.paper.findViewByModel(end.id);
4436             args[i + 1] = end.selector && args[i].el.querySelector(end.selector);
4437         }
4438
4439         function validateConnectionArgs(cellView, magnet) {
4440             args[j] = cellView;
4441             args[j + 1] = cellView.el === magnet ? undefined : magnet;
4442             return args;
4443         }
4444
4445         return validateConnectionArgs;
4446     },
4447
4448     _markAvailableMagnets: function() {
4449
4450         var elements = this.paper.model.getElements();
4451         var validate = this.paper.options.validateConnection;
4452
4453         _.chain(elements).map(this.paper.findViewByModel, this.paper).each(function(view) {
4454
4455             var isElementAvailable = view.el.getAttribute('magnet') !== 'false' &&
4456                 validate.apply(this.paper, this._validateConnectionArgs(view, null));
4457
4458             var availableMagnets = _.filter(view.el.querySelectorAll('[magnet]'), function(magnet) {
4459                 return validate.apply(this.paper, this._validateConnectionArgs(view, magnet));
4460             }, this);
4461
4462             if (isElementAvailable) {
4463                 V(view.el).addClass('available-magnet');
4464             }
4465
4466             _.each(availableMagnets, function(magnet) {
4467                 V(magnet).addClass('available-magnet');
4468             });
4469
4470             if (isElementAvailable || availableMagnets.length) {
4471                 V(view.el).addClass('available-cell');
4472             }
4473
4474         }, this);
4475     },
4476
4477     _unmarkAvailableMagnets: function() {
4478
4479         _.each(this.paper.el.querySelectorAll('.available-cell, .available-magnet'), function(magnet) {
4480             V(magnet).removeClass('available-magnet').removeClass('available-cell');
4481         });
4482     },
4483
4484     startArrowheadMove: function(end) {
4485         // Allow to delegate events from an another view to this linkView in order to trigger arrowhead
4486         // move without need to click on the actual arrowhead dom element.
4487         this._action = 'arrowhead-move';
4488         this._arrowhead = end;
4489         this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead);
4490         this._beforeArrowheadMove();
4491     },
4492
4493     // Return `true` if the link is allowed to perform a certain UI `feature`.
4494     // Example: `can('vertexMove')`, `can('labelMove')`.
4495     can: function(feature) {
4496
4497         var interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this, 'pointerdown') : this.options.interactive;
4498         if (!_.isObject(interactive) || interactive[feature] !== false) return true;
4499         return false;
4500     },
4501
4502     pointerdown: function(evt, x, y) {
4503
4504         joint.dia.CellView.prototype.pointerdown.apply(this, arguments);
4505         this.notify('link:pointerdown', evt, x, y);
4506
4507         this._dx = x;
4508         this._dy = y;
4509
4510         // if are simulating pointerdown on a link during a magnet click, skip link interactions
4511         if (evt.target.getAttribute('magnet') != null) return;
4512
4513         var interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this, 'pointerdown') : this.options.interactive;
4514         if (interactive === false) return;
4515
4516         var className = evt.target.getAttribute('class');
4517         var parentClassName = evt.target.parentNode.getAttribute('class');
4518         var labelNode;
4519         if (parentClassName === 'label') {
4520             className = parentClassName;
4521             labelNode = evt.target.parentNode;
4522         } else {
4523             labelNode = evt.target;
4524         }
4525
4526         switch (className) {
4527
4528             case 'marker-vertex':
4529                 if (this.can('vertexMove')) {
4530                     this._action = 'vertex-move';
4531                     this._vertexIdx = evt.target.getAttribute('idx');
4532                 }
4533                 break;
4534
4535             case 'marker-vertex-remove':
4536             case 'marker-vertex-remove-area':
4537                 if (this.can('vertexRemove')) {
4538                     this.removeVertex(evt.target.getAttribute('idx'));
4539                 }
4540                 break;
4541
4542             case 'marker-arrowhead':
4543                 if (this.can('arrowheadMove')) {
4544                     this.startArrowheadMove(evt.target.getAttribute('end'));
4545                 }
4546                 break;
4547
4548             case 'label':
4549                 if (this.can('labelMove')) {
4550                     this._action = 'label-move';
4551                     this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10);
4552                     // Precalculate samples so that we don't have to do that
4553                     // over and over again while dragging the label.
4554                     this._samples = this._V.connection.sample(1);
4555                     this._linkLength = this._V.connection.node.getTotalLength();
4556                 }
4557                 break;
4558
4559             default:
4560
4561                 var targetParentEvent = evt.target.parentNode.getAttribute('event');
4562                 if (targetParentEvent) {
4563
4564                     // `remove` event is built-in. Other custom events are triggered on the paper.
4565                     if (targetParentEvent === 'remove') {
4566                         this.model.remove();
4567                     } else {
4568                         this.paper.trigger(targetParentEvent, evt, this, x, y);
4569                     }
4570
4571                 } else {
4572                     if (this.can('vertexAdd')) {
4573
4574                         // Store the index at which the new vertex has just been placed.
4575                         // We'll be update the very same vertex position in `pointermove()`.
4576                         this._vertexIdx = this.addVertex({ x: x, y: y });
4577                         this._action = 'vertex-move';
4578                     }
4579                 }
4580         }
4581     },
4582
4583     pointermove: function(evt, x, y) {
4584
4585         switch (this._action) {
4586
4587         case 'vertex-move':
4588
4589             var vertices = _.clone(this.model.get('vertices'));
4590             vertices[this._vertexIdx] = { x: x, y: y };
4591             this.model.set('vertices', vertices, { ui: true });
4592             break;
4593
4594         case 'label-move':
4595
4596             var dragPoint = { x: x, y: y };
4597             var label = this.model.get('labels')[this._labelIdx];
4598             var samples = this._samples;
4599             var minSqDistance = Infinity;
4600             var closestSample;
4601             var closestSampleIndex;
4602             var p;
4603             var sqDistance;
4604             for (var i = 0, len = samples.length; i < len; i++) {
4605                 p = samples[i];
4606                 sqDistance = g.line(p, dragPoint).squaredLength();
4607                 if (sqDistance < minSqDistance) {
4608                     minSqDistance = sqDistance;
4609                     closestSample = p;
4610                     closestSampleIndex = i;
4611                 }
4612             }
4613             var prevSample = samples[closestSampleIndex - 1];
4614             var nextSample = samples[closestSampleIndex + 1];
4615
4616             var closestSampleDistance = g.point(closestSample).distance(dragPoint);
4617             var offset = 0;
4618             if (prevSample && nextSample) {
4619                 offset = g.line(prevSample, nextSample).pointOffset(dragPoint);
4620             } else if (prevSample) {
4621                 offset = g.line(prevSample, closestSample).pointOffset(dragPoint);
4622             } else if (nextSample) {
4623                 offset = g.line(closestSample, nextSample).pointOffset(dragPoint);
4624             }
4625
4626             this.model.label(this._labelIdx, {
4627                 position: {
4628                     distance: closestSample.distance / this._linkLength,
4629                     offset: offset
4630                 }
4631             });
4632             break;
4633
4634         case 'arrowhead-move':
4635
4636             if (this.paper.options.snapLinks) {
4637
4638                 // checking view in close area of the pointer
4639
4640                 var r = this.paper.options.snapLinks.radius || 50;
4641                 var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
4642
4643                 this._closestView && this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true });
4644                 this._closestView = this._closestEnd = null;
4645
4646                 var distance;
4647                 var minDistance = Number.MAX_VALUE;
4648                 var pointer = g.point(x, y);
4649
4650                 _.each(viewsInArea, function(view) {
4651
4652                     // skip connecting to the element in case '.': { magnet: false } attribute present
4653                     if (view.el.getAttribute('magnet') !== 'false') {
4654
4655                         // find distance from the center of the model to pointer coordinates
4656                         distance = view.model.getBBox().center().distance(pointer);
4657
4658                         // the connection is looked up in a circle area by `distance < r`
4659                         if (distance < r && distance < minDistance) {
4660
4661                             if (this.paper.options.validateConnection.apply(
4662                                 this.paper, this._validateConnectionArgs(view, null)
4663                             )) {
4664                                 minDistance = distance;
4665                                 this._closestView = view;
4666                                 this._closestEnd = { id: view.model.id };
4667                             }
4668                         }
4669                     }
4670
4671                     view.$('[magnet]').each(_.bind(function(index, magnet) {
4672
4673                         var bbox = V(magnet).bbox(false, this.paper.viewport);
4674
4675                         distance = pointer.distance({
4676                             x: bbox.x + bbox.width / 2,
4677                             y: bbox.y + bbox.height / 2
4678                         });
4679
4680                         if (distance < r && distance < minDistance) {
4681
4682                             if (this.paper.options.validateConnection.apply(
4683                                 this.paper, this._validateConnectionArgs(view, magnet)
4684                             )) {
4685                                 minDistance = distance;
4686                                 this._closestView = view;
4687                                 this._closestEnd = {
4688                                     id: view.model.id,
4689                                     selector: view.getSelector(magnet),
4690                                     port: magnet.getAttribute('port')
4691                                 };
4692                             }
4693                         }
4694
4695                     }, this));
4696
4697                 }, this);
4698
4699                 this._closestView && this._closestView.highlight(this._closestEnd.selector, { connecting: true, snapping: true });
4700
4701                 this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true });
4702
4703             } else {
4704
4705                 // checking views right under the pointer
4706
4707                 // Touchmove event's target is not reflecting the element under the coordinates as mousemove does.
4708                 // It holds the element when a touchstart triggered.
4709                 var target = (evt.type === 'mousemove')
4710                     ? evt.target
4711                     : document.elementFromPoint(evt.clientX, evt.clientY);
4712
4713                 if (this._targetEvent !== target) {
4714                     // Unhighlight the previous view under pointer if there was one.
4715                     this._magnetUnderPointer && this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { connecting: true });
4716                     this._viewUnderPointer = this.paper.findView(target);
4717                     if (this._viewUnderPointer) {
4718                         // If we found a view that is under the pointer, we need to find the closest
4719                         // magnet based on the real target element of the event.
4720                         this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target);
4721
4722                         if (this._magnetUnderPointer && this.paper.options.validateConnection.apply(
4723                             this.paper,
4724                             this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer)
4725                         )) {
4726                             // If there was no magnet found, do not highlight anything and assume there
4727                             // is no view under pointer we're interested in reconnecting to.
4728                             // This can only happen if the overall element has the attribute `'.': { magnet: false }`.
4729                             this._magnetUnderPointer && this._viewUnderPointer.highlight(this._magnetUnderPointer, { connecting: true });
4730                         } else {
4731                             // This type of connection is not valid. Disregard this magnet.
4732                             this._magnetUnderPointer = null;
4733                         }
4734                     } else {
4735                         // Make sure we'll delete previous magnet
4736                         this._magnetUnderPointer = null;
4737                     }
4738                 }
4739
4740                 this._targetEvent = target;
4741
4742                 this.model.set(this._arrowhead, { x: x, y: y }, { ui: true });
4743             }
4744
4745             break;
4746         }
4747
4748         this._dx = x;
4749         this._dy = y;
4750
4751         joint.dia.CellView.prototype.pointermove.apply(this, arguments);
4752         this.notify('link:pointermove', evt, x, y);
4753     },
4754
4755     pointerup: function(evt, x, y) {
4756
4757         if (this._action === 'label-move') {
4758
4759             this._samples = null;
4760
4761         } else if (this._action === 'arrowhead-move') {
4762
4763             if (this.paper.options.snapLinks) {
4764
4765                 this._closestView && this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true });
4766                 this._closestView = this._closestEnd = null;
4767
4768             } else {
4769
4770                 if (this._magnetUnderPointer) {
4771                     this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { connecting: true });
4772                     // Find a unique `selector` of the element under pointer that is a magnet. If the
4773                     // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself,
4774                     // the returned `selector` will be `undefined`. That means we can directly pass it to the
4775                     // `source`/`target` attribute of the link model below.
4776                     this.model.set(this._arrowhead, {
4777                         id: this._viewUnderPointer.model.id,
4778                         selector: this._viewUnderPointer.getSelector(this._magnetUnderPointer),
4779                         port: $(this._magnetUnderPointer).attr('port')
4780                     }, { ui: true });
4781                 }
4782
4783                 delete this._viewUnderPointer;
4784                 delete this._magnetUnderPointer;
4785             }
4786
4787             // Reparent the link if embedding is enabled
4788             if (this.paper.options.embeddingMode && this.model.reparent()) {
4789
4790                 // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()).
4791                 delete this._z;
4792             }
4793
4794             this._afterArrowheadMove();
4795         }
4796
4797         delete this._action;
4798
4799         this.notify('link:pointerup', evt, x, y);
4800         joint.dia.CellView.prototype.pointerup.apply(this, arguments);
4801
4802     }
4803
4804 }, {
4805
4806     makeSelector: function(end) {
4807
4808         var selector = '[model-id="' + end.id + '"]';
4809         // `port` has a higher precendence over `selector`. This is because the selector to the magnet
4810         // might change while the name of the port can stay the same.
4811         if (end.port) {
4812             selector += ' [port="' + end.port + '"]';
4813         } else if (end.selector) {
4814             selector += ' ' + end.selector;
4815         }
4816
4817         return selector;
4818     }
4819
4820 });
4821
4822 //      JointJS library.
4823 //      (c) 2011-2013 client IO
4824
4825
4826 joint.dia.Paper = Backbone.View.extend({
4827
4828     className: 'paper',
4829
4830     options: {
4831
4832         width: 800,
4833         height: 600,
4834         origin: { x: 0, y: 0 }, // x,y coordinates in top-left corner
4835         gridSize: 50,
4836         perpendicularLinks: false,
4837         elementView: joint.dia.ElementView,
4838         linkView: joint.dia.LinkView,
4839         snapLinks: false, // false, true, { radius: value }
4840
4841         // Marks all available magnets with 'available-magnet' class name and all available cells with
4842         // 'available-cell' class name. Marks them when dragging a link is started and unmark
4843         // when the dragging is stopped.
4844         markAvailable: false,
4845
4846         // Defines what link model is added to the graph after an user clicks on an active magnet.
4847         // Value could be the Backbone.model or a function returning the Backbone.model
4848         // defaultLink: function(elementView, magnet) { return condition ? new customLink1() : new customLink2() }
4849         defaultLink: new joint.dia.Link,
4850
4851         /* CONNECTING */
4852
4853         // Check whether to add a new link to the graph when user clicks on an a magnet.
4854         validateMagnet: function(cellView, magnet) {
4855             return magnet.getAttribute('magnet') !== 'passive';
4856         },
4857
4858         // Check whether to allow or disallow the link connection while an arrowhead end (source/target)
4859         // being changed.
4860         validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
4861             return (end === 'target' ? cellViewT : cellViewS) instanceof joint.dia.ElementView;
4862         },
4863
4864         /* EMBEDDING */
4865
4866         // Enables embedding. Reparents the dragged element with elements under it and makes sure that
4867         // all links and elements are visible taken the level of embedding into account.
4868         embeddingMode: false,
4869
4870         // Check whether to allow or disallow the element embedding while an element being translated.
4871         validateEmbedding: function(childView, parentView) {
4872             // by default all elements can be in relation child-parent
4873             return true;
4874         },
4875
4876         // Determines the way how a cell finds a suitable parent when it's dragged over the paper.
4877         // The cell with the highest z-index (visually on the top) will be choosen.
4878         findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft'
4879
4880         // If enabled only the element on the very front is taken into account for the embedding.
4881         // If disabled the elements under the dragged view are tested one by one
4882         // (from front to back) until a valid parent found.
4883         frontParentOnly: true,
4884
4885         // Interactive flags. See online docs for the complete list of interactive flags.
4886         interactive: {
4887             labelMove: false
4888         }
4889     },
4890
4891     events: {
4892
4893         'mousedown': 'pointerdown',
4894         'dblclick': 'mousedblclick',
4895         'click': 'mouseclick',
4896         'touchstart': 'pointerdown',
4897         'mousemove': 'pointermove',
4898         'touchmove': 'pointermove',
4899         'mouseover .element': 'cellMouseover',
4900         'mouseover .link': 'cellMouseover',
4901         'mouseout .element': 'cellMouseout',
4902         'mouseout .link': 'cellMouseout'
4903     },
4904
4905     constructor: function(options) {
4906
4907         this._configure(options);
4908         Backbone.View.apply(this, arguments);
4909     },
4910
4911     _configure: function(options) {
4912
4913         if (this.options) options = _.extend({}, _.result(this, 'options'), options);
4914         this.options = options;
4915     },
4916
4917     initialize: function() {
4918
4919         _.bindAll(this, 'addCell', 'sortCells', 'resetCells', 'pointerup', 'asyncRenderCells');
4920
4921         this.svg = V('svg').node;
4922         this.viewport = V('g').addClass('viewport').node;
4923         this.defs = V('defs').node;
4924
4925         // Append `<defs>` element to the SVG document. This is useful for filters and gradients.
4926         V(this.svg).append([this.viewport, this.defs]);
4927
4928         this.$el.append(this.svg);
4929
4930         this.setOrigin();
4931         this.setDimensions();
4932
4933         this.listenTo(this.model, 'add', this.onAddCell);
4934         this.listenTo(this.model, 'reset', this.resetCells);
4935         this.listenTo(this.model, 'sort', this.sortCells);
4936
4937         $(document).on('mouseup touchend', this.pointerup);
4938
4939         // Hold the value when mouse has been moved: when mouse moved, no click event will be triggered.
4940         this._mousemoved = false;
4941
4942         // default cell highlighting
4943         this.on({ 'cell:highlight': this.onCellHighlight, 'cell:unhighlight': this.onCellUnhighlight });
4944     },
4945
4946     remove: function() {
4947
4948         //clean up all DOM elements/views to prevent memory leaks
4949         this.removeCells();
4950
4951         $(document).off('mouseup touchend', this.pointerup);
4952
4953         Backbone.View.prototype.remove.call(this);
4954     },
4955
4956     setDimensions: function(width, height) {
4957
4958         width = this.options.width = width || this.options.width;
4959         height = this.options.height = height || this.options.height;
4960
4961         V(this.svg).attr({ width: width, height: height });
4962
4963         this.trigger('resize', width, height);
4964     },
4965
4966     setOrigin: function(ox, oy) {
4967
4968         this.options.origin.x = ox || 0;
4969         this.options.origin.y = oy || 0;
4970
4971         V(this.viewport).translate(ox, oy, { absolute: true });
4972
4973         this.trigger('translate', ox, oy);
4974     },
4975
4976     // Expand/shrink the paper to fit the content. Snap the width/height to the grid
4977     // defined in `gridWidth`, `gridHeight`. `padding` adds to the resulting width/height of the paper.
4978     // When options { fitNegative: true } it also translates the viewport in order to make all
4979     // the content visible.
4980     fitToContent: function(gridWidth, gridHeight, padding, opt) { // alternatively function(opt)
4981
4982         if (_.isObject(gridWidth)) {
4983             // first parameter is an option object
4984             opt = gridWidth;
4985             gridWidth = opt.gridWidth || 1;
4986             gridHeight = opt.gridHeight || 1;
4987             padding = opt.padding || 0;
4988
4989         } else {
4990
4991             opt = opt || {};
4992             gridWidth = gridWidth || 1;
4993             gridHeight = gridHeight || 1;
4994             padding = padding || 0;
4995         }
4996
4997         padding = _.isNumber(padding)
4998             ? { left: padding, right: padding, top: padding, bottom: padding }
4999         : { left: padding.left || 0, right: padding.right || 0, top: padding.top || 0, bottom: padding.bottom || 0 };
5000
5001         // Calculate the paper size to accomodate all the graph's elements.
5002         var bbox = V(this.viewport).bbox(true, this.svg);
5003
5004         var currentScale = V(this.viewport).scale();
5005
5006         bbox.x *= currentScale.sx;
5007         bbox.y *= currentScale.sy;
5008         bbox.width *= currentScale.sx;
5009         bbox.height *= currentScale.sy;
5010
5011         var calcWidth = Math.max(Math.ceil((bbox.width + bbox.x) / gridWidth), 1) * gridWidth;
5012         var calcHeight = Math.max(Math.ceil((bbox.height + bbox.y) / gridHeight), 1) * gridHeight;
5013
5014         var tx = 0;
5015         var ty = 0;
5016
5017         if ((opt.allowNewOrigin == 'negative' && bbox.x < 0) || (opt.allowNewOrigin == 'positive' && bbox.x >= 0) || opt.allowNewOrigin == 'any') {
5018             tx = Math.ceil(-bbox.x / gridWidth) * gridWidth;
5019             tx += padding.left;
5020             calcWidth += tx;
5021         }
5022
5023         if ((opt.allowNewOrigin == 'negative' && bbox.y < 0) || (opt.allowNewOrigin == 'positive' && bbox.y >= 0) || opt.allowNewOrigin == 'any') {
5024             ty = Math.ceil(-bbox.y / gridHeight) * gridHeight;
5025             ty += padding.top;
5026             calcHeight += ty;
5027         }
5028
5029         calcWidth += padding.right;
5030         calcHeight += padding.bottom;
5031
5032         // Make sure the resulting width and height are greater than minimum.
5033         calcWidth = Math.max(calcWidth, opt.minWidth || 0);
5034         calcHeight = Math.max(calcHeight, opt.minHeight || 0);
5035
5036         var dimensionChange = calcWidth != this.options.width || calcHeight != this.options.height;
5037         var originChange = tx != this.options.origin.x || ty != this.options.origin.y;
5038
5039         // Change the dimensions only if there is a size discrepency or an origin change
5040         if (originChange) {
5041             this.setOrigin(tx, ty);
5042         }
5043         if (dimensionChange) {
5044             this.setDimensions(calcWidth, calcHeight);
5045         }
5046     },
5047
5048     scaleContentToFit: function(opt) {
5049
5050         var contentBBox = this.getContentBBox();
5051
5052         if (!contentBBox.width || !contentBBox.height) return;
5053
5054         opt = opt || {};
5055
5056         _.defaults(opt, {
5057             padding: 0,
5058             preserveAspectRatio: true,
5059             scaleGrid: null,
5060             minScale: 0,
5061             maxScale: Number.MAX_VALUE
5062             //minScaleX
5063             //minScaleY
5064             //maxScaleX
5065             //maxScaleY
5066             //fittingBBox
5067         });
5068
5069         var padding = opt.padding;
5070
5071         var minScaleX = opt.minScaleX || opt.minScale;
5072         var maxScaleX = opt.maxScaleX || opt.maxScale;
5073         var minScaleY = opt.minScaleY || opt.minScale;
5074         var maxScaleY = opt.maxScaleY || opt.maxScale;
5075
5076         var fittingBBox = opt.fittingBBox || ({
5077             x: this.options.origin.x,
5078             y: this.options.origin.y,
5079             width: this.options.width,
5080             height: this.options.height
5081         });
5082
5083         fittingBBox = g.rect(fittingBBox).moveAndExpand({
5084             x: padding,
5085             y: padding,
5086             width: -2 * padding,
5087             height: -2 * padding
5088         });
5089
5090         var currentScale = V(this.viewport).scale();
5091
5092         var newSx = fittingBBox.width / contentBBox.width * currentScale.sx;
5093         var newSy = fittingBBox.height / contentBBox.height * currentScale.sy;
5094
5095         if (opt.preserveAspectRatio) {
5096             newSx = newSy = Math.min(newSx, newSy);
5097         }
5098
5099         // snap scale to a grid
5100         if (opt.scaleGrid) {
5101
5102             var gridSize = opt.scaleGrid;
5103
5104             newSx = gridSize * Math.floor(newSx / gridSize);
5105             newSy = gridSize * Math.floor(newSy / gridSize);
5106         }
5107
5108         // scale min/max boundaries
5109         newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx));
5110         newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy));
5111
5112         this.scale(newSx, newSy);
5113
5114         var contentTranslation = this.getContentBBox();
5115
5116         var newOx = fittingBBox.x - contentTranslation.x;
5117         var newOy = fittingBBox.y - contentTranslation.y;
5118
5119         this.setOrigin(newOx, newOy);
5120     },
5121
5122     getContentBBox: function() {
5123
5124         var crect = this.viewport.getBoundingClientRect();
5125
5126         // Using Screen CTM was the only way to get the real viewport bounding box working in both
5127         // Google Chrome and Firefox.
5128         var screenCTM = this.viewport.getScreenCTM();
5129
5130         // for non-default origin we need to take the viewport translation into account
5131         var viewportCTM = this.viewport.getCTM();
5132
5133         var bbox = g.rect({
5134             x: crect.left - screenCTM.e + viewportCTM.e,
5135             y: crect.top - screenCTM.f + viewportCTM.f,
5136             width: crect.width,
5137             height: crect.height
5138         });
5139
5140         return bbox;
5141     },
5142
5143     createViewForModel: function(cell) {
5144
5145         var view;
5146
5147         var type = cell.get('type');
5148         var module = type.split('.')[0];
5149         var entity = type.split('.')[1];
5150
5151         // If there is a special view defined for this model, use that one instead of the default `elementView`/`linkView`.
5152         if (joint.shapes[module] && joint.shapes[module][entity + 'View']) {
5153
5154             view = new joint.shapes[module][entity + 'View']({ model: cell, interactive: this.options.interactive });
5155
5156         } else if (cell instanceof joint.dia.Element) {
5157
5158             view = new this.options.elementView({ model: cell, interactive: this.options.interactive });
5159
5160         } else {
5161
5162             view = new this.options.linkView({ model: cell, interactive: this.options.interactive });
5163         }
5164
5165         return view;
5166     },
5167
5168     onAddCell: function(cell, graph, options) {
5169
5170         if (this.options.async && options.async !== false && _.isNumber(options.position)) {
5171
5172             this._asyncCells = this._asyncCells || [];
5173             this._asyncCells.push(cell);
5174
5175             if (options.position == 0) {
5176
5177                 if (this._frameId) throw 'another asynchronous rendering in progress';
5178
5179                 this.asyncRenderCells(this._asyncCells);
5180                 delete this._asyncCells;
5181             }
5182
5183         } else {
5184
5185             this.addCell(cell);
5186         }
5187     },
5188
5189     addCell: function(cell) {
5190
5191         var view = this.createViewForModel(cell);
5192
5193         V(this.viewport).append(view.el);
5194         view.paper = this;
5195         view.render();
5196
5197         // This is the only way to prevent image dragging in Firefox that works.
5198         // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
5199         $(view.el).find('image').on('dragstart', function() { return false; });
5200     },
5201
5202     beforeRenderCells: function(cells) {
5203
5204         // Make sure links are always added AFTER elements.
5205         // They wouldn't find their sources/targets in the DOM otherwise.
5206         cells.sort(function(a, b) { return a instanceof joint.dia.Link ? 1 : -1; });
5207
5208         return cells;
5209     },
5210
5211     afterRenderCells: function() {
5212
5213         this.sortCells();
5214     },
5215
5216     resetCells: function(cellsCollection, opt) {
5217
5218         $(this.viewport).empty();
5219
5220         var cells = cellsCollection.models.slice();
5221
5222         cells = this.beforeRenderCells(cells, opt);
5223
5224         if (this._frameId) {
5225
5226             joint.util.cancelFrame(this._frameId);
5227             delete this._frameId;
5228         }
5229
5230         if (this.options.async) {
5231
5232             this.asyncRenderCells(cells, opt);
5233             // Sort the cells once all elements rendered (see asyncRenderCells()).
5234
5235         } else {
5236
5237             _.each(cells, this.addCell, this);
5238
5239             // Sort the cells in the DOM manually as we might have changed the order they
5240             // were added to the DOM (see above).
5241             this.sortCells();
5242         }
5243     },
5244
5245     removeCells: function() {
5246
5247         this.model.get('cells').each(function(cell) {
5248             var view = this.findViewByModel(cell);
5249             view && view.remove();
5250         }, this);
5251     },
5252
5253     asyncBatchAdded: _.identity,
5254
5255     asyncRenderCells: function(cells, opt) {
5256
5257         var done = false;
5258
5259         if (this._frameId) {
5260
5261             _.each(_.range(this.options.async && this.options.async.batchSize || 50), function() {
5262
5263                 var cell = cells.shift();
5264                 done = !cell;
5265                 if (!done) this.addCell(cell);
5266
5267             }, this);
5268
5269             this.asyncBatchAdded();
5270         }
5271
5272         if (done) {
5273
5274             delete this._frameId;
5275             this.afterRenderCells(opt);
5276             this.trigger('render:done', opt);
5277
5278         } else {
5279
5280             this._frameId = joint.util.nextFrame(_.bind(function() {
5281                 this.asyncRenderCells(cells, opt);
5282             }, this));
5283         }
5284     },
5285
5286     sortCells: function() {
5287
5288         // Run insertion sort algorithm in order to efficiently sort DOM elements according to their
5289         // associated model `z` attribute.
5290
5291         var $cells = $(this.viewport).children('[model-id]');
5292         var cells = this.model.get('cells');
5293
5294         this.sortElements($cells, function(a, b) {
5295
5296             var cellA = cells.get($(a).attr('model-id'));
5297             var cellB = cells.get($(b).attr('model-id'));
5298
5299             return (cellA.get('z') || 0) > (cellB.get('z') || 0) ? 1 : -1;
5300         });
5301     },
5302
5303     // Highly inspired by the jquery.sortElements plugin by Padolsey.
5304     // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/.
5305     sortElements: function(elements, comparator) {
5306
5307         var $elements = $(elements);
5308
5309         var placements = $elements.map(function() {
5310
5311             var sortElement = this;
5312             var parentNode = sortElement.parentNode;
5313
5314             // Since the element itself will change position, we have
5315             // to have some way of storing it's original position in
5316             // the DOM. The easiest way is to have a 'flag' node:
5317             var nextSibling = parentNode.insertBefore(
5318                 document.createTextNode(''),
5319                 sortElement.nextSibling
5320             );
5321
5322             return function() {
5323
5324                 if (parentNode === this) {
5325                     throw new Error(
5326                         "You can't sort elements if any one is a descendant of another."
5327                     );
5328                 }
5329
5330                 // Insert before flag:
5331                 parentNode.insertBefore(this, nextSibling);
5332                 // Remove flag:
5333                 parentNode.removeChild(nextSibling);
5334
5335             };
5336         });
5337
5338         return Array.prototype.sort.call($elements, comparator).each(function(i) {
5339             placements[i].call(this);
5340         });
5341     },
5342
5343     scale: function(sx, sy, ox, oy) {
5344
5345         sy = sy || sx;
5346
5347         if (_.isUndefined(ox)) {
5348
5349             ox = 0;
5350             oy = 0;
5351         }
5352
5353         // Remove previous transform so that the new scale is not affected by previous scales, especially
5354         // the old translate() does not affect the new translate if an origin is specified.
5355         V(this.viewport).attr('transform', '');
5356
5357         var oldTx = this.options.origin.x;
5358         var oldTy = this.options.origin.y;
5359
5360         // TODO: V.scale() doesn't support setting scale origin. #Fix
5361         if (ox || oy || oldTx || oldTy) {
5362
5363             var newTx = oldTx - ox * (sx - 1);
5364             var newTy = oldTy - oy * (sy - 1);
5365             this.setOrigin(newTx, newTy);
5366         }
5367
5368         V(this.viewport).scale(sx, sy);
5369
5370         this.trigger('scale', sx, sy, ox, oy);
5371
5372         return this;
5373     },
5374
5375     rotate: function(deg, ox, oy) {
5376
5377         // If the origin is not set explicitely, rotate around the center. Note that
5378         // we must use the plain bounding box (`this.el.getBBox()` instead of the one that gives us
5379         // the real bounding box (`bbox()`) including transformations).
5380         if (_.isUndefined(ox)) {
5381
5382             var bbox = this.viewport.getBBox();
5383             ox = bbox.width / 2;
5384             oy = bbox.height / 2;
5385         }
5386
5387         V(this.viewport).rotate(deg, ox, oy);
5388     },
5389
5390     // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
5391     // be a selector or a jQuery object.
5392     findView: function(el) {
5393
5394         var $el = this.$(el);
5395
5396         if ($el.length === 0 || $el[0] === this.el) {
5397
5398             return undefined;
5399         }
5400
5401         return $el.data('view') || this.findView($el.parent());
5402     },
5403
5404     // Find a view for a model `cell`. `cell` can also be a string representing a model `id`.
5405     findViewByModel: function(cell) {
5406
5407         var id = _.isString(cell) ? cell : cell.id;
5408         var $view = this.$('[model-id="' + id + '"]');
5409
5410         return $view.length ? $view.data('view') : undefined;
5411     },
5412
5413     // Find all views at given point
5414     findViewsFromPoint: function(p) {
5415
5416         p = g.point(p);
5417
5418         var views = _.map(this.model.getElements(), this.findViewByModel);
5419
5420         return _.filter(views, function(view) {
5421             return view && g.rect(V(view.el).bbox(false, this.viewport)).containsPoint(p);
5422         }, this);
5423     },
5424
5425     // Find all views in given area
5426     findViewsInArea: function(r) {
5427
5428         r = g.rect(r);
5429
5430         var views = _.map(this.model.getElements(), this.findViewByModel);
5431
5432         return _.filter(views, function(view) {
5433             return view && r.intersect(g.rect(V(view.el).bbox(false, this.viewport)));
5434         }, this);
5435     },
5436
5437     getModelById: function(id) {
5438
5439         return this.model.getCell(id);
5440     },
5441
5442     snapToGrid: function(p) {
5443
5444         // Convert global coordinates to the local ones of the `viewport`. Otherwise,
5445         // improper transformation would be applied when the viewport gets transformed (scaled/rotated).
5446         var localPoint = V(this.viewport).toLocalPoint(p.x, p.y);
5447
5448         return {
5449             x: g.snapToGrid(localPoint.x, this.options.gridSize),
5450             y: g.snapToGrid(localPoint.y, this.options.gridSize)
5451         };
5452     },
5453
5454     // Transform client coordinates to the paper local coordinates.
5455     // Useful when you have a mouse event object and you'd like to get coordinates
5456     // inside the paper that correspond to `evt.clientX` and `evt.clientY` point.
5457     // Exmaple: var paperPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY });
5458     clientToLocalPoint: function(p) {
5459
5460         var svgPoint = this.svg.createSVGPoint();
5461         svgPoint.x = p.x;
5462         svgPoint.y = p.y;
5463
5464         // This is a hack for Firefox! If there wasn't a fake (non-visible) rectangle covering the
5465         // whole SVG area, `$(paper.svg).offset()` used below won't work.
5466         var fakeRect = V('rect', { width: this.options.width, height: this.options.height, x: 0, y: 0, opacity: 0 });
5467         V(this.svg).prepend(fakeRect);
5468
5469         var paperOffset = $(this.svg).offset();
5470
5471         // Clean up the fake rectangle once we have the offset of the SVG document.
5472         fakeRect.remove();
5473
5474         var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
5475         var scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
5476
5477         svgPoint.x += scrollLeft - paperOffset.left;
5478         svgPoint.y += scrollTop - paperOffset.top;
5479
5480         // Transform point into the viewport coordinate system.
5481         var pointTransformed = svgPoint.matrixTransform(this.viewport.getCTM().inverse());
5482
5483         return pointTransformed;
5484     },
5485
5486     getDefaultLink: function(cellView, magnet) {
5487
5488         return _.isFunction(this.options.defaultLink)
5489         // default link is a function producing link model
5490             ? this.options.defaultLink.call(this, cellView, magnet)
5491         // default link is the Backbone model
5492             : this.options.defaultLink.clone();
5493     },
5494
5495     // Cell highlighting
5496     // -----------------
5497
5498     onCellHighlight: function(cellView, el) {
5499         V(el).addClass('highlighted');
5500     },
5501
5502     onCellUnhighlight: function(cellView, el) {
5503         V(el).removeClass('highlighted');
5504     },
5505
5506     // Interaction.
5507     // ------------
5508
5509     mousedblclick: function(evt) {
5510
5511         evt.preventDefault();
5512         evt = joint.util.normalizeEvent(evt);
5513
5514         var view = this.findView(evt.target);
5515         var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY });
5516
5517         if (view) {
5518
5519             view.pointerdblclick(evt, localPoint.x, localPoint.y);
5520
5521         } else {
5522
5523             this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y);
5524         }
5525     },
5526
5527     mouseclick: function(evt) {
5528
5529         // Trigger event when mouse not moved.
5530         if (!this._mousemoved) {
5531
5532             evt = joint.util.normalizeEvent(evt);
5533
5534             var view = this.findView(evt.target);
5535             var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY });
5536
5537             if (view) {
5538
5539                 view.pointerclick(evt, localPoint.x, localPoint.y);
5540
5541             } else {
5542
5543                 this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y);
5544             }
5545         }
5546
5547         this._mousemoved = false;
5548     },
5549
5550     pointerdown: function(evt) {
5551
5552         evt = joint.util.normalizeEvent(evt);
5553
5554         var view = this.findView(evt.target);
5555
5556         var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY });
5557
5558         if (view) {
5559
5560             this.sourceView = view;
5561
5562             view.pointerdown(evt, localPoint.x, localPoint.y);
5563
5564         } else {
5565
5566             this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y);
5567         }
5568     },
5569
5570     pointermove: function(evt) {
5571
5572         evt.preventDefault();
5573         evt = joint.util.normalizeEvent(evt);
5574
5575         if (this.sourceView) {
5576
5577             // Mouse moved.
5578             this._mousemoved = true;
5579
5580             var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY });
5581
5582             this.sourceView.pointermove(evt, localPoint.x, localPoint.y);
5583         }
5584     },
5585
5586     pointerup: function(evt) {
5587
5588         evt = joint.util.normalizeEvent(evt);
5589
5590         var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY });
5591
5592         if (this.sourceView) {
5593
5594             this.sourceView.pointerup(evt, localPoint.x, localPoint.y);
5595
5596             //"delete sourceView" occasionally throws an error in chrome (illegal access exception)
5597             this.sourceView = null;
5598
5599         } else {
5600
5601             this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y);
5602         }
5603     },
5604
5605     cellMouseover: function(evt) {
5606
5607         evt = joint.util.normalizeEvent(evt);
5608         var view = this.findView(evt.target);
5609         if (view) {
5610
5611             view.mouseover(evt);
5612         }
5613     },
5614
5615     cellMouseout: function(evt) {
5616
5617         evt = joint.util.normalizeEvent(evt);
5618         var view = this.findView(evt.target);
5619         if (view) {
5620
5621             view.mouseout(evt);
5622         }
5623     }
5624 });
5625
5626 //      JointJS library.
5627 //      (c) 2011-2013 client IO
5628
5629 joint.shapes.basic = {};
5630
5631 joint.shapes.basic.Generic = joint.dia.Element.extend({
5632
5633     defaults: joint.util.deepSupplement({
5634
5635         type: 'basic.Generic',
5636         attrs: {
5637             '.': { fill: '#FFFFFF', stroke: 'none' }
5638         }
5639
5640     }, joint.dia.Element.prototype.defaults)
5641 });
5642
5643 joint.shapes.basic.Rect = joint.shapes.basic.Generic.extend({
5644
5645     markup: '<g class="rotatable"><g class="scalable"><rect/></g><text/></g>',
5646
5647     defaults: joint.util.deepSupplement({
5648
5649         type: 'basic.Rect',
5650         attrs: {
5651             'rect': { fill: '#FFFFFF', stroke: 'black', width: 100, height: 60 },
5652             'text': { 'font-size': 14, text: '', 'ref-x': .5, 'ref-y': .5, ref: 'rect', 'y-alignment': 'middle', 'x-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5653         }
5654
5655     }, joint.shapes.basic.Generic.prototype.defaults)
5656 });
5657
5658 joint.shapes.basic.TextView = joint.dia.ElementView.extend({
5659
5660     initialize: function() {
5661         joint.dia.ElementView.prototype.initialize.apply(this, arguments);
5662         // The element view is not automatically rescaled to fit the model size
5663         // when the attribute 'attrs' is changed.
5664         this.listenTo(this.model, 'change:attrs', this.resize);
5665     }
5666 });
5667
5668 joint.shapes.basic.Text = joint.shapes.basic.Generic.extend({
5669
5670     markup: '<g class="rotatable"><g class="scalable"><text/></g></g>',
5671
5672     defaults: joint.util.deepSupplement({
5673
5674         type: 'basic.Text',
5675         attrs: {
5676             'text': { 'font-size': 18, fill: 'black' }
5677         }
5678
5679     }, joint.shapes.basic.Generic.prototype.defaults)
5680 });
5681
5682 joint.shapes.basic.Circle = joint.shapes.basic.Generic.extend({
5683
5684     markup: '<g class="rotatable"><g class="scalable"><circle/></g><text/></g>',
5685
5686     defaults: joint.util.deepSupplement({
5687
5688         type: 'basic.Circle',
5689         size: { width: 60, height: 60 },
5690         attrs: {
5691             'circle': { fill: '#FFFFFF', stroke: 'black', r: 30, transform: 'translate(30, 30)' },
5692             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': .5, ref: 'circle', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5693         }
5694     }, joint.shapes.basic.Generic.prototype.defaults)
5695 });
5696
5697 joint.shapes.basic.Ellipse = joint.shapes.basic.Generic.extend({
5698
5699     markup: '<g class="rotatable"><g class="scalable"><ellipse/></g><text/></g>',
5700
5701     defaults: joint.util.deepSupplement({
5702
5703         type: 'basic.Ellipse',
5704         size: { width: 60, height: 40 },
5705         attrs: {
5706             'ellipse': { fill: '#FFFFFF', stroke: 'black', rx: 30, ry: 20, transform: 'translate(30, 20)' },
5707             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': .5, ref: 'ellipse', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5708         }
5709     }, joint.shapes.basic.Generic.prototype.defaults)
5710 });
5711
5712 joint.shapes.basic.Polygon = joint.shapes.basic.Generic.extend({
5713
5714     markup: '<g class="rotatable"><g class="scalable"><polygon/></g><text/></g>',
5715
5716     defaults: joint.util.deepSupplement({
5717
5718         type: 'basic.Polygon',
5719         size: { width: 60, height: 40 },
5720         attrs: {
5721             'polygon': { fill: '#FFFFFF', stroke: 'black' },
5722             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, ref: 'polygon', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5723         }
5724     }, joint.shapes.basic.Generic.prototype.defaults)
5725 });
5726
5727 joint.shapes.basic.Polyline = joint.shapes.basic.Generic.extend({
5728
5729     markup: '<g class="rotatable"><g class="scalable"><polyline/></g><text/></g>',
5730
5731     defaults: joint.util.deepSupplement({
5732
5733         type: 'basic.Polyline',
5734         size: { width: 60, height: 40 },
5735         attrs: {
5736             'polyline': { fill: '#FFFFFF', stroke: 'black' },
5737             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, ref: 'polyline', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5738         }
5739     }, joint.shapes.basic.Generic.prototype.defaults)
5740 });
5741
5742 joint.shapes.basic.Image = joint.shapes.basic.Generic.extend({
5743
5744     markup: '<g class="rotatable"><g class="scalable"><image/></g><text/></g>',
5745
5746     defaults: joint.util.deepSupplement({
5747
5748         type: 'basic.Image',
5749         attrs: {
5750             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, ref: 'image', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5751         }
5752     }, joint.shapes.basic.Generic.prototype.defaults)
5753 });
5754
5755 joint.shapes.basic.Path = joint.shapes.basic.Generic.extend({
5756
5757     markup: '<g class="rotatable"><g class="scalable"><path/></g><text/></g>',
5758
5759     defaults: joint.util.deepSupplement({
5760
5761         type: 'basic.Path',
5762         size: { width: 60, height: 60 },
5763         attrs: {
5764             'path': { fill: '#FFFFFF', stroke: 'black' },
5765             'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, ref: 'path', 'y-alignment': 'middle', fill: 'black', 'font-family': 'Arial, helvetica, sans-serif' }
5766         }
5767     }, joint.shapes.basic.Generic.prototype.defaults)
5768 });
5769
5770 joint.shapes.basic.Rhombus = joint.shapes.basic.Path.extend({
5771
5772     defaults: joint.util.deepSupplement({
5773
5774         type: 'basic.Rhombus',
5775         attrs: {
5776             'path': { d: 'M 30 0 L 60 30 30 60 0 30 z' },
5777             'text': { 'ref-y': .5 }
5778         }
5779
5780     }, joint.shapes.basic.Path.prototype.defaults)
5781 });
5782
5783
5784 // PortsModelInterface is a common interface for shapes that have ports. This interface makes it easy
5785 // to create new shapes with ports functionality. It is assumed that the new shapes have
5786 // `inPorts` and `outPorts` array properties. Only these properties should be used to set ports.
5787 // In other words, using this interface, it is no longer recommended to set ports directly through the
5788 // `attrs` object.
5789
5790 // Usage:
5791 // joint.shapes.custom.MyElementWithPorts = joint.shapes.basic.Path.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, {
5792 //     getPortAttrs: function(portName, index, total, selector, type) {
5793 //         var attrs = {};
5794 //         var portClass = 'port' + index;
5795 //         var portSelector = selector + '>.' + portClass;
5796 //         var portTextSelector = portSelector + '>text';
5797 //         var portCircleSelector = portSelector + '>circle';
5798 //
5799 //         attrs[portTextSelector] = { text: portName };
5800 //         attrs[portCircleSelector] = { port: { id: portName || _.uniqueId(type) , type: type } };
5801 //         attrs[portSelector] = { ref: 'rect', 'ref-y': (index + 0.5) * (1 / total) };
5802 //
5803 //         if (selector === '.outPorts') { attrs[portSelector]['ref-dx'] = 0; }
5804 //
5805 //         return attrs;
5806 //     }
5807 //}));
5808 joint.shapes.basic.PortsModelInterface = {
5809
5810     initialize: function() {
5811
5812         this.updatePortsAttrs();
5813         this.on('change:inPorts change:outPorts', this.updatePortsAttrs, this);
5814
5815         // Call the `initialize()` of the parent.
5816         this.constructor.__super__.constructor.__super__.initialize.apply(this, arguments);
5817     },
5818
5819     updatePortsAttrs: function(eventName) {
5820
5821         // Delete previously set attributes for ports.
5822         var currAttrs = this.get('attrs');
5823         _.each(this._portSelectors, function(selector) {
5824             if (currAttrs[selector]) delete currAttrs[selector];
5825         });
5826
5827         // This holds keys to the `attrs` object for all the port specific attribute that
5828         // we set in this method. This is necessary in order to remove previously set
5829         // attributes for previous ports.
5830         this._portSelectors = [];
5831
5832         var attrs = {};
5833
5834         _.each(this.get('inPorts'), function(portName, index, ports) {
5835             var portAttributes = this.getPortAttrs(portName, index, ports.length, '.inPorts', 'in');
5836             this._portSelectors = this._portSelectors.concat(_.keys(portAttributes));
5837             _.extend(attrs, portAttributes);
5838         }, this);
5839
5840         _.each(this.get('outPorts'), function(portName, index, ports) {
5841             var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out');
5842             this._portSelectors = this._portSelectors.concat(_.keys(portAttributes));
5843             _.extend(attrs, portAttributes);
5844         }, this);
5845
5846         // Silently set `attrs` on the cell so that noone knows the attrs have changed. This makes sure
5847         // that, for example, command manager does not register `change:attrs` command but only
5848         // the important `change:inPorts`/`change:outPorts` command.
5849         this.attr(attrs, { silent: true });
5850         // Manually call the `processPorts()` method that is normally called on `change:attrs` (that we just made silent).
5851         this.processPorts();
5852         // Let the outside world (mainly the `ModelView`) know that we're done configuring the `attrs` object.
5853         this.trigger('process:ports');
5854     },
5855
5856     getPortSelector: function(name) {
5857
5858         var selector = '.inPorts';
5859         var index = this.get('inPorts').indexOf(name);
5860
5861         if (index < 0) {
5862             selector = '.outPorts';
5863             index = this.get('outPorts').indexOf(name);
5864
5865             if (index < 0) throw new Error("getPortSelector(): Port doesn't exist.");
5866         }
5867
5868         return selector + '>g:nth-child(' + (index + 1) + ')>circle';
5869     }
5870 };
5871
5872 joint.shapes.basic.PortsViewInterface = {
5873
5874     initialize: function() {
5875
5876         // `Model` emits the `process:ports` whenever it's done configuring the `attrs` object for ports.
5877         this.listenTo(this.model, 'process:ports', this.update);
5878
5879         joint.dia.ElementView.prototype.initialize.apply(this, arguments);
5880     },
5881
5882     update: function() {
5883
5884         // First render ports so that `attrs` can be applied to those newly created DOM elements
5885         // in `ElementView.prototype.update()`.
5886         this.renderPorts();
5887         joint.dia.ElementView.prototype.update.apply(this, arguments);
5888     },
5889
5890     renderPorts: function() {
5891
5892         var $inPorts = this.$('.inPorts').empty();
5893         var $outPorts = this.$('.outPorts').empty();
5894
5895         var portTemplate = _.template(this.model.portMarkup);
5896
5897         _.each(_.filter(this.model.ports, function(p) { return p.type === 'in'; }), function(port, index) {
5898
5899             $inPorts.append(V(portTemplate({ id: index, port: port })).node);
5900         });
5901         _.each(_.filter(this.model.ports, function(p) { return p.type === 'out'; }), function(port, index) {
5902
5903             $outPorts.append(V(portTemplate({ id: index, port: port })).node);
5904         });
5905     }
5906 };
5907
5908 joint.shapes.basic.TextBlock = joint.shapes.basic.Generic.extend({
5909
5910     markup: ['<g class="rotatable"><g class="scalable"><rect/></g><switch>',
5911
5912              // if foreignObject supported
5913
5914              '<foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" class="fobj">',
5915              '<body xmlns="http://www.w3.org/1999/xhtml"><div/></body>',
5916              '</foreignObject>',
5917
5918              // else foreignObject is not supported (fallback for IE)
5919              '<text class="content"/>',
5920
5921              '</switch></g>'].join(''),
5922
5923     defaults: joint.util.deepSupplement({
5924
5925         type: 'basic.TextBlock',
5926
5927         // see joint.css for more element styles
5928         attrs: {
5929             rect: {
5930                 fill: '#ffffff',
5931                 stroke: '#000000',
5932                 width: 80,
5933                 height: 100
5934             },
5935             text: {
5936                 fill: '#000000',
5937                 'font-size': 14,
5938                 'font-family': 'Arial, helvetica, sans-serif'
5939             },
5940             '.content': {
5941                 text: '',
5942                 ref: 'rect',
5943                 'ref-x': .5,
5944                 'ref-y': .5,
5945                 'y-alignment': 'middle',
5946                 'x-alignment': 'middle'
5947             }
5948         },
5949
5950         content: ''
5951
5952     }, joint.shapes.basic.Generic.prototype.defaults),
5953
5954     initialize: function() {
5955
5956         if (typeof SVGForeignObjectElement !== 'undefined') {
5957
5958             // foreignObject supported
5959             this.setForeignObjectSize(this, this.get('size'));
5960             this.setDivContent(this, this.get('content'));
5961             this.listenTo(this, 'change:size', this.setForeignObjectSize);
5962             this.listenTo(this, 'change:content', this.setDivContent);
5963
5964         }
5965
5966         joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments);
5967     },
5968
5969     setForeignObjectSize: function(cell, size) {
5970
5971         // Selector `foreignObject' doesn't work accross all browsers, we'r using class selector instead.
5972         // We have to clone size as we don't want attributes.div.style to be same object as attributes.size.
5973         cell.attr({
5974             '.fobj': _.clone(size),
5975             div: { style: _.clone(size) }
5976         });
5977     },
5978
5979     setDivContent: function(cell, content) {
5980
5981         // Append the content to div as html.
5982         cell.attr({ div : {
5983             html: content
5984         }});
5985     }
5986
5987 });
5988
5989 // TextBlockView implements the fallback for IE when no foreignObject exists and
5990 // the text needs to be manually broken.
5991 joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({
5992
5993     initialize: function() {
5994
5995         joint.dia.ElementView.prototype.initialize.apply(this, arguments);
5996
5997         if (typeof SVGForeignObjectElement === 'undefined') {
5998
5999             this.noSVGForeignObjectElement = true;
6000
6001             this.listenTo(this.model, 'change:content', function(cell) {
6002                 // avoiding pass of extra paramters
6003                 this.updateContent(cell);
6004             });
6005         }
6006     },
6007
6008     update: function(cell, renderingOnlyAttrs) {
6009
6010         if (this.noSVGForeignObjectElement) {
6011
6012             var model = this.model;
6013
6014             // Update everything but the content first.
6015             var noTextAttrs = _.omit(renderingOnlyAttrs || model.get('attrs'), '.content');
6016             joint.dia.ElementView.prototype.update.call(this, model, noTextAttrs);
6017
6018             if (!renderingOnlyAttrs || _.has(renderingOnlyAttrs, '.content')) {
6019                 // Update the content itself.
6020                 this.updateContent(model, renderingOnlyAttrs);
6021             }
6022
6023         } else {
6024
6025             joint.dia.ElementView.prototype.update.call(this, model, renderingOnlyAttrs);
6026         }
6027     },
6028
6029     updateContent: function(cell, renderingOnlyAttrs) {
6030
6031         // Create copy of the text attributes
6032         var textAttrs = _.merge({}, (renderingOnlyAttrs || cell.get('attrs'))['.content']);
6033
6034         delete textAttrs.text;
6035
6036         // Break the content to fit the element size taking into account the attributes
6037         // set on the model.
6038         var text = joint.util.breakText(cell.get('content'), cell.get('size'), textAttrs, {
6039             // measuring sandbox svg document
6040             svgDocument: this.paper.svg
6041         });
6042
6043         // Create a new attrs with same structure as the model attrs { text: { *textAttributes* }}
6044         var attrs = joint.util.setByPath({}, '.content', textAttrs, '/');
6045
6046         // Replace text attribute with the one we just processed.
6047         attrs['.content'].text = text;
6048
6049         // Update the view using renderingOnlyAttributes parameter.
6050         joint.dia.ElementView.prototype.update.call(this, cell, attrs);
6051     }
6052 });
6053
6054 joint.routers.orthogonal = (function() {
6055
6056     // bearing -> opposite bearing
6057     var opposite = {
6058         N: 'S',
6059         S: 'N',
6060         E: 'W',
6061         W: 'E'
6062     };
6063
6064     // bearing -> radians
6065     var radians = {
6066         N: -Math.PI / 2 * 3,
6067         S: -Math.PI / 2,
6068         E: 0,
6069         W: Math.PI
6070     };
6071
6072     // HELPERS //
6073
6074     // simple bearing method (calculates only orthogonal cardinals)
6075     function bearing(from, to) {
6076         if (from.x == to.x) return from.y > to.y ? 'N' : 'S';
6077         if (from.y == to.y) return from.x > to.x ? 'W' : 'E';
6078         return null;
6079     }
6080
6081     // returns either width or height of a bbox based on the given bearing
6082     function boxSize(bbox, brng) {
6083         return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height'];
6084     }
6085
6086     // expands a box by specific value
6087     function expand(bbox, val) {
6088         return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val });
6089     }
6090
6091     // transform point to a rect
6092     function pointBox(p) {
6093         return g.rect(p.x, p.y, 0, 0);
6094     }
6095
6096     // returns a minimal rect which covers the given boxes
6097     function boundary(bbox1, bbox2) {
6098
6099         var x1 = Math.min(bbox1.x, bbox2.x);
6100         var y1 = Math.min(bbox1.y, bbox2.y);
6101         var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width);
6102         var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height);
6103
6104         return g.rect(x1, y1, x2 - x1, y2 - y1);
6105     }
6106
6107     // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained
6108     // in the given box
6109     function freeJoin(p1, p2, bbox) {
6110
6111         var p = g.point(p1.x, p2.y);
6112         if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y);
6113         // kept for reference
6114         // if (bbox.containsPoint(p)) p = null;
6115         return p;
6116     }
6117
6118     // PARTIAL ROUTERS //
6119
6120     function vertexVertex(from, to, brng) {
6121
6122         var p1 = g.point(from.x, to.y);
6123         var p2 = g.point(to.x, from.y);
6124         var d1 = bearing(from, p1);
6125         var d2 = bearing(from, p2);
6126         var xBrng = opposite[brng];
6127
6128         var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2;
6129
6130         return { points: [p], direction: bearing(p, to) };
6131     }
6132
6133     function elementVertex(from, to, fromBBox) {
6134
6135         var p = freeJoin(from, to, fromBBox);
6136
6137         return { points: [p], direction: bearing(p, to) };
6138     }
6139
6140     function vertexElement(from, to, toBBox, brng) {
6141
6142         var route = {};
6143
6144         var pts = [g.point(from.x, to.y), g.point(to.x, from.y)];
6145         var freePts = _.filter(pts, function(pt) { return !toBBox.containsPoint(pt); });
6146         var freeBrngPts = _.filter(freePts, function(pt) { return bearing(pt, from) != brng; });
6147
6148         var p;
6149
6150         if (freeBrngPts.length > 0) {
6151
6152             // try to pick a point which bears the same direction as the previous segment
6153             p = _.filter(freeBrngPts, function(pt) { return bearing(from, pt) == brng; }).pop();
6154             p = p || freeBrngPts[0];
6155
6156             route.points = [p];
6157             route.direction = bearing(p, to);
6158
6159         } else {
6160
6161             // Here we found only points which are either contained in the element or they would create
6162             // a link segment going in opposite direction from the previous one.
6163             // We take the point inside element and move it outside the element in the direction the
6164             // route is going. Now we can join this point with the current end (using freeJoin).
6165
6166             p = _.difference(pts, freePts)[0];
6167
6168             var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2);
6169             var p1 = freeJoin(p2, from, toBBox);
6170
6171             route.points = [p1, p2];
6172             route.direction = bearing(p2, to);
6173         }
6174
6175         return route;
6176     }
6177
6178     function elementElement(from, to, fromBBox, toBBox) {
6179
6180         var route = elementVertex(to, from, toBBox);
6181         var p1 = route.points[0];
6182
6183         if (fromBBox.containsPoint(p1)) {
6184
6185             route = elementVertex(from, to, fromBBox);
6186             var p2 = route.points[0];
6187
6188             if (toBBox.containsPoint(p2)) {
6189
6190                 var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2);
6191                 var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2);
6192                 var mid = g.line(fromBorder, toBorder).midpoint();
6193
6194                 var startRoute = elementVertex(from, mid, fromBBox);
6195                 var endRoute = vertexVertex(mid, to, startRoute.direction);
6196
6197                 route.points = [startRoute.points[0], endRoute.points[0]];
6198                 route.direction = endRoute.direction;
6199             }
6200         }
6201
6202         return route;
6203     }
6204
6205     // Finds route for situations where one of end is inside the other.
6206     // Typically the route is conduct outside the outer element first and
6207     // let go back to the inner element.
6208     function insideElement(from, to, fromBBox, toBBox, brng) {
6209
6210         var route = {};
6211         var bndry = expand(boundary(fromBBox, toBBox), 1);
6212
6213         // start from the point which is closer to the boundary
6214         var reversed = bndry.center().distance(to) > bndry.center().distance(from);
6215         var start = reversed ? to : from;
6216         var end = reversed ? from : to;
6217
6218         var p1, p2, p3;
6219
6220         if (brng) {
6221             // Points on circle with radius equals 'W + H` are always outside the rectangle
6222             // with width W and height H if the center of that circle is the center of that rectangle.
6223             p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start);
6224             p1 = bndry.pointNearestToPoint(p1).move(p1, -1);
6225         } else {
6226             p1 = bndry.pointNearestToPoint(start).move(start, 1);
6227         }
6228
6229         p2 = freeJoin(p1, end, bndry);
6230
6231         if (p1.round().equals(p2.round())) {
6232             p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end);
6233             p2 = bndry.pointNearestToPoint(p2).move(end, 1).round();
6234             p3 = freeJoin(p1, p2, bndry);
6235             route.points = reversed ? [p2, p3, p1] : [p1, p3, p2];
6236         } else {
6237             route.points = reversed ? [p2, p1] : [p1, p2];
6238         }
6239
6240         route.direction = reversed ? bearing(p1, to) : bearing(p2, to);
6241
6242         return route;
6243     }
6244
6245     // MAIN ROUTER //
6246
6247     // Return points that one needs to draw a connection through in order to have a orthogonal link
6248     // routing from source to target going through `vertices`.
6249     function findOrthogonalRoute(vertices, opt, linkView) {
6250
6251         var padding = opt.elementPadding || 20;
6252
6253         var orthogonalVertices = [];
6254         var sourceBBox = expand(linkView.sourceBBox, padding);
6255         var targetBBox = expand(linkView.targetBBox, padding);
6256
6257         vertices = _.map(vertices, g.point);
6258         vertices.unshift(sourceBBox.center());
6259         vertices.push(targetBBox.center());
6260
6261         var brng;
6262
6263         for (var i = 0, max = vertices.length - 1; i < max; i++) {
6264
6265             var route = null;
6266             var from = vertices[i];
6267             var to = vertices[i + 1];
6268             var isOrthogonal = !!bearing(from, to);
6269
6270             if (i == 0) {
6271
6272                 if (i + 1 == max) { // route source -> target
6273
6274                     // Expand one of elements by 1px so we detect also situations when they
6275                     // are positioned one next other with no gap between.
6276                     if (sourceBBox.intersect(expand(targetBBox, 1))) {
6277                         route = insideElement(from, to, sourceBBox, targetBBox);
6278                     } else if (!isOrthogonal) {
6279                         route = elementElement(from, to, sourceBBox, targetBBox);
6280                     }
6281
6282                 } else { // route source -> vertex
6283
6284                     if (sourceBBox.containsPoint(to)) {
6285                         route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding));
6286                     } else if (!isOrthogonal) {
6287                         route = elementVertex(from, to, sourceBBox);
6288                     }
6289                 }
6290
6291             } else if (i + 1 == max) { // route vertex -> target
6292
6293                 var orthogonalLoop = isOrthogonal && bearing(to, from) == brng;
6294
6295                 if (targetBBox.containsPoint(from) || orthogonalLoop) {
6296                     route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng);
6297                 } else if (!isOrthogonal) {
6298                     route = vertexElement(from, to, targetBBox, brng);
6299                 }
6300
6301             } else if (!isOrthogonal) { // route vertex -> vertex
6302                 route = vertexVertex(from, to, brng);
6303             }
6304
6305             if (route) {
6306                 Array.prototype.push.apply(orthogonalVertices, route.points);
6307                 brng = route.direction;
6308             } else {
6309                 // orthogonal route and not looped
6310                 brng = bearing(from, to);
6311             }
6312
6313             if (i + 1 < max) {
6314                 orthogonalVertices.push(to);
6315             }
6316         }
6317
6318         return orthogonalVertices;
6319     };
6320
6321     return findOrthogonalRoute;
6322
6323 })();
6324
6325 joint.routers.manhattan = (function() {
6326
6327     'use strict';
6328
6329     var config = {
6330
6331         // size of the step to find a route
6332         step: 10,
6333
6334         // use of the perpendicular linkView option to connect center of element with first vertex
6335         perpendicular: true,
6336
6337         // tells how to divide the paper when creating the elements map
6338         mapGridSize: 100,
6339
6340         // should be source or target not to be consider as an obstacle
6341         excludeEnds: [], // 'source', 'target'
6342
6343         // should be any element with a certain type not to be consider as an obstacle
6344         excludeTypes: ['basic.Text'],
6345
6346         // if number of route finding loops exceed the maximum, stops searching and returns
6347         // fallback route
6348         maximumLoops: 500,
6349
6350         // possible starting directions from an element
6351         startDirections: ['left', 'right', 'top', 'bottom'],
6352
6353         // possible ending directions to an element
6354         endDirections: ['left', 'right', 'top', 'bottom'],
6355
6356         // specify directions above
6357         directionMap: {
6358             right: { x: 1, y: 0 },
6359             bottom: { x: 0, y: 1 },
6360             left: { x: -1, y: 0 },
6361             top: { x: 0, y: -1 }
6362         },
6363
6364         // maximum change of the direction
6365         maxAllowedDirectionChange: 1,
6366
6367         // padding applied on the element bounding boxes
6368         paddingBox: function() {
6369
6370             var step = this.step;
6371
6372             return {
6373                 x: -step,
6374                 y: -step,
6375                 width: 2 * step,
6376                 height: 2 * step
6377             };
6378         },
6379
6380         // an array of directions to find next points on the route
6381         directions: function() {
6382
6383             var step = this.step;
6384
6385             return [
6386                 { offsetX: step  , offsetY: 0     , cost: step },
6387                 { offsetX: 0     , offsetY: step  , cost: step },
6388                 { offsetX: -step , offsetY: 0     , cost: step },
6389                 { offsetX: 0     , offsetY: -step , cost: step }
6390             ];
6391         },
6392
6393         // a penalty received for direction change
6394         penalties: function() {
6395
6396             return [0, this.step / 2, this.step];
6397         },
6398
6399         // heurestic method to determine the distance between two points
6400         estimateCost: function(from, to) {
6401
6402             return from.manhattanDistance(to);
6403         },
6404
6405         // a simple route used in situations, when main routing method fails
6406         // (exceed loops, inaccessible).
6407         fallbackRoute: function(from, to, opts) {
6408
6409             // Find an orthogonal route ignoring obstacles.
6410
6411             var prevDirIndexes = opts.prevDirIndexes || {};
6412
6413             var point = (prevDirIndexes[from] || 0) % 2
6414                     ? g.point(from.x, to.y)
6415                     : g.point(to.x, from.y);
6416
6417             return [point, to];
6418         },
6419
6420         // if a function is provided, it's used to route the link while dragging an end
6421         // i.e. function(from, to, opts) { return []; }
6422         draggingRoute: null
6423     };
6424
6425     // reconstructs a route by concating points with their parents
6426     function reconstructRoute(parents, point) {
6427
6428         var route = [];
6429         var prevDiff = { x: 0, y: 0 };
6430         var current = point;
6431         var parent;
6432
6433         while ((parent = parents[current])) {
6434
6435             var diff = parent.difference(current);
6436
6437             if (!diff.equals(prevDiff)) {
6438
6439                 route.unshift(current);
6440                 prevDiff = diff;
6441             }
6442
6443             current = parent;
6444         }
6445
6446         route.unshift(current);
6447
6448         return route;
6449     };
6450
6451     // find points around the rectangle taking given directions in the account
6452     function getRectPoints(bbox, directionList, opts) {
6453
6454         var step = opts.step;
6455
6456         var center = bbox.center();
6457
6458         var startPoints = _.chain(opts.directionMap).pick(directionList).map(function(direction) {
6459
6460             var x = direction.x * bbox.width / 2;
6461             var y = direction.y * bbox.height / 2;
6462
6463             var point = g.point(center).offset(x, y).snapToGrid(step);
6464
6465             if (bbox.containsPoint(point)) {
6466
6467                 point.offset(direction.x * step, direction.y * step);
6468             }
6469
6470             return point;
6471
6472         }).value();
6473
6474         return startPoints;
6475     };
6476
6477     // returns a direction index from start point to end point
6478     function getDirection(start, end, dirLen) {
6479
6480         var dirAngle = 360 / dirLen;
6481
6482         var q = Math.floor(start.theta(end) / dirAngle);
6483
6484         return dirLen - q;
6485     }
6486
6487     // finds the route between to points/rectangles implementing A* alghoritm
6488     function findRoute(start, end, map, opt) {
6489
6490         var startDirections = opt.reversed ? opt.endDirections : opt.startDirections;
6491         var endDirections = opt.reversed ? opt.startDirections : opt.endDirections;
6492
6493         // set of points we start pathfinding from
6494         var startSet = start instanceof g.rect
6495                 ? getRectPoints(start, startDirections, opt)
6496                 : [start];
6497
6498         // set of points we want the pathfinding to finish at
6499         var endSet = end instanceof g.rect
6500                 ? getRectPoints(end, endDirections, opt)
6501                 : [end];
6502
6503         var startCenter = startSet.length > 1 ? start.center() : startSet[0];
6504         var endCenter = endSet.length > 1 ? end.center() : endSet[0];
6505
6506         // take into account  only accessible end points
6507         var endPoints = _.filter(endSet, function(point) {
6508
6509             var mapKey = g.point(point).snapToGrid(opt.mapGridSize).toString();
6510
6511             var accesible = _.every(map[mapKey], function(obstacle) {
6512                 return !obstacle.containsPoint(point);
6513             });
6514
6515             return accesible;
6516         });
6517
6518
6519         if (endPoints.length) {
6520
6521             var step = opt.step;
6522             var penalties = opt.penalties;
6523
6524             // choose the end point with the shortest estimated path cost
6525             var endPoint = _.chain(endPoints).invoke('snapToGrid', step).min(function(point) {
6526
6527                 return opt.estimateCost(startCenter, point);
6528
6529             }).value();
6530
6531             var parents = {};
6532             var costFromStart = {};
6533             var totalCost = {};
6534
6535             // directions
6536             var dirs = opt.directions;
6537             var dirLen = dirs.length;
6538             var dirHalfLen = dirLen / 2;
6539             var dirIndexes = opt.previousDirIndexes || {};
6540
6541             // The set of point already evaluated.
6542             var closeHash = {}; // keeps only information whether a point was evaluated'
6543
6544             // The set of tentative points to be evaluated, initially containing the start points
6545             var openHash = {}; // keeps only information whether a point is to be evaluated'
6546             var openSet = _.chain(startSet).invoke('snapToGrid', step).each(function(point) {
6547
6548                 var key = point.toString();
6549
6550                 costFromStart[key] = 0; // Cost from start along best known path.
6551                 totalCost[key] = opt.estimateCost(point, endPoint);
6552                 dirIndexes[key] = dirIndexes[key] || getDirection(startCenter, point, dirLen);
6553                 openHash[key] = true;
6554
6555             }).map(function(point) {
6556
6557                 return point.toString();
6558
6559             }).sortBy(function(pointKey) {
6560
6561                 return totalCost[pointKey];
6562
6563             }).value();
6564
6565             var loopCounter = opt.maximumLoops;
6566
6567             var maxAllowedDirectionChange = opt.maxAllowedDirectionChange;
6568
6569             // main route finding loop
6570             while (openSet.length && loopCounter--) {
6571
6572                 var currentKey = openSet[0];
6573                 var currentPoint = g.point(currentKey);
6574
6575                 if (endPoint.equals(currentPoint)) {
6576
6577                     opt.previousDirIndexes = _.pick(dirIndexes, currentKey);
6578                     return reconstructRoute(parents, currentPoint);
6579                 }
6580
6581                 // remove current from the open list
6582                 openSet.splice(0, 1);
6583                 openHash[neighborKey] = null;
6584
6585                 // add current to the close list
6586                 closeHash[neighborKey] = true;
6587
6588                 var currentDirIndex = dirIndexes[currentKey];
6589                 var currentDist = costFromStart[currentKey];
6590
6591                 for (var dirIndex = 0; dirIndex < dirLen; dirIndex++) {
6592
6593                     var dirChange = Math.abs(dirIndex - currentDirIndex);
6594
6595                     if (dirChange > dirHalfLen) {
6596
6597                         dirChange = dirLen - dirChange;
6598                     }
6599
6600                     // if the direction changed rapidly don't use this point
6601                     if (dirChange > maxAllowedDirectionChange) {
6602
6603                         continue;
6604                     }
6605
6606                     var dir = dirs[dirIndex];
6607
6608                     var neighborPoint = g.point(currentPoint).offset(dir.offsetX, dir.offsetY);
6609                     var neighborKey = neighborPoint.toString();
6610
6611                     if (closeHash[neighborKey]) {
6612
6613                         continue;
6614                     }
6615
6616                     // is point accesible - no obstacle in the way
6617
6618                     var mapKey = g.point(neighborPoint).snapToGrid(opt.mapGridSize).toString();
6619
6620                     var isAccesible = _.every(map[mapKey], function(obstacle) {
6621                         return !obstacle.containsPoint(neighborPoint);
6622                     });
6623
6624                     if (!isAccesible) {
6625
6626                         continue;
6627                     }
6628
6629                     var inOpenSet = _.has(openHash, neighborKey);
6630
6631                     var costToNeighbor = currentDist + dir.cost;
6632
6633                     if (!inOpenSet || costToNeighbor < costFromStart[neighborKey]) {
6634
6635                         parents[neighborKey] = currentPoint;
6636                         dirIndexes[neighborKey] = dirIndex;
6637                         costFromStart[neighborKey] = costToNeighbor;
6638
6639                         totalCost[neighborKey] = costToNeighbor +
6640                             opt.estimateCost(neighborPoint, endPoint) +
6641                             penalties[dirChange];
6642
6643                         if (!inOpenSet) {
6644
6645                             var openIndex = _.sortedIndex(openSet, neighborKey, function(openKey) {
6646
6647                                 return totalCost[openKey];
6648                             });
6649
6650                             openSet.splice(openIndex, 0, neighborKey);
6651                             openHash[neighborKey] = true;
6652                         }
6653                     };
6654                 };
6655             }
6656         }
6657
6658         // no route found ('to' point wasn't either accessible or finding route took
6659         // way to much calculations)
6660         return opt.fallbackRoute(startCenter, endCenter, opt);
6661     };
6662
6663     // initiation of the route finding
6664     function router(oldVertices, opt) {
6665
6666         // resolve some of the options
6667         opt.directions = _.result(opt, 'directions');
6668         opt.penalties = _.result(opt, 'penalties');
6669         opt.paddingBox = _.result(opt, 'paddingBox');
6670
6671         // enable/disable linkView perpendicular option
6672         this.options.perpendicular = !!opt.perpendicular;
6673
6674         // As route changes its shape rapidly when we start finding route from different point
6675         // it's necessary to start from the element that was not interacted with
6676         // (the position was changed) at very last.
6677         var reverseRouting = opt.reversed = (this.lastEndChange === 'source');
6678
6679         var sourceBBox = reverseRouting ? g.rect(this.targetBBox) : g.rect(this.sourceBBox);
6680         var targetBBox = reverseRouting ? g.rect(this.sourceBBox) : g.rect(this.targetBBox);
6681
6682         // expand boxes by specific padding
6683         sourceBBox.moveAndExpand(opt.paddingBox);
6684         targetBBox.moveAndExpand(opt.paddingBox);
6685
6686         // building an elements map
6687
6688         var link = this.model;
6689         var graph = this.paper.model;
6690
6691         // source or target element could be excluded from set of obstacles
6692         var excludedEnds = _.chain(opt.excludeEnds)
6693                 .map(link.get, link)
6694                 .pluck('id')
6695                 .map(graph.getCell, graph).value();
6696
6697         var mapGridSize = opt.mapGridSize;
6698
6699         var excludeAncestors = [];
6700
6701         var sourceId = link.get('source').id;
6702         if (sourceId !== undefined) {
6703             var source = graph.getCell(sourceId);
6704             if (source !== undefined) {
6705                 excludeAncestors = _.union(excludeAncestors, _.map(source.getAncestors(), 'id'));
6706             };
6707         }
6708
6709         var targetId = link.get('target').id;
6710         if (targetId !== undefined) {
6711             var target = graph.getCell(targetId);
6712             if (target !== undefined) {
6713                 excludeAncestors = _.union(excludeAncestors, _.map(target.getAncestors(), 'id'));
6714             }
6715         }
6716
6717         // builds a map of all elements for quicker obstacle queries (i.e. is a point contained
6718         // in any obstacle?) (a simplified grid search)
6719         // The paper is divided to smaller cells, where each of them holds an information which
6720         // elements belong to it. When we query whether a point is in an obstacle we don't need
6721         // to go through all obstacles, we check only those in a particular cell.
6722         var map = _.chain(graph.getElements())
6723             // remove source and target element if required
6724             .difference(excludedEnds)
6725             // remove all elements whose type is listed in excludedTypes array
6726             .reject(function(element) {
6727                 // reject any element which is an ancestor of either source or target
6728                 return _.contains(opt.excludeTypes, element.get('type')) || _.contains(excludeAncestors, element.id);
6729             })
6730             // change elements (models) to their bounding boxes
6731             .invoke('getBBox')
6732             // expand their boxes by specific padding
6733             .invoke('moveAndExpand', opt.paddingBox)
6734             // build the map
6735             .foldl(function(res, bbox) {
6736
6737                 var origin = bbox.origin().snapToGrid(mapGridSize);
6738                 var corner = bbox.corner().snapToGrid(mapGridSize);
6739
6740                 for (var x = origin.x; x <= corner.x; x += mapGridSize) {
6741                     for (var y = origin.y; y <= corner.y; y += mapGridSize) {
6742
6743                         var gridKey = x + '@' + y;
6744
6745                         res[gridKey] = res[gridKey] || [];
6746                         res[gridKey].push(bbox);
6747                     }
6748                 }
6749
6750                 return res;
6751
6752             }, {}).value();
6753
6754         // pathfinding
6755
6756         var newVertices = [];
6757
6758         var points = _.map(oldVertices, g.point);
6759
6760         var tailPoint = sourceBBox.center();
6761
6762         // find a route by concating all partial routes (routes need to go through the vertices)
6763         // startElement -> vertex[1] -> ... -> vertex[n] -> endElement
6764         for (var i = 0, len = points.length; i <= len; i++) {
6765
6766             var partialRoute = null;
6767
6768             var from = to || sourceBBox;
6769             var to = points[i];
6770
6771             if (!to) {
6772
6773                 to = targetBBox;
6774
6775                 // 'to' is not a vertex. If the target is a point (i.e. it's not an element), we
6776                 // might use dragging route instead of main routing method if that is enabled.
6777                 var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id;
6778
6779                 if (endingAtPoint && _.isFunction(opt.draggingRoute)) {
6780                     // Make sure we passing points only (not rects).
6781                     var dragFrom = from instanceof g.rect ? from.center() : from;
6782                     partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt);
6783                 }
6784             }
6785
6786             // if partial route has not been calculated yet use the main routing method to find one
6787             partialRoute = partialRoute || findRoute(from, to, map, opt);
6788
6789             var leadPoint = _.first(partialRoute);
6790
6791             if (leadPoint && leadPoint.equals(tailPoint)) {
6792
6793                 // remove the first point if the previous partial route had the same point as last
6794                 partialRoute.shift();
6795             }
6796
6797             tailPoint = _.last(partialRoute) || tailPoint;
6798
6799             newVertices = newVertices.concat(partialRoute);
6800         };
6801
6802         // we might have to reverse the result if we swapped source and target at the beginning
6803         return reverseRouting ? newVertices.reverse() : newVertices;
6804     };
6805
6806     // public function
6807     return function(vertices, opt, linkView) {
6808
6809         return router.call(linkView, vertices, _.extend({}, config, opt));
6810     };
6811
6812 })();
6813
6814 joint.routers.metro = (function() {
6815
6816     if (!_.isFunction(joint.routers.manhattan)) {
6817
6818         throw('Metro requires the manhattan router.');
6819     }
6820
6821     var config = {
6822
6823         // cost of a diagonal step (calculated if not defined).
6824         diagonalCost: null,
6825
6826         // an array of directions to find next points on the route
6827         directions: function() {
6828
6829             var step = this.step;
6830             var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1));
6831
6832             return [
6833                 { offsetX: step  , offsetY: 0     , cost: step },
6834                 { offsetX: step  , offsetY: step  , cost: diagonalCost },
6835                 { offsetX: 0     , offsetY: step  , cost: step },
6836                 { offsetX: -step , offsetY: step  , cost: diagonalCost },
6837                 { offsetX: -step , offsetY: 0     , cost: step },
6838                 { offsetX: -step , offsetY: -step , cost: diagonalCost },
6839                 { offsetX: 0     , offsetY: -step , cost: step },
6840                 { offsetX: step  , offsetY: -step , cost: diagonalCost }
6841             ];
6842         },
6843
6844         // a simple route used in situations, when main routing method fails
6845         // (exceed loops, inaccessible).
6846         fallbackRoute: function(from, to, opts) {
6847
6848             // Find a route which breaks by 45 degrees ignoring all obstacles.
6849
6850             var theta = from.theta(to);
6851
6852             var a = { x: to.x, y: from.y };
6853             var b = { x: from.x, y: to.y };
6854
6855             if (theta % 180 > 90) {
6856                 var t = a;
6857                 a = b;
6858                 b = t;
6859             }
6860
6861             var p1 = (theta % 90) < 45 ? a : b;
6862
6863             var l1 = g.line(from, p1);
6864
6865             var alpha = 90 * Math.ceil(theta / 90);
6866
6867             var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1);
6868
6869             var l2 = g.line(to, p2);
6870
6871             var point = l1.intersection(l2);
6872
6873             return point ? [point.round(), to] : [to];
6874         }
6875     };
6876
6877     // public function
6878     return function(vertices, opts, linkView) {
6879
6880         return joint.routers.manhattan(vertices, _.extend({}, config, opts), linkView);
6881     };
6882
6883 })();
6884
6885 joint.connectors.normal = function(sourcePoint, targetPoint, vertices) {
6886
6887     // Construct the `d` attribute of the `<path>` element.
6888     var d = ['M', sourcePoint.x, sourcePoint.y];
6889
6890     _.each(vertices, function(vertex) {
6891
6892         d.push(vertex.x, vertex.y);
6893     });
6894
6895     d.push(targetPoint.x, targetPoint.y);
6896
6897     return d.join(' ');
6898 };
6899
6900 joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) {
6901
6902     var offset = opts.radius || 10;
6903
6904     var c1, c2, d1, d2, prev, next;
6905
6906     // Construct the `d` attribute of the `<path>` element.
6907     var d = ['M', sourcePoint.x, sourcePoint.y];
6908
6909     _.each(vertices, function(vertex, index) {
6910
6911         // the closest vertices
6912         prev = vertices[index - 1] || sourcePoint;
6913         next = vertices[index + 1] || targetPoint;
6914
6915         // a half distance to the closest vertex
6916         d1 = d2 || g.point(vertex).distance(prev) / 2;
6917         d2 = g.point(vertex).distance(next) / 2;
6918
6919         // control points
6920         c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round();
6921         c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round();
6922
6923         d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L');
6924     });
6925
6926     d.push(targetPoint.x, targetPoint.y);
6927
6928     return d.join(' ');
6929 };
6930
6931 joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) {
6932
6933     var d;
6934
6935     if (vertices.length) {
6936
6937         d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint]));
6938
6939     } else {
6940         // if we have no vertices use a default cubic bezier curve, cubic bezier requires
6941         // two control points. The two control points are both defined with X as mid way
6942         // between the source and target points. SourceControlPoint Y is equal to sourcePoint Y
6943         // and targetControlPointY being equal to targetPointY. Handle situation were
6944         // sourcePointX is greater or less then targetPointX.
6945         var controlPointX = (sourcePoint.x < targetPoint.x)
6946                 ? targetPoint.x - ((targetPoint.x - sourcePoint.x) / 2)
6947                 : sourcePoint.x - ((sourcePoint.x - targetPoint.x) / 2);
6948
6949         d = [
6950             'M', sourcePoint.x, sourcePoint.y,
6951             'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y,
6952             targetPoint.x, targetPoint.y
6953         ];
6954     }
6955
6956     return d.join(' ');
6957 };
6958
6959
6960         return joint;
6961
6962 }));