1 /**
  2  * network.js
  3  *
  4  * @brief
  5  * Links Network is an interactive chart to visualize networks.
  6  * It allows creating nodes, links between the nodes, and interactive packages
  7  * moving between nodes. The visualization supports custom styles, colors,
  8  * sizes, images, and more.
  9  *
 10  * The network visualization works smooth on any modern browser for up to a
 11  * few hundred nodes and connections.
 12  *
 13  * Network is developed as a Google Visualization Chart in javascript.
 14  * There is a GWT wrapper available to use the Network in GWT (Google Web
 15  * Toolkit). It runs on all modern browsers without additional requirements.
 16  * Network is tested on Firefox 3.6+, Safari 5.0+, Chrome 6.0+, Opera 10.6+,
 17  * Internet Explorer 9+.
 18  *
 19  * Links Network is part of the CHAP Links library.
 20  *
 21  * @license
 22  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 23  * use this file except in compliance with the License. You may obtain a copy
 24  * of the License at
 25  *
 26  * http://www.apache.org/licenses/LICENSE-2.0
 27  *
 28  * Unless required by applicable law or agreed to in writing, software
 29  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 30  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 31  * License for the specific language governing permissions and limitations under
 32  * the License.
 33  *
 34  * Copyright (c) 2011-2012 Almende B.V.
 35  *
 36  * @author 	Jos de Jong, <jos@almende.org>
 37  * @date    2013-04-26
 38  * @version 1.5.0
 39  */
 40 
 41 /*
 42  internally rename radiusMin to widthMin and radiusMax to widthMax ?
 43  right now the default widthMin and widthMax are not taken in case of an image
 44  solve problem with nodes initially having no velocity, thus not starting animation
 45  let the strength of a link depend on the number of (intermediate) connections
 46  group clusters as one node when zooming out
 47  make a smarter "random" start position for the nodes, based on the links
 48  currently it does not work well to switch from realtime to history animation and vice versa
 49  when adding nodes/links in realtime, they have no nice start position, causing wild movements
 50  when a node is created/replaced/removed, adjust all references in the defined links and packages 
 51  setting the length of links extra long does not result in nice looking network... repulsion should be changed too. 
 52  Or better: repulsion must be stronger than gravity
 53  */
 54 
 55 /**
 56  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 57  * "links"
 58  */
 59 if (typeof links === 'undefined') {
 60     links = {};
 61     // important: do not use var, as "var links = {};" will overwrite 
 62     //            the existing links variable value with undefined in IE8, IE7.  
 63 }
 64 
 65 
 66 /**
 67  * Ensure the variable google exists
 68  */
 69 if (typeof google === 'undefined') {
 70     google = undefined;
 71     // important: do not use var, as "var google = undefined;" will overwrite 
 72     //            the existing google variable value with undefined in IE8, IE7.
 73 }
 74 
 75 
 76 /**
 77  * @constructor links.Network
 78  *
 79  * <p>
 80  * Links Network is an interactive chart to visualize networks.
 81  * It allows creating nodes, links between the nodes, and interactive packages
 82  * moving between nodes. The visualization supports custom styles, colors,
 83  * sizes, images, and more.
 84  * </p>
 85  * <p>
 86  * The network visualization works smooth on any modern browser for up to a
 87  * few hundred nodes and connections.
 88  * </p>
 89  * <p>Network is developed as a Google Visualization Chart in javascript.
 90  * There is a GWT wrapper available to use the Network in GWT (Google Web
 91  * Toolkit). It runs on all modern browsers without additional requirements.
 92  * Network is tested on Firefox 3.6+, Safari 5.0+, Chrome 6.0+, Opera 10.6+,
 93  * Internet Explorer 9+.
 94  * </p>
 95  *
 96  * Usage:
 97  * <code><pre>
 98  *   var nodesTable = new google.visualization.DataTable();
 99  *   nodesTable.addColumn('number', 'id');
100  *   nodesTable.addColumn('string', 'text');
101  *   nodesTable.addRow([1, "Node 1"]);
102  *   nodesTable.addRow([2, "Node 2"]);
103  *   // ...
104  *
105  *   var linksTable = new google.visualization.DataTable();
106  *   linksTable.addColumn('number', 'from');
107  *   linksTable.addColumn('number', 'to');
108  *   linksTable.addRow([1, 2]);
109  *
110  *   var packageTable = undefined;
111  *
112  *   var network = new links.Network(document.getElementById('network'));
113  *   network.draw(nodesTable, linksTable, packagesTable, options);
114  * </pre></code>
115  * <br>
116  *
117  * @param {Element} container   The DOM element in which the Network will
118  *                                  be created. Normally a div element.
119  */
120 links.Network = function(container) {
121     // create variables and set default values
122     this.containerElement = container;
123     this.width = "100%";
124     this.height = "100%";
125     this.refreshRate = 50; // milliseconds
126     this.stabilize = true; // stabilize before displaying the network
127     this.selectable = true;
128 
129     // set constant values
130     this.constants = {
131         "nodes": {
132             "radiusMin": 5,
133             "radiusMax": 20,
134             "radius": 5,
135             "distance": 100, // px
136             "style": "rect",
137             "image": undefined,
138             "widthMin": 16, // px
139             "widthMax": 64, // px
140             "fontColor": "black",
141             "fontSize": 14, // px
142             //"fontFace": "verdana",
143             "fontFace": "arial",
144             "borderColor": "#2B7CE9",
145             "backgroundColor": "#97C2FC",
146             "highlightColor": "#D2E5FF",
147             "group": undefined
148         },
149         "links": {
150             "widthMin": 1,
151             "widthMax": 15,
152             "width": 1,
153             "style": "line",
154             "color": "#343434",
155             "fontColor": "#343434",
156             "fontSize": 14, // px
157             "fontFace": "arial",
158             //"distance": 100, //px
159             "length": 100,   // px
160             "dashlength": 10,
161             "dashgap": 5
162         },
163         "packages": {
164             "radius": 5,
165             "radiusMin": 5,
166             "radiusMax": 10,
167             "style": "dot",
168             "color": "#2B7CE9",
169             "image": undefined,
170             "widthMin": 16, // px
171             "widthMax": 64, // px
172             "duration": 1.0   // seconds
173         },
174         "minForce": 0.05,
175         "minVelocity": 0.02,   // px/s
176         "maxIterations": 1000  // maximum number of iteration to stabilize
177     };
178 
179     this.nodes = [];     // array with Node objects
180     this.links = [];     // array with Link objects
181     this.packages = [];  // array with all Package packages
182     this.images = new links.Network.Images();     // object with images
183     this.groups = new links.Network.Groups();     // object with groups
184 
185     // properties of the data
186     this.hasMovingLinks = false;    // True if one or more of the links or nodes have an animation
187     this.hasMovingNodes = false;    // True if any of the nodes have an undefined position
188     this.hasMovingPackages = false; // True if there are one or more packages
189 
190     this.selection = [];
191     this.timer = undefined;
192 
193     // create a frame and canvas
194     this._create();
195 };
196 
197 /**
198  * Main drawing logic. This is the function that needs to be called
199  * in the html page, to draw the Network.
200  * Note that Object DataTable is defined in google.visualization.DataTable
201  *
202  * A data table with the events must be provided, and an options table.
203  * @param {google.visualization.DataTable | Array} [nodes]     The data containing the nodes.
204  * @param {google.visualization.DataTable | Array} [links]     The data containing the links.
205  * @param {google.visualization.DataTable | Array} [packages]  The data containing the packages
206  * @param {Object} options                                     A name/value map containing settings
207  */
208 links.Network.prototype.draw = function(nodes, links, packages, options) {
209     var nodesTable, linksTable, packagesTable;
210     // correctly read the parameters. links and packages are optional.
211     if (options != undefined) {
212         nodesTable = nodes;
213         linksTable = links;
214         packagesTable = packages;
215     }
216     else if (packages != undefined) {
217         nodesTable = nodes;
218         linksTable = links;
219         packagesTable = undefined;
220         options = packages;
221     }
222     else if (links != undefined) {
223         nodesTable = nodes;
224         linksTable = undefined;
225         packagesTable = undefined;
226         options = links;
227     }
228     else if (nodes != undefined) {
229         nodesTable = undefined;
230         linksTable = undefined;
231         packagesTable = undefined;
232         options = nodes;
233     }
234 
235     if (options != undefined) {
236         // retrieve parameter values
237         if (options.width != undefined)           {this.width = options.width;}
238         if (options.height != undefined)          {this.height = options.height;}
239         if (options.stabilize != undefined)       {this.stabilize = options.stabilize;}
240         if (options.selectable != undefined)      {this.selectable = options.selectable;}
241         
242         // TODO: work out these options and document them
243         if (options.links) {
244             for (var prop in options.links) {
245                 if (options.links.hasOwnProperty(prop)) {
246                     this.constants.links[prop] = options.links[prop];
247                 }
248             }
249 
250             if (options.links.length != undefined &&
251                     options.nodes && options.nodes.distance == undefined) {
252                 this.constants.links.length   = options.links.length;
253                 this.constants.nodes.distance = options.links.length * 1.25;
254             }
255 
256             if (!options.links.fontColor) {
257                 this.constants.links.fontColor = options.links.color;
258             }
259 
260             // Added to support dashed lines
261             // David Jordan
262             // 2012-08-08
263             if (options.links.dashlength != undefined) {
264                 this.constants.links.dashlength   = options.links.dashlength;
265             }
266             if (options.links.dashgap != undefined) {
267                 this.constants.links.dashgap   = options.links.dashgap;
268             }
269             if (options.links.altdashlength != undefined) {
270                 this.constants.links.altdashlength   = options.links.altdashlength;
271             }
272         }
273         if (options.nodes) {
274             for (prop in options.nodes) {
275                 if (options.nodes.hasOwnProperty(prop)) {
276                     this.constants.nodes[prop] = options.nodes[prop];
277                 }
278             }
279 
280             /*
281              if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
282              if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
283              */
284         }
285         if (options.packages) {
286             for (prop in options.packages) {
287                 if (options.packages.hasOwnProperty(prop)) {
288                     this.constants.packages[prop] = options.packages[prop];
289                 }
290             }
291 
292             /*
293              if (options.packages.widthMin) this.constants.packages.radiusMin = options.packages.widthMin;
294              if (options.packages.widthMax) this.constants.packages.radiusMax = options.packages.widthMax;
295              */
296         }
297 
298         if (options.groups) {
299             for (var groupname in options.groups) {
300                 if (options.groups.hasOwnProperty(groupname)) {
301                     var group = options.groups[groupname];
302                     this.groups.add(groupname, group);
303                 }
304             }
305         }
306     }
307 
308     this._setBackgroundColor(options.backgroundColor);
309 
310     this._setSize(this.width, this.height);
311     this._setTranslation(0, 0);
312     this._setScale(1.0);
313 
314     // set all data
315     this.hasTimestamps = false;
316     this.setNodes(nodesTable);
317     this.setLinks(linksTable);
318     this.setPackages(packagesTable);
319 
320     this._reposition(); // TODO: bad solution  
321     if (this.stabilize) {
322         this._doStabilize();
323     }
324     this.start();
325 
326     // create an onload callback method for the images
327     var network = this;
328     var callback = function () {
329         network._redraw();
330     };
331     this.images.setOnloadCallback(callback);
332 
333     // fire the ready event
334     this.trigger('ready');
335 };
336 
337 /**
338  * fire an event
339  * @param {String} event   The name of an event, for example "select" or "ready"
340  * @param {Object} params  Optional object with event parameters
341  */
342 links.Network.prototype.trigger = function (event, params) {
343     // trigger the links event bus
344     links.events.trigger(this, event, params);
345 
346     // trigger the google event bus
347     if (google && google.visualization && google.visualization.events) {
348         google.visualization.events.trigger(this, event, params);
349     }
350 };
351 
352 
353 /**
354  * Create the main frame for the Network.
355  * This function is executed once when a Network object is created. The frame
356  * contains a canvas, and this canvas contains all objects like the axis and
357  * nodes.
358  */
359 links.Network.prototype._create = function () {
360     // remove all elements from the container element.
361     while (this.containerElement.hasChildNodes()) {
362         this.containerElement.removeChild(this.containerElement.firstChild);
363     }
364 
365     this.frame = document.createElement("div");
366     this.frame.className = "network-frame";
367     this.frame.style.position = "relative";
368     this.frame.style.overflow = "hidden";
369 
370     // create the graph canvas (HTML canvas element)
371     this.frame.canvas = document.createElement( "canvas" );
372     this.frame.canvas.style.position = "relative";
373     this.frame.appendChild(this.frame.canvas);
374     if (!this.frame.canvas.getContext) {
375         var noCanvas = document.createElement( "DIV" );
376         noCanvas.style.color = "red";
377         noCanvas.style.fontWeight =  "bold" ;
378         noCanvas.style.padding =  "10px";
379         noCanvas.innerHTML =  "Error: your browser does not support HTML canvas";
380         this.frame.canvas.appendChild(noCanvas);
381     }
382 
383     // create event listeners
384     var me = this;
385     var onmousedown = function (event) {me._onMouseDown(event);};
386     var onmousemove = function (event) {me._onMouseMoveTitle(event);};
387     var onmousewheel = function (event) {me._onMouseWheel(event);};
388     var ontouchstart = function (event) {me._onTouchStart(event);};
389     links.Network.addEventListener(this.frame.canvas, "mousedown", onmousedown);
390     links.Network.addEventListener(this.frame.canvas, "mousemove", onmousemove);
391     links.Network.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
392     links.Network.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
393 
394     // add the frame to the container element
395     this.containerElement.appendChild(this.frame);
396 };
397 
398 /**
399  * Set the background  and border styling for the graph
400  * @param {String | Object} backgroundColor
401  */
402 links.Network.prototype._setBackgroundColor = function(backgroundColor) {
403     var fill = "white";
404     var stroke = "lightgray";
405     var strokeWidth = 1;
406 
407     if (typeof(backgroundColor) == "string") {
408         fill = backgroundColor;
409         stroke = "none";
410         strokeWidth = 0;
411     }
412     else if (typeof(backgroundColor) == "object") {
413         if (backgroundColor.fill != undefined)        fill = backgroundColor.fill;
414         if (backgroundColor.stroke != undefined)      stroke = backgroundColor.stroke;
415         if (backgroundColor.strokeWidth != undefined) strokeWidth = backgroundColor.strokeWidth;
416     }
417     else if  (backgroundColor == undefined) {
418         // use use defaults
419     }
420     else {
421         throw "Unsupported type of backgroundColor";
422     }
423 
424     this.frame.style.boxSizing = 'border-box';
425     this.frame.style.backgroundColor = fill;
426     this.frame.style.borderColor = stroke;
427     this.frame.style.borderWidth = strokeWidth + "px";
428     this.frame.style.borderStyle = "solid";
429 };
430 
431 
432 /**
433  * handle on mouse down event
434  */
435 links.Network.prototype._onMouseDown = function (event) {
436     event = event || window.event;
437 
438     if (!this.selectable) {
439         return;
440     }
441 
442     // check if mouse is still down (may be up when focus is lost for example
443     // in an iframe)
444     if (this.leftButtonDown) {
445         this._onMouseUp(event);
446     }
447 
448     // only react on left mouse button down
449     this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
450     if (!this.leftButtonDown && !this.touchDown) {
451         return;
452     }
453 
454     // add event listeners to handle moving the contents
455     // we store the function onmousemove and onmouseup in the timeline, so we can
456     // remove the eventlisteners lateron in the function mouseUp()
457     var me = this;
458     if (!this.onmousemove) {
459         this.onmousemove = function (event) {me._onMouseMove(event);};
460         links.Network.addEventListener(document, "mousemove", me.onmousemove);
461     }
462     if (!this.onmouseup) {
463         this.onmouseup = function (event) {me._onMouseUp(event);};
464         links.Network.addEventListener(document, "mouseup", me.onmouseup);
465     }
466     links.Network.preventDefault(event);
467 
468     // store the start x and y position of the mouse
469     this.startMouseX = event.clientX || event.targetTouches[0].clientX;
470     this.startMouseY = event.clientY || event.targetTouches[0].clientY;
471     this.startFrameLeft = links.Network._getAbsoluteLeft(this.frame.canvas);
472     this.startFrameTop = links.Network._getAbsoluteTop(this.frame.canvas);
473     this.startTranslation = this._getTranslation();
474 
475     this.ctrlKeyDown = event.ctrlKey;
476     this.shiftKeyDown = event.shiftKey;
477 
478     var obj = {
479         "left" :   this._xToCanvas(this.startMouseX - this.startFrameLeft),
480         "top" :    this._yToCanvas(this.startMouseY - this.startFrameTop),
481         "right" :  this._xToCanvas(this.startMouseX - this.startFrameLeft),
482         "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop)
483     };
484     var overlappingNodes = this._getNodesOverlappingWith(obj);
485     // if there are overlapping nodes, select the last one, this is the 
486     // one which is drawn on top of the others
487     this.startClickedObj = (overlappingNodes.length > 0) ?
488         overlappingNodes[overlappingNodes.length - 1] : undefined;
489 
490     if (this.startClickedObj) {
491         // move clicked node with the mouse
492 
493         // make the clicked node temporarily fixed, and store their original state
494         var node = this.nodes[this.startClickedObj.row];
495         this.startClickedObj.xFixed = node.xFixed;
496         this.startClickedObj.yFixed = node.yFixed;
497         node.xFixed = true;
498         node.yFixed = true;
499 
500         if (!this.ctrlKeyDown || !node.isSelected()) {
501             // select this node
502             this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
503         }
504         else {
505             // unselect this node
506             this._unselectNodes([this.startClickedObj]);
507         }
508 
509         if (!this.hasMovingNodes) {
510             this._redraw();
511         }
512     }
513     else if (this.shiftKeyDown) {
514         // start selection of multiple nodes
515     }
516     else {
517         // start moving the graph
518         this.moved = false;
519     }
520 };
521 
522 /**
523  * handle on mouse move event
524  */
525 links.Network.prototype._onMouseMove = function (event) {
526     event = event || window.event;
527 
528     if (!this.selectable) {
529         return;
530     }
531 
532     var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0;
533     var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0;
534     this.mouseX = mouseX;
535     this.mouseY = mouseY;
536 
537     if (this.startClickedObj) {
538         var node = this.nodes[this.startClickedObj.row];
539 
540         if (!this.startClickedObj.xFixed)
541             node.x = this._xToCanvas(mouseX - this.startFrameLeft);
542 
543         if (!this.startClickedObj.yFixed)
544             node.y = this._yToCanvas(mouseY - this.startFrameTop);
545 
546         // start animation if not yet running
547         if (!this.hasMovingNodes) {
548             this.hasMovingNodes = true;
549             this.start();
550         }
551     }
552     else if (this.shiftKeyDown) {
553         // draw a rect from start mouse location to current mouse location
554         if (this.frame.selRect == undefined) {
555             this.frame.selRect = document.createElement("DIV");
556             this.frame.appendChild(this.frame.selRect);
557 
558             this.frame.selRect.style.position = "absolute";
559             this.frame.selRect.style.border = "1px dashed red";
560         }
561 
562         var left =   Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
563         var top =    Math.min(this.startMouseY, mouseY) - this.startFrameTop;
564         var right =  Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
565         var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
566 
567         this.frame.selRect.style.left = left + "px";
568         this.frame.selRect.style.top = top + "px";
569         this.frame.selRect.style.width = (right - left) + "px";
570         this.frame.selRect.style.height = (bottom - top) + "px";
571     }
572     else {
573         // move the network
574         var diffX = mouseX - this.startMouseX;
575         var diffY = mouseY - this.startMouseY;
576 
577         this._setTranslation(
578             this.startTranslation.x + diffX,
579             this.startTranslation.y + diffY);
580         this._redraw();
581 
582         this.moved = true;
583     }
584 
585     links.Network.preventDefault(event);
586 };
587 
588 /**
589  * handle on mouse up event
590  */
591 links.Network.prototype._onMouseUp = function (event) {
592     event = event || window.event;
593 
594     if (!this.selectable) {
595         return;
596     }
597 
598     // remove event listeners here, important for Safari
599     if (this.onmousemove) {
600         links.Network.removeEventListener(document, "mousemove", this.onmousemove);
601         this.onmousemove = undefined;
602     }
603     if (this.onmouseup) {
604         links.Network.removeEventListener(document, "mouseup",   this.onmouseup);
605         this.onmouseup = undefined;
606     }
607     links.Network.preventDefault(event);
608 
609     // check selected nodes
610     var endMouseX = event.clientX || this.mouseX || 0;
611     var endMouseY = event.clientY || this.mouseY || 0;
612 
613     var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
614 
615     if (this.startClickedObj) {
616         // restore the original fixed state
617         var node = this.nodes[this.startClickedObj.row];
618         node.xFixed = this.startClickedObj.xFixed;
619         node.yFixed = this.startClickedObj.yFixed;
620     }
621     else if (this.shiftKeyDown) {
622         // select nodes inside selection area
623         var obj = {
624             "left":   this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
625             "top":    this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
626             "right":  this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
627             "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
628         };
629         var overlappingNodes = this._getNodesOverlappingWith(obj);
630         this._selectNodes(overlappingNodes, ctrlKey);
631         this.redraw();
632 
633         // remove the selection rectangle
634         if (this.frame.selRect) {
635             this.frame.removeChild(this.frame.selRect);
636             this.frame.selRect = undefined;
637         }
638     }
639     else {
640         if (!this.ctrlKeyDown && !this.moved) {
641             // remove selection
642             this._unselectNodes();
643             this._redraw();
644         }
645     }
646 
647     this.leftButtonDown = false;
648     this.ctrlKeyDown = false;
649 };
650 
651 
652 /**
653  * Event handler for mouse wheel event, used to zoom the timeline
654  * Code from http://adomas.org/javascript-mouse-wheel/
655  * @param {event}  event   The event
656  */
657 links.Network.prototype._onMouseWheel = function(event) {
658     event = event || window.event;
659     var mouseX = event.clientX;
660     var mouseY = event.clientY;
661 
662     // retrieve delta    
663     var delta = 0;
664     if (event.wheelDelta) { /* IE/Opera. */
665         delta = event.wheelDelta/120;
666     } else if (event.detail) { /* Mozilla case. */
667         // In Mozilla, sign of delta is different than in IE.
668         // Also, delta is multiple of 3.
669         delta = -event.detail/3;
670     }
671 
672     // If delta is nonzero, handle it.
673     // Basically, delta is now positive if wheel was scrolled up,
674     // and negative, if wheel was scrolled down.
675     if (delta) {
676         // determine zoom factor, and adjust the zoom factor such that zooming in
677         // and zooming out correspond wich each other
678         var zoom = delta / 10;
679         if (delta < 0) {
680             zoom = zoom / (1 - zoom);
681         }
682 
683         var scaleOld = this._getScale();
684         var scaleNew = scaleOld * (1 + zoom);
685         if (scaleNew < 0.01) {
686             scaleNew = 0.01;
687         }
688         if (scaleNew > 10) {
689             scaleNew = 10;
690         }
691 
692         var frameLeft = links.Network._getAbsoluteLeft(this.frame.canvas);
693         var frameTop = links.Network._getAbsoluteTop(this.frame.canvas);
694         var x = mouseX - frameLeft;
695         var y = mouseY - frameTop;
696 
697         var translation = this._getTranslation();
698         var scaleFrac = scaleNew / scaleOld;
699         var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
700         var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
701 
702         this._setScale(scaleNew);
703         this._setTranslation(tx, ty);
704         this._redraw();
705     }
706 
707     // Prevent default actions caused by mouse wheel.
708     // That might be ugly, but we handle scrolls somehow
709     // anyway, so don't bother here...
710     links.Network.preventDefault(event);
711 };
712 
713 
714 /**
715  * Mouse move handler for checking whether the title moves over a node or
716  * package with a title.
717  */
718 links.Network.prototype._onMouseMoveTitle = function (event) {
719     event = event || window.event;
720 
721     var startMouseX = event.clientX;
722     var startMouseY = event.clientY;
723     this.startFrameLeft = this.startFrameLeft || links.Network._getAbsoluteLeft(this.frame.canvas);
724     this.startFrameTop = this.startFrameTop || links.Network._getAbsoluteTop(this.frame.canvas);
725 
726     var x = startMouseX - this.startFrameLeft;
727     var y = startMouseY - this.startFrameTop;
728 
729     // check if the previously selected node is still selected
730     if (this.popupNode) {
731         this._checkHidePopup(x, y);
732     }
733 
734     // start a timeout that will check if the mouse is positioned above 
735     // an element
736     var me = this;
737     var checkShow = function() {
738         me._checkShowPopup(x, y);
739     };
740     if (this.popupTimer) {
741         clearInterval(this.popupTimer); // stop any running timer
742     }
743     if (!this.leftButtonDown) {
744         this.popupTimer = setTimeout(checkShow, 300);
745     }
746 };
747 
748 /**
749  * Check if there is an element on the given position in the network (
750  * (a node, package, or link). If so, and if this element has a title,
751  * show a popup window with its title.
752  *
753  * @param {number} x
754  * @param {number} y
755  */
756 links.Network.prototype._checkShowPopup = function (x, y) {
757     var obj = {
758         "left" : this._xToCanvas(x),
759         "top" : this._yToCanvas(y),
760         "right" : this._xToCanvas(x),
761         "bottom" : this._yToCanvas(y)
762     };
763 
764     var i, len;
765     var lastPopupNode = this.popupNode;
766 
767     if (this.popupNode == undefined) {
768         // search the packages for overlap
769 
770         for (i = 0, len = this.packages.length; i < len; i++) {
771             var p = this.packages[i];
772             if (p.getTitle() != undefined && p.isOverlappingWith(obj)) {
773                 this.popupNode = p;
774                 break;
775             }
776         }
777     }
778 
779     if (this.popupNode == undefined) {
780         // search the nodes for overlap, select the top one in case of multiple nodes
781         var nodes = this.nodes;
782         for (i = nodes.length - 1; i >= 0; i--) {
783             var node = nodes[i];
784             if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
785                 this.popupNode = node;
786                 break;
787             }
788         }
789     }
790 
791     if (this.popupNode == undefined) {
792         // search the links for overlap
793         var allLinks = this.links;
794         for (i = 0, len = allLinks.length; i < len; i++) {
795             var link = allLinks[i];
796             if (link.getTitle() != undefined && link.isOverlappingWith(obj)) {
797                 this.popupNode = link;
798                 break;
799             }
800         }
801     }
802 
803     if (this.popupNode) {
804         // show popup message window
805         if (this.popupNode != lastPopupNode) {
806             var me = this;
807             if (!me.popup) {
808                 me.popup = new links.Network.Popup(me.frame);
809             }
810 
811             // adjust a small offset such that the mouse cursor is located in the
812             // bottom left location of the popup, and you can easily move over the
813             // popup area
814             me.popup.setPosition(x - 3, y - 3);
815             me.popup.setText(me.popupNode.getTitle());
816             me.popup.show();
817         }
818     }
819     else {
820         if (this.popup) {
821             this.popup.hide();
822         }
823     }
824 };
825 
826 /**
827  * Check if the popup must be hided, which is the case when the mouse is no
828  * longer hovering on the object
829  * @param {number} x
830  * @param {number} y
831  */
832 links.Network.prototype._checkHidePopup = function (x, y) {
833     var obj = {
834         "left" : x,
835         "top" : y,
836         "right" : x,
837         "bottom" : y
838     };
839 
840     if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
841         this.popupNode = undefined;
842         if (this.popup) {
843             this.popup.hide();
844         }
845     }
846 };
847 
848 /**
849  * Event handler for touchstart event on mobile devices
850  */
851 links.Network.prototype._onTouchStart = function(event) {
852     links.Network.preventDefault(event);
853 
854     if (this.touchDown) {
855         // if already moving, return
856         return;
857     }
858     this.touchDown = true;
859 
860     var me = this;
861     if (!this.ontouchmove) {
862         this.ontouchmove = function (event) {me._onTouchMove(event);};
863         links.Network.addEventListener(document, "touchmove", this.ontouchmove);
864     }
865     if (!this.ontouchend) {
866         this.ontouchend   = function (event) {me._onTouchEnd(event);};
867         links.Network.addEventListener(document, "touchend", this.ontouchend);
868     }
869 
870     this._onMouseDown(event);
871 };
872 
873 /**
874  * Event handler for touchmove event on mobile devices
875  */
876 links.Network.prototype._onTouchMove = function(event) {
877     links.Network.preventDefault(event);
878     this._onMouseMove(event);
879 };
880 
881 /**
882  * Event handler for touchend event on mobile devices
883  */
884 links.Network.prototype._onTouchEnd = function(event) {
885     links.Network.preventDefault(event);
886 
887     this.touchDown = false;
888 
889     if (this.ontouchmove) {
890         links.Network.removeEventListener(document, "touchmove", this.ontouchmove);
891         this.ontouchmove = undefined;
892     }
893     if (this.ontouchend) {
894         links.Network.removeEventListener(document, "touchend", this.ontouchend);
895         this.ontouchend = undefined;
896     }
897 
898     this._onMouseUp(event);
899 };
900 
901 
902 /**
903  * Unselect selected nodes. If no selection array is provided, all nodes
904  * are unselected
905  * @param {Object[]} selection     Array with selection objects, each selection
906  *                                 object has a parameter row. Optional
907  * @param {Boolean} triggerSelect  If true (default), the select event
908  *                                 is triggered when nodes are unselected
909  * @return {Boolean} changed       True if the selection is changed
910  */
911 links.Network.prototype._unselectNodes = function(selection, triggerSelect) {
912     var changed = false;
913     var i, iMax, row;
914 
915     if (selection) {
916         // remove provided selections
917         for (i = 0, iMax = selection.length; i < iMax; i++) {
918             row = selection[i].row;
919             this.nodes[row].unselect();
920 
921             var j = 0;
922             while (j < this.selection.length) {
923                 if (this.selection[j].row == row) {
924                     this.selection.splice(j, 1);
925                     changed = true;
926                 }
927                 else {
928                     j++;
929                 }
930             }
931         }
932     }
933     else if (this.selection && this.selection.length) {
934         // remove all selections
935         for (i = 0, iMax = this.selection.length; i < iMax; i++) {
936             row = this.selection[i].row;
937             this.nodes[row].unselect();
938             changed = true;
939         }
940         this.selection = [];
941     }
942 
943     if (changed && (triggerSelect == true || triggerSelect == undefined)) {
944         // fire the select event
945         this.trigger('select');
946     }
947 
948     return changed;
949 };
950 
951 /**
952  * select all nodes on given location x, y
953  * @param {Array} selection   an array with selection objects. Each selection
954  *                            object has a parameter row
955  * @param {boolean} append    If true, the new selection will be appended to the
956  *                            current selection (except for duplicate entries)
957  * @return {Boolean} changed  True if the selection is changed
958  */
959 links.Network.prototype._selectNodes = function(selection, append) {
960     var changed = false;
961     var i, iMax;
962 
963     // TODO: the selectNodes method is a little messy, rework this
964 
965     // check if the current selection equals the desired selection
966     var selectionAlreadyDone = true;
967     if (selection.length != this.selection.length) {
968         selectionAlreadyDone = false;
969     }
970     else {
971         for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
972             if (selection[i].row != this.selection[i].row) {
973                 selectionAlreadyDone = false;
974                 break;
975             }
976         }
977     }
978     if (selectionAlreadyDone) {
979         return changed;
980     }
981 
982     if (append == undefined || append == false) {
983         // first deselect any selected node
984         var triggerSelect = false;
985         changed = this._unselectNodes(undefined, triggerSelect);
986     }
987 
988     for (i = 0, iMax = selection.length; i < iMax; i++) {
989         // add each of the new selections, but only when they are not duplicate
990         var row = selection[i].row;
991         var isDuplicate = false;
992         for (var j = 0, jMax = this.selection.length; j < jMax; j++) {
993             if (this.selection[j].row == row) {
994                 isDuplicate = true;
995                 break;
996             }
997         }
998 
999         if (!isDuplicate) {
1000             this.nodes[row].select();
1001             this.selection.push(selection[i]);
1002             changed = true;
1003         }
1004     }
1005 
1006     if (changed) {
1007         // fire the select event
1008         this.trigger('select');
1009     }
1010 
1011     return changed;
1012 };
1013 
1014 /**
1015  * retrieve all nodes overlapping with given object
1016  * @param {Object} obj  An object with parameters left, top, right, bottom
1017  * @return {Object[]}   An array with selection objects containing
1018  *                      the parameter row.
1019  */
1020 links.Network.prototype._getNodesOverlappingWith = function (obj) {
1021     var overlappingNodes = [];
1022 
1023     for (var i = 0; i < this.nodes.length; i++) {
1024         if (this.nodes[i].isOverlappingWith(obj)) {
1025             var sel = {"row": i};
1026             overlappingNodes.push(sel);
1027         }
1028     }
1029 
1030     return overlappingNodes;
1031 };
1032 
1033 /**
1034  * retrieve the currently selected nodes
1035  * @return {Object[]} an array with zero or more objects. Each object
1036  *                              contains the parameter row
1037  */
1038 links.Network.prototype.getSelection = function() {
1039     var selection = [];
1040 
1041     for (var i = 0; i < this.selection.length; i++) {
1042         var row = this.selection[i].row;
1043         selection.push({"row": row});
1044     }
1045 
1046     return selection;
1047 };
1048 
1049 /**
1050  * select zero or more nodes
1051  * @param {object[]} selection  an array with zero or more objects. Each object
1052  *                              contains the parameter row
1053  */
1054 links.Network.prototype.setSelection = function(selection) {
1055     var i, iMax, row;
1056 
1057     if (selection.length == undefined)
1058         throw "Selection must be an array with objects";
1059 
1060     // first unselect any selected node
1061     for (i = 0, iMax = this.selection.length; i < iMax; i++) {
1062         row = this.selection[i].row;
1063         this.nodes[row].unselect();
1064     }
1065 
1066     this.selection = [];
1067 
1068     for (i = 0, iMax = selection.length; i < iMax; i++) {
1069         row = selection[i].row;
1070 
1071         if (row == undefined)
1072             throw "Parameter row missing in selection object";
1073         if (row > this.nodes.length-1)
1074             throw "Parameter row out of range";
1075 
1076         var sel = {"row": row};
1077         this.selection.push(sel);
1078         this.nodes[row].select();
1079     }
1080 
1081     this.redraw();
1082 };
1083 
1084 
1085 /**
1086  * Temporary method to test calculating a hub value for the nodes
1087  * @param {number} level        Maximum number links between two nodes in order
1088  *                              to call them connected. Optional, 1 by default
1089  * @return {Number[]} connectioncount array with the connection count
1090  *                                    for each node
1091  */
1092 links.Network.prototype._getConnectionCount = function(level) {
1093     var conn = this.links;
1094     if (level == undefined) {
1095         level = 1;
1096     }
1097 
1098     // get the nodes connected to given nodes
1099     function getConnectedNodes(nodes) {
1100         var connectedNodes = [];
1101 
1102         for (var j = 0, jMax = nodes.length; j < jMax; j++) {
1103             var node = nodes[j];
1104 
1105             // find all nodes connected to this node
1106             for (var i = 0, iMax = conn.length; i < iMax; i++) {
1107                 var other = null;
1108 
1109                 // check if connected
1110                 if (conn[i].from == node)
1111                     other = conn[i].to;
1112                 else if (conn[i].to == node)
1113                     other = conn[i].from;
1114 
1115                 // check if the other node is not already in the list with nodes
1116                 var k, kMax;
1117                 if (other) {
1118                     for (k = 0, kMax = nodes.length; k < kMax; k++) {
1119                         if (nodes[k] == other) {
1120                             other = null;
1121                             break;
1122                         }
1123                     }
1124                 }
1125                 if (other) {
1126                     for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
1127                         if (connectedNodes[k] == other) {
1128                             other = null;
1129                             break;
1130                         }
1131                     }
1132                 }
1133 
1134                 if (other)
1135                     connectedNodes.push(other);
1136             }
1137         }
1138 
1139         return connectedNodes;
1140     }
1141 
1142     var connections = [];
1143     var level0 = [];
1144     var nodes = this.nodes;
1145     var i, iMax;
1146     for (i = 0, iMax = nodes.length; i < iMax; i++) {
1147         var c = [nodes[i]];
1148         for (var l = 0; l < level; l++) {
1149             c = c.concat(getConnectedNodes(c));
1150         }
1151         connections.push(c);
1152     }
1153 
1154     var hubs = [];
1155     for (i = 0, len = connections.length; i < len; i++) {
1156         hubs.push(connections[i].length);
1157     }
1158 
1159     return hubs;
1160 };
1161 
1162 
1163 /**
1164  * Set a new size for the network
1165  * @param {string} width   Width in pixels or percentage (for example "800px"
1166  *                         or "50%")
1167  * @param {string} height  Height in pixels or percentage  (for example "400px"
1168  *                         or "30%")
1169  */
1170 links.Network.prototype._setSize = function(width, height) {
1171     this.frame.style.width = width;
1172     this.frame.style.height = height;
1173 
1174     this.frame.canvas.style.width = "100%";
1175     this.frame.canvas.style.height = "100%";
1176 
1177     this.frame.canvas.width = this.frame.canvas.clientWidth;
1178     this.frame.canvas.height = this.frame.canvas.clientHeight;
1179 
1180     if (this.slider) {
1181         this.slider.redraw();
1182     }
1183 };
1184 
1185 /**
1186  * Convert a Google DataTable to a Javascript Array
1187  * @param {google.visualization.DataTable} table
1188  * @return {Array} array
1189  */
1190 links.Network.tableToArray = function(table) {
1191     var array = [];
1192     var col;
1193 
1194     // read the column names
1195     var colCount = table.getNumberOfColumns();
1196     var cols = {};
1197     for (col = 0; col < colCount; col++) {
1198         var label = table.getColumnLabel(col);
1199         cols[label] = col;
1200     }
1201 
1202     var rowCount = table.getNumberOfRows();
1203     for (var i = 0; i < rowCount; i++) {
1204         // copy all properties from the table columns to an object
1205         var properties = {};
1206         for (col in cols) {
1207             if (cols.hasOwnProperty(col)) {
1208                 properties[col] = table.getValue(i, cols[col]);
1209             }
1210         }
1211 
1212         array.push(properties);
1213     }
1214 
1215     return array;
1216 };
1217 
1218 
1219 /**
1220  * Append nodes
1221  * Nodes with a duplicate id will be replaced
1222  * @param {google.visualization.DataTable | Array} nodesTable  The data containing the nodes.
1223  */
1224 links.Network.prototype.addNodes = function(nodesTable) {
1225     var table;
1226     if (google && google.visualization && google.visualization.DataTable &&
1227         nodesTable instanceof google.visualization.DataTable) {
1228         // Google DataTable. 
1229         // Convert to a Javascript Array
1230         table = links.Network.tableToArray(nodesTable);
1231     }
1232     else if (links.Network.isArray(nodesTable)){
1233         // Javascript Array
1234         table = nodesTable;
1235     }
1236     else {
1237         return;
1238     }
1239 
1240     var hasValues = false;
1241     var rowCount = table.length;
1242     for (var i = 0; i < rowCount; i++) {
1243         var properties = table[i];
1244 
1245         if (properties.value != undefined) {
1246             hasValues = true;
1247         }
1248         if (properties.id == undefined) {
1249             throw "Column 'id' missing in table with nodes (row " + i + ")";
1250         }
1251 
1252         this._createNode(properties);
1253     }
1254 
1255     // calculate scaling function when value is provided
1256     if (hasValues) {
1257         this._updateValueRange(this.nodes);
1258     }
1259 
1260     this.start();
1261 };
1262 
1263 /**
1264  * Load all nodes by reading the data table nodesTable
1265  * Note that Object DataTable is defined in google.visualization.DataTable
1266  * @param {google.visualization.DataTable | Array} nodesTable    The data containing the nodes.
1267  */
1268 links.Network.prototype.setNodes = function(nodesTable) {
1269     var table;
1270     if (google && google.visualization && google.visualization.DataTable &&
1271         nodesTable instanceof google.visualization.DataTable) {
1272         // Google DataTable. 
1273         // Convert to a Javascript Array
1274         table = links.Network.tableToArray(nodesTable);
1275     }
1276     else if (links.Network.isArray(nodesTable)){
1277         // Javascript Array
1278         table = nodesTable;
1279     }
1280     else {
1281         return;
1282     }
1283 
1284     this.hasMovingNodes = false;
1285     this.nodesTable = table;
1286     this.nodes = [];
1287     this.selection = [];
1288 
1289     var hasValues = false;
1290     var rowCount = table.length;
1291     for (var i = 0; i < rowCount; i++) {
1292         var properties = table[i];
1293 
1294         if (properties.value != undefined) {
1295             hasValues = true;
1296         }
1297         if (properties.timestamp) {
1298             this.hasTimestamps = this.hasTimestamps || properties.timestamp;
1299         }
1300         if (properties.id == undefined) {
1301             throw "Column 'id' missing in table with nodes (row " + i + ")";
1302         }
1303         this._createNode(properties);
1304     }
1305 
1306     // calculate scaling function when value is provided
1307     if (hasValues) {
1308         this._updateValueRange(this.nodes);
1309     }
1310 };
1311 
1312 /**
1313  * Filter the current nodes table for nodes with a timestamp older than given
1314  * timestamp. Can only be used for nodes added via setNodes(), not via
1315  * addNodes().
1316  * @param {*} [timestamp]    If timestamp is undefined, all nodes are shown
1317  */
1318 links.Network.prototype._filterNodes = function(timestamp) {
1319     if (this.nodesTable == undefined) {
1320         return;
1321     }
1322 
1323     // remove existing nodes with a too new timestamp
1324     if (timestamp !== undefined) {
1325         var ns = this.nodes;
1326         var n = 0;
1327         while (n < ns.length) {
1328             var t = ns[n].timestamp;
1329             if (t !== undefined && t > timestamp) {
1330                 // remove this node
1331                 ns.splice(n, 1);
1332             }
1333             else {
1334                 n++;
1335             }
1336         }
1337     }
1338 
1339     // add all nodes with an old enough timestamp
1340     var table = this.nodesTable;
1341     var rowCount = table.length;
1342     for (var i = 0; i < rowCount; i++) {
1343         // copy all properties
1344         var properties = table[i];
1345 
1346         if (properties.id === undefined) {
1347             throw "Column 'id' missing in table with nodes (row " + i + ")";
1348         }
1349 
1350         // check what the timestamp is
1351         var ts = properties.timestamp ? properties.timestamp : undefined;
1352 
1353         var visible = true;
1354         if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
1355             visible = false;
1356         }
1357 
1358         if (visible) {
1359             // create or update the node
1360             this._createNode(properties);
1361         }
1362     }
1363 
1364     this.start();
1365 };
1366 
1367 /**
1368  * Create a node with the given properties
1369  * If the new node has an id identical to an existing package, the existing
1370  * node will be overwritten.
1371  * The properties can contain a property "action", which can have values
1372  * "create", "update", or "delete"
1373  * @param {Object} properties  An object with properties
1374  */
1375 links.Network.prototype._createNode = function(properties) {
1376     var action = properties.action ? properties.action : "update";
1377     var id, index, newNode, oldNode;
1378 
1379     if (action === "create") {
1380         // create the node
1381         newNode = new links.Network.Node(properties, this.images, this.groups, this.constants);
1382         id = properties.id;
1383         index = (id !== undefined) ? this._findNode(id) : undefined;
1384 
1385         if (index !== undefined) {
1386             // replace node
1387             oldNode = this.nodes[index];
1388             this.nodes[index] = newNode;
1389 
1390             // remove selection of old node
1391             if (oldNode.selected) {
1392                 this._unselectNodes([{'row': index}], false);
1393             }
1394 
1395             /* TODO: implement this? -> will give performance issues, searching all links and node...
1396              // update links linking to this node
1397              var linksTable = this.links;
1398              for (var i = 0, iMax = linksTable.length; i < iMax; i++) {
1399              var link = linksTable[i];
1400              if (link.from == oldNode) {
1401              link.from = newNode;
1402              }
1403              if (link.to == oldNode) {
1404              link.to = newNode;
1405              }
1406              }
1407 
1408              // update packages linking to this node
1409              var packagesTable = this.packages;
1410              for (var i = 0, iMax = packagesTable.length; i < iMax; i++) {
1411              var package = packagesTable[i];
1412              if (package.from == oldNode) {
1413              package.from = newNode;
1414              }
1415              if (package.to == oldNode) {
1416              package.to = newNode;
1417              }
1418              }
1419              */
1420         }
1421         else {
1422             // add new node
1423             this.nodes.push(newNode);
1424         }
1425 
1426         if (!newNode.isFixed()) {
1427             // note: no not use node.isMoving() here, as that gives the current
1428             // velocity of the node, which is zero after creation of the node.
1429             this.hasMovingNodes = true;
1430         }
1431     }
1432     else if (action === "update") {
1433         // update existing node, or create it when not yet existing
1434         id = properties.id;
1435         if (id === undefined) {
1436             throw "Cannot update a node without id";
1437         }
1438 
1439         index = this._findNode(id);
1440         if (index !== undefined) {
1441             // update node
1442             this.nodes[index].setProperties(properties, this.constants);
1443         }
1444         else {
1445             // create node
1446             newNode = new links.Network.Node(properties, this.images, this.groups, this.constants);
1447             this.nodes.push(newNode);
1448 
1449             if (!newNode.isFixed()) {
1450                 // note: no not use node.isMoving() here, as that gives the current
1451                 // velocity of the node, which is zero after creation of the node.
1452                 this.hasMovingNodes = true;
1453             }
1454         }
1455     }
1456     else if (action === "delete") {
1457         // delete existing node
1458         id = properties.id;
1459         if (id === undefined) {
1460             throw "Cannot delete node without its id";
1461         }
1462 
1463         index = this._findNode(id);
1464         if (index !== undefined) {
1465             oldNode = this.nodes[index];
1466             // remove selection of old node
1467             if (oldNode.selected) {
1468                 this._unselectNodes([{'row': index}], false);
1469             }
1470             this.nodes.splice(index, 1);
1471         }
1472         else {
1473             throw "Node with id " + id + " not found";
1474         }
1475     }
1476     else {
1477         throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
1478     }
1479 };
1480 
1481 /**
1482  * Find a node by its id
1483  * @param {Number} id       Id of the node
1484  * @return {Number} index   Index of the node in the array this.nodes, or
1485  *                          undefined when not found. *
1486  */
1487 links.Network.prototype._findNode = function (id) {
1488     var nodes = this.nodes;
1489     for (var n = 0, len = nodes.length; n < len; n++) {
1490         if (nodes[n].id === id) {
1491             return n;
1492         }
1493     }
1494 
1495     return undefined;
1496 };
1497 
1498 /**
1499  * Find a node by its rowNumber
1500  * @param {Number} row                   Row number of the node
1501  * @return {links.Network.Node} node     The node with the given row number, or
1502  *                                       undefined when not found.
1503  */
1504 links.Network.prototype._findNodeByRow = function (row) {
1505     return this.nodes[row];
1506 };
1507 
1508 /**
1509  * Load links by reading the data table
1510  * Note that Object DataTable is defined in google.visualization.DataTable
1511  * @param {google.visualization.DataTable | Array}      linksTable    The data containing the links.
1512  */
1513 links.Network.prototype.setLinks = function(linksTable) {
1514     var table;
1515     if (google && google.visualization && google.visualization.DataTable &&
1516         linksTable instanceof google.visualization.DataTable) {
1517         // Google DataTable. 
1518         // Convert to a Javascript Array
1519         table = links.Network.tableToArray(linksTable);
1520     }
1521     else if (links.Network.isArray(linksTable)){
1522         // Javascript Array
1523         table = linksTable;
1524     }
1525     else {
1526         return;
1527     }
1528 
1529     this.linksTable = table;
1530     this.links = [];
1531     this.hasMovingLinks = false;
1532 
1533     var hasValues = false;
1534     var rowCount = table.length;
1535     for (var i = 0; i < rowCount; i++) {
1536         var properties = table[i];
1537 
1538         if (properties.from === undefined) {
1539             throw "Column 'from' missing in table with links (row " + i + ")";
1540         }
1541         if (properties.to === undefined) {
1542             throw "Column 'to' missing in table with links (row " + i + ")";
1543         }
1544         if (properties.timestamp != undefined) {
1545             this.hasTimestamps = this.hasTimestamps || properties.timestamp;
1546         }
1547         if (properties.value != undefined) {
1548             hasValues = true;
1549         }
1550 
1551         this._createLink(properties);
1552     }
1553 
1554     // calculate scaling function when value is provided
1555     if (hasValues) {
1556         this._updateValueRange(this.links);
1557     }
1558 };
1559 
1560 
1561 /**
1562  * Load links by reading the data table
1563  * Note that Object DataTable is defined in google.visualization.DataTable
1564  * @param {google.visualization.DataTable | Array}      linksTable    The data containing the links.
1565  */
1566 links.Network.prototype.addLinks = function(linksTable) {
1567     var table;
1568     if (google && google.visualization && google.visualization.DataTable &&
1569         linksTable instanceof google.visualization.DataTable) {
1570         // Google DataTable. 
1571         // Convert to a Javascript Array
1572         table = links.Network.tableToArray(linksTable);
1573     }
1574     else if (links.Network.isArray(linksTable)){
1575         // Javascript Array
1576         table = linksTable;
1577     }
1578     else {
1579         return;
1580     }
1581 
1582     var hasValues = false;
1583     var rowCount = table.length;
1584     for (var i = 0; i < rowCount; i++) {
1585         // copy all properties
1586         var properties = table[i];
1587 
1588         if (properties.from === undefined) {
1589             throw "Column 'from' missing in table with links (row " + i + ")";
1590         }
1591         if (properties.to === undefined) {
1592             throw "Column 'to' missing in table with links (row " + i + ")";
1593         }
1594         if (properties.value != undefined) {
1595             hasValues = true;
1596         }
1597 
1598         this._createLink(properties);
1599     }
1600 
1601     // calculate scaling function when value is provided
1602     if (hasValues) {
1603         this._updateValueRange(this.links);
1604     }
1605 
1606     this.start();
1607 };
1608 
1609 
1610 /**
1611  * Filter the current links table for links with a timestamp below given
1612  * timestamp. Can only be used for links added via setLinks(), not via
1613  * addLinks().
1614  * @param {*} [timestamp]  If timestamp is undefined, all links are shown
1615  */
1616 links.Network.prototype._filterLinks = function(timestamp) {
1617     if (this.linksTable == undefined) {
1618         return;
1619     }
1620 
1621     // remove existing packages with a too new timestamp
1622     if (timestamp !== undefined) {
1623         var ls = this.links;
1624         var l = 0;
1625         while (l < ls.length) {
1626             var t = ls[l].timestamp;
1627             if (t !== undefined && t > timestamp) {
1628                 // remove this link
1629                 ls.splice(l, 1);
1630             }
1631             else {
1632                 l++;
1633             }
1634         }
1635     }
1636 
1637     // add all links with an old enough timestamp
1638     var table = this.linksTable;
1639     var rowCount = table.length;
1640     for (var i = 0; i < rowCount; i++) {
1641         var properties = table[i];
1642 
1643         if (properties.from === undefined) {
1644             throw "Column 'from' missing in table with links (row " + i + ")";
1645         }
1646         if (properties.to === undefined) {
1647             throw "Column 'to' missing in table with links (row " + i + ")";
1648         }
1649 
1650         // check what the timestamp is
1651         var ts = properties.timestamp ? properties.timestamp : undefined;
1652 
1653         var visible = true;
1654         if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
1655             visible = false;
1656         }
1657 
1658         if (visible) {
1659             // create or update the link
1660             this._createLink(properties);
1661         }
1662     }
1663 
1664     this.start();
1665 };
1666 
1667 /**
1668  * Create a link with the given properties
1669  * If the new link has an id identical to an existing link, the existing
1670  * link will be overwritten or updated.
1671  * The properties can contain a property "action", which can have values
1672  * "create", "update", or "delete"
1673  * @param {Object} properties   An object with properties
1674  */
1675 links.Network.prototype._createLink = function(properties) {
1676     var action = properties.action ? properties.action : "create";
1677     var id, index, link, oldLink, newLink;
1678 
1679     if (action === "create") {
1680         // create the link, or replace it if already existing
1681         id = properties.id;
1682         index = (id !== undefined) ? this._findLink(id) : undefined;
1683         link = new links.Network.Link(properties, this, this.constants);
1684 
1685         if (index !== undefined) {
1686             // replace existing link
1687             oldLink = this.links[index];
1688             oldLink.from.detachLink(oldLink);
1689             oldLink.to.detachLink(oldLink);
1690             this.links[index] = link;
1691         }
1692         else {
1693             // add new link
1694             this.links.push(link);
1695         }
1696         link.from.attachLink(link);
1697         link.to.attachLink(link);
1698 
1699         if (link.isMoving()) {
1700             this.hasMovingLinks = true;
1701         }
1702     }
1703     else if (action === "update") {
1704         // update existing link, or create the link if not existing
1705         id = properties.id;
1706         if (id === undefined) {
1707             throw "Cannot update a link without id";
1708         }
1709 
1710         index = this._findLink(id);
1711         if (index !== undefined) {
1712             // update link
1713             link = this.links[index];
1714             link.from.detachLink(link);
1715             link.to.detachLink(link);
1716 
1717             link.setProperties(properties, this.constants);
1718             link.from.attachLink(link);
1719             link.to.attachLink(link);
1720         }
1721         else {
1722             // add new link
1723             link = new links.Network.Link(properties, this, this.constants);
1724             link.from.attachLink(link);
1725             link.to.attachLink(link);
1726             this.links.push(link);
1727             if (link.isMoving()) {
1728                 this.hasMovingLinks = true;
1729             }
1730         }
1731     }
1732     else if (action === "delete") {
1733         // delete existing link
1734         id = properties.id;
1735         if (id === undefined) {
1736             throw "Cannot delete link without its id";
1737         }
1738 
1739         index = this._findLink(id);
1740         if (index !== undefined) {
1741             oldLink = this.links[id];
1742             link.from.detachLink(oldLink);
1743             link.to.detachLink(oldLink);
1744             this.links.splice(index, 1);
1745         }
1746         else {
1747             throw "Link with id " + id + " not found";
1748         }
1749     }
1750     else {
1751         throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
1752     }
1753 };
1754 
1755 /**
1756  * Update the link to oldNode in all links and packages.
1757  * @param {Node} oldNode
1758  * @param {Node} newNode
1759  */
1760 // TODO: start utilizing this method _updateNodeReferences
1761 links.Network.prototype._updateNodeReferences = function(oldNode, newNode) {
1762     var arrays = [this.links, this.packages];
1763     for (var a = 0, aMax = arrays.length; a < aMax; a++) {
1764         var array = arrays[a];
1765         for (var i = 0, iMax = array.length; i < iMax; i++) {
1766             if (array.from === oldNode) {
1767                 array.from = newNode;
1768             }
1769             if (array.to === oldNode) {
1770                 array.to = newNode;
1771             }
1772         }
1773     }
1774 };
1775 
1776 /**
1777  * Find a link by its id
1778  * @param {Number} id       Id of the link
1779  * @return {Number} index   Index of the link in the array this.links, or
1780  *                          undefined when not found. *
1781  */
1782 links.Network.prototype._findLink = function (id) {
1783     var links = this.links;
1784     for (var n = 0, len = links.length; n < len; n++) {
1785         if (links[n].id === id) {
1786             return n;
1787         }
1788     }
1789 
1790     return undefined;
1791 };
1792 
1793 /**
1794  * Find a link by its row
1795  * @param {Number} row          Row of the link
1796  * @return {links.Network.Link} the found link, or undefined when not found
1797  */
1798 links.Network.prototype._findLinkByRow = function (row) {
1799     return this.links[row];
1800 };
1801 
1802 /**
1803  * Append packages
1804  * Packages with a duplicate id will be replaced
1805  * Note that Object DataTable is defined in google.visualization.DataTable
1806  * @param {google.visualization.DataTable | Array}   packagesTable    The data containing the packages.
1807  */
1808 links.Network.prototype.addPackages = function(packagesTable) {
1809     var table;
1810     if (google && google.visualization && google.visualization.DataTable &&
1811         packagesTable instanceof google.visualization.DataTable) {
1812         // Google DataTable. 
1813         // Convert to a Javascript Array
1814         table = links.Network.tableToArray(packagesTable);
1815     }
1816     else if (links.Network.isArray(packagesTable)){
1817         // Javascript Array
1818         table = packagesTable;
1819     }
1820     else {
1821         return;
1822     }
1823 
1824     var rowCount = table.length;
1825     for (var i = 0; i < rowCount; i++) {
1826         var properties = table[i];
1827 
1828         if (properties.from === undefined) {
1829             throw "Column 'from' missing in table with packages (row " + i + ")";
1830         }
1831         if (properties.to === undefined) {
1832             throw "Column 'to' missing in table with packages (row " + i + ")";
1833         }
1834 
1835         this._createPackage(properties);
1836     }
1837 
1838     // calculate scaling function when value is provided
1839     this._updateValueRange(this.packages);
1840 
1841     this.start();
1842 };
1843 
1844 /**
1845  * Set a new packages table
1846  * Packages with a duplicate id will be replaced
1847  * Note that Object DataTable is defined in google.visualization.DataTable
1848  * @param {google.visualization.DataTable | Array}   packagesTable    The data containing the packages.
1849  */
1850 links.Network.prototype.setPackages = function(packagesTable) {
1851     var table;
1852     if (google && google.visualization && google.visualization.DataTable &&
1853         packagesTable instanceof google.visualization.DataTable) {
1854         // Google DataTable. 
1855         // Convert to a Javascript Array
1856         table = links.Network.tableToArray(packagesTable);
1857     }
1858     else if (links.Network.isArray(packagesTable)){
1859         // Javascript Array
1860         table = packagesTable;
1861     }
1862     else {
1863         return;
1864     }
1865 
1866     this.packagesTable = table;
1867     this.packages = [];
1868 
1869     var rowCount = table.length;
1870     for (var i = 0; i < rowCount; i++) {
1871         var properties = table[i];
1872 
1873         if (properties.from === undefined) {
1874             throw "Column 'from' missing in table with packages (row " + i + ")";
1875         }
1876         if (properties.to === undefined) {
1877             throw "Column 'to' missing in table with packages (row " + i + ")";
1878         }
1879         if (properties.timestamp) {
1880             this.hasTimestamps = this.hasTimestamps || properties.timestamp;
1881         }
1882 
1883         this._createPackage(properties);
1884     }
1885 
1886     // calculate scaling function when value is provided
1887     this._updateValueRange(this.packages);
1888 
1889     /* TODO: adjust examples and documentation for this?
1890      this.start();
1891      */
1892 };
1893 
1894 /**
1895  * Filter the current package table for packages with a timestamp below given
1896  * timestamp. Can only be used for packages added via setPackages(), not via
1897  * addPackages().
1898  * @param {*} [timestamp] If timestamp is undefined, all packages are shown
1899  */
1900 links.Network.prototype._filterPackages = function(timestamp) {
1901     if (this.packagesTable == undefined) {
1902         return;
1903     }
1904 
1905     // remove all current packages
1906     this.packages = [];
1907 
1908     /* TODO: cleanup
1909      // remove existing packages with a too new timestamp
1910      if (timestamp !== undefined) {
1911      var packages = this.packages;
1912      var p = 0;
1913      while (p < packages.length) {
1914      var package = packages[p];
1915      var t = package.timestamp;
1916 
1917      if (t !== undefined &&  t > timestamp ) {
1918      // remove this package
1919      packages.splice(p, 1);
1920      }
1921      else {
1922      p++;
1923      }
1924      }
1925      }
1926      */
1927 
1928     // add all packages with an old enough timestamp
1929     var table = this.packagesTable;
1930     var rowCount = table.length;
1931     for (var i = 0; i < rowCount; i++) {
1932         var properties = table[i];
1933 
1934         if (properties.from === undefined) {
1935             throw "Column 'from' missing in table with packages (row " + i + ")";
1936         }
1937         if (properties.to === undefined) {
1938             throw "Column 'to' missing in table with packages (row " + i + ")";
1939         }
1940         // check what the timestamp is
1941         var pTimestamp = properties.timestamp ? properties.timestamp : undefined;
1942 
1943         var visible = true;
1944         if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) {
1945             visible = false;
1946         }
1947 
1948         if (visible === true) {
1949             if (properties.progress == undefined) {
1950                 // when no progress is provided, we need to add our own progress
1951                 var duration = properties.duration || this.constants.packages.duration; // seconds
1952 
1953                 var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds
1954                 if (diff < duration) {
1955                     // copy the properties, and fill in the current progress based on the
1956                     // timestamp and the duration
1957                     var original = properties;
1958                     properties = {};
1959                     for (var j in original) {
1960                         if (original.hasOwnProperty(j)) {
1961                             properties[j] = original[j];
1962                         }
1963                     }
1964 
1965                     properties.progress = diff / duration;  // scale 0-1
1966                 }
1967                 else {
1968                     visible = false;
1969                 }
1970             }
1971         }
1972 
1973         if (visible === true) {
1974             // create or update the package
1975             this._createPackage(properties);
1976         }
1977     }
1978 
1979     this.start();
1980 };
1981 
1982 /**
1983  * Create a package with the given properties
1984  * If the new package has an id identical to an existing package, the existing
1985  * package will be overwritten.
1986  * The properties can contain a property "action", which can have values
1987  * "create", "update", or "delete"
1988  * @param {Object} properties   An object with properties
1989  */
1990 links.Network.prototype._createPackage = function(properties) {
1991     var action = properties.action ? properties.action : "create";
1992     var id, index, newPackage;
1993 
1994     if (action === "create") {
1995         // create the package
1996         id = properties.id;
1997         index = (id !== undefined) ? this._findPackage(id) : undefined;
1998         newPackage = new links.Network.Package(properties, this, this.images, this.constants);
1999 
2000         if (index !== undefined) {
2001             // replace existing package
2002             this.packages[index] = newPackage;
2003         }
2004         else {
2005             // add new package
2006             this.packages.push(newPackage);
2007         }
2008 
2009         if (newPackage.isMoving()) {
2010             this.hasMovingPackages = true;
2011         }
2012     }
2013     else if (action === "update") {
2014         // update a package, or create it when not existing
2015         id = properties.id;
2016         if (id === undefined) {
2017             throw "Cannot update a link without id";
2018         }
2019 
2020         index = this._findPackage(id);
2021         if (index !== undefined) {
2022             // update existing package 
2023             this.packages[index].setProperties(properties, this.constants);
2024         }
2025         else {
2026             // add new package
2027             newPackage = new links.Network.Package(properties, this, this.images, this.constants);
2028             this.packages.push(newPackage);
2029             if (newPackage.isMoving()) {
2030                 this.hasMovingPackages = true;
2031             }
2032         }
2033     }
2034     else if (action === "delete") {
2035         // delete existing package
2036         id = properties.id;
2037         if (id === undefined) {
2038             throw "Cannot delete package without its id";
2039         }
2040 
2041         index = this._findPackage(id);
2042         if (index !== undefined) {
2043             this.packages.splice(index, 1);
2044         }
2045         else {
2046             throw "Package with id " + id + " not found";
2047         }
2048     }
2049     else {
2050         throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
2051     }
2052 };
2053 
2054 /**
2055  * Find a package by its id.
2056  * @param {Number} id
2057  * @return {Number} index    Index of the package in the array this.packages,
2058  *                           or undefined when not found
2059  */
2060 links.Network.prototype._findPackage = function (id) {
2061     var packages = this.packages;
2062     for (var n = 0, len = packages.length; n < len; n++) {
2063         if (packages[n].id === id) {
2064             return n;
2065         }
2066     }
2067 
2068     return undefined;
2069 };
2070 
2071 /**
2072  * Find a package by its row
2073  * @param {Number} row          Row of the package
2074  * @return {links.Network.Package} the found package, or undefined when not found
2075  */
2076 links.Network.prototype._findPackageByRow = function (row) {
2077     return this.packages[row];
2078 };
2079 
2080 /**
2081  * Retrieve an object which maps the column ids by their names
2082  * For example a table with columns [id, name, value] will return an
2083  * object {"id": 0, "name": 1, "value": 2}
2084  * @param {google.visualization.DataTable} table    A google datatable
2085  * @return {Object} columnIds   An object
2086  */
2087 // TODO: cleanup this unused method
2088 links.Network.prototype._getColumnNames = function (table) {
2089     var colCount = table.getNumberOfColumns();
2090     var cols = {};
2091     for (var col = 0; col < colCount; col++) {
2092         var label = table.getColumnLabel(col);
2093         cols[label] = col;
2094     }
2095     return cols;
2096 };
2097 
2098 
2099 /**
2100  * Update the values of all object in the given array according to the current
2101  * value range of the objects in the array.
2102  * @param {Array} array.  An array with objects like Links, Nodes, or Packages
2103  *                        The objects must have a method getValue() and
2104  *                        setValueRange(min, max).
2105  */
2106 links.Network.prototype._updateValueRange = function(array) {
2107     var count = array.length;
2108     var i;
2109 
2110     // determine the range of the node values
2111     var valueMin = undefined;
2112     var valueMax = undefined;
2113     for (i = 0; i < count; i++) {
2114         var value = array[i].getValue();
2115         if (value !== undefined) {
2116             valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
2117             valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
2118         }
2119     }
2120 
2121     // adjust the range of all nodes
2122     if (valueMin !== undefined && valueMax !== undefined) {
2123         for (i = 0; i < count; i++) {
2124             array[i].setValueRange(valueMin, valueMax);
2125         }
2126     }
2127 };
2128 
2129 
2130 /**
2131  * Set the current timestamp. All packages with a timestamp smaller or equal
2132  * than the given timestamp will be drawn.
2133  * @param {Date | Number} timestamp
2134  */
2135 links.Network.prototype.setTimestamp = function(timestamp) {
2136     this._filterNodes(timestamp);
2137     this._filterLinks(timestamp);
2138     this._filterPackages(timestamp);
2139 };
2140 
2141 
2142 /**
2143  * Get the range of all timestamps defined in the nodes, links and packages
2144  * @return {Object}   A range object, containing parameters start and end.
2145  */
2146 links.Network.prototype._getRange = function() {
2147     // range is stored as number. at the end of the method, it is converted to
2148     // Date when needed.
2149     var range = {
2150         "start": undefined,
2151         "end": undefined
2152     };
2153 
2154     var tables = [this.nodesTable, this.linksTable];
2155     for (var t = 0, tMax = tables.length; t < tMax; t++) {
2156         var table = tables[t];
2157 
2158         if (table !== undefined) {
2159             for (var i = 0, iMax = table.length; i < iMax; i++) {
2160                 var timestamp = table[i].timestamp;
2161                 if (timestamp) {
2162                     // to long
2163                     if (timestamp instanceof Date) {
2164                         timestamp = timestamp.getTime();
2165                     }
2166 
2167                     // calculate new range
2168                     range.start = range.start ? Math.min(timestamp, range.start) : timestamp;
2169                     range.end = range.end ? Math.max(timestamp, range.end) : timestamp;
2170                 }
2171             }
2172         }
2173     }
2174 
2175     // calculate the range for the packagesTable by hand. In case of packages
2176     // without a progress provided, we need to calculate the end time by hand.
2177     if (this.packagesTable) {
2178         var packagesTable = this.packagesTable;
2179         for (var row = 0, len = packagesTable.length; row < len; row ++) {
2180             var pkg = packagesTable[row],
2181                 timestamp = pkg.timestamp,
2182                 progress = pkg.progress,
2183                 duration = pkg.duration || this.constants.packages.duration;
2184 
2185             // convert to number
2186             if (timestamp instanceof Date) {
2187                 timestamp = timestamp.getTime();
2188             }
2189 
2190             if (timestamp != undefined) {
2191                 var start = timestamp,
2192                     end = progress ? timestamp : (timestamp + duration * 1000);
2193 
2194                 range.start = range.start ? Math.min(start, range.start) : start;
2195                 range.end = range.end ? Math.max(end, range.end) : end;
2196             }
2197         }
2198     }
2199 
2200     // convert to the right type: number or date
2201     var rangeFormat = {
2202         "start": new Date(range.start),
2203         "end": new Date(range.end)
2204     };
2205 
2206     return rangeFormat;
2207 };
2208 
2209 /**
2210  * Start animation.
2211  * Only applicable when packages with a timestamp are available
2212  */
2213 links.Network.prototype.animationStart = function() {
2214     if (this.slider) {
2215         this.slider.play();
2216     }
2217 };
2218 
2219 /**
2220  * Start animation.
2221  * Only applicable when packages with a timestamp are available
2222  */
2223 links.Network.prototype.animationStop = function() {
2224     if (this.slider) {
2225         this.slider.stop();
2226     }
2227 };
2228 
2229 /**
2230  * Set framerate for the animation.
2231  * Only applicable when packages with a timestamp are available
2232  * @param {number} framerate    The framerate in frames per second
2233  */
2234 links.Network.prototype.setAnimationFramerate = function(framerate) {
2235     if (this.slider) {
2236         this.slider.setFramerate(framerate);
2237     }
2238 }
2239 
2240 /**
2241  * Set the duration of playing the whole package history
2242  * Only applicable when packages with a timestamp are available
2243  * @param {number} duration    The duration in seconds
2244  */
2245 links.Network.prototype.setAnimationDuration = function(duration) {
2246     if (this.slider) {
2247         this.slider.setDuration(duration);
2248     }
2249 };
2250 
2251 /**
2252  * Set the time acceleration for playing the history.
2253  * Only applicable when packages with a timestamp are available
2254  * @param {number} acceleration    Acceleration, for example 10 means play
2255  *                                 ten times as fast as real time. A value
2256  *                                 of 1 will play the history in real time.
2257  */
2258 links.Network.prototype.setAnimationAcceleration = function(acceleration) {
2259     if (this.slider) {
2260         this.slider.setAcceleration(acceleration);
2261     }
2262 };
2263 
2264 /**
2265  * Redraw the network with the current data
2266  * chart will be resized too.
2267  */
2268 links.Network.prototype.redraw = function() {
2269     this._setSize(this.width, this.height);
2270 
2271     this._redraw();
2272 };
2273 
2274 /**
2275  * Redraw the network with the current data
2276  */
2277 links.Network.prototype._redraw = function() {
2278     var ctx = this.frame.canvas.getContext("2d");
2279 
2280     // clear the canvas
2281     var w = this.frame.canvas.width;
2282     var h = this.frame.canvas.height;
2283     ctx.clearRect(0, 0, w, h);
2284 
2285     // set scaling and translation
2286     ctx.save();
2287     ctx.translate(this.translation.x, this.translation.y);
2288     ctx.scale(this.scale, this.scale);
2289 
2290     this._drawLinks(ctx);
2291     this._drawNodes(ctx);
2292     this._drawPackages(ctx);
2293     this._drawSlider();
2294 
2295     // restore original scaling and translation
2296     ctx.restore();
2297 };
2298 
2299 /**
2300  * Set the translation of the network
2301  * @param {Number} offsetX    Horizontal offset
2302  * @param {Number} offsetY    Vertical offset
2303  */
2304 links.Network.prototype._setTranslation = function(offsetX, offsetY) {
2305     if (this.translation === undefined) {
2306         this.translation = {
2307             "x": 0,
2308             "y": 0
2309         };
2310     }
2311 
2312     if (offsetX !== undefined) {
2313         this.translation.x = offsetX;
2314     }
2315     if (offsetY !== undefined) {
2316         this.translation.y = offsetY;
2317     }
2318 };
2319 
2320 /**
2321  * Get the translation of the network
2322  * @return {Object} translation    An object with parameters x and y, both a number
2323  */
2324 links.Network.prototype._getTranslation = function() {
2325     return {
2326         "x": this.translation.x,
2327         "y": this.translation.y
2328     };
2329 };
2330 
2331 /**
2332  * Scale the network
2333  * @param {Number} scale   Scaling factor 1.0 is unscaled
2334  */
2335 links.Network.prototype._setScale = function(scale) {
2336     this.scale = scale;
2337 };
2338 /**
2339  * Get the current scale of  the network
2340  * @return {Number} scale   Scaling factor 1.0 is unscaled
2341  */
2342 links.Network.prototype._getScale = function() {
2343     return this.scale;
2344 };
2345 
2346 links.Network.prototype._xToCanvas = function(x) {
2347     return (x - this.translation.x) / this.scale;
2348 };
2349 
2350 links.Network.prototype._canvasToX = function(x) {
2351     return x * this.scale + this.translation.x;
2352 };
2353 
2354 links.Network.prototype._yToCanvas = function(y) {
2355     return (y - this.translation.y) / this.scale;
2356 };
2357 
2358 links.Network.prototype._canvasToY = function(y) {
2359     return y * this.scale + this.translation.y ;
2360 };
2361 
2362 
2363 
2364 /**
2365  * Get a node by its id
2366  * @param {number} id
2367  * @return {Node}  node, or null if not found
2368  */
2369 links.Network.prototype._getNode = function(id) {
2370     for (var i = 0; i < this.nodes.length; i++) {
2371         if (this.nodes[i].id == id)
2372             return this.nodes[i];
2373     }
2374 
2375     return null;
2376 };
2377 
2378 /**
2379  * Redraw all nodes
2380  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
2381  * @param {CanvasRenderingContext2D}   ctx
2382  */
2383 links.Network.prototype._drawNodes = function(ctx) {
2384     // first draw the unselected nodes
2385     var nodes = this.nodes;
2386     var selected = [];
2387     for (var i = 0, iMax = nodes.length; i < iMax; i++) {
2388         if (nodes[i].isSelected()) {
2389             selected.push(i);
2390         }
2391         else {
2392             nodes[i].draw(ctx);
2393         }
2394     }
2395 
2396     // draw the selected nodes on top
2397     for (var s = 0, sMax = selected.length; s < sMax; s++) {
2398         nodes[selected[s]].draw(ctx);
2399     }
2400 };
2401 
2402 /**
2403  * Redraw all links
2404  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
2405  * @param {CanvasRenderingContext2D}   ctx
2406  */
2407 links.Network.prototype._drawLinks = function(ctx) {
2408     var links = this.links;
2409     for (var i = 0, iMax = links.length; i < iMax; i++) {
2410         links[i].draw(ctx);
2411     }
2412 };
2413 
2414 /**
2415  * Redraw all packages
2416  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
2417  * @param {CanvasRenderingContext2D}   ctx
2418  */
2419 links.Network.prototype._drawPackages = function(ctx) {
2420     var packages = this.packages;
2421     for (var i = 0, iMax = packages.length; i < iMax; i++) {
2422         packages[i].draw(ctx);
2423     }
2424 };
2425 
2426 
2427 /**
2428  * Redraw the filter
2429  */
2430 links.Network.prototype._drawSlider = function() {
2431     var sliderNode;
2432     if (this.hasTimestamps) {
2433         sliderNode = this.frame.slider;
2434         if (sliderNode === undefined) {
2435             sliderNode = document.createElement( "div" );
2436             sliderNode.style.position = "absolute";
2437             sliderNode.style.bottom = "0px";
2438             sliderNode.style.left = "0px";
2439             sliderNode.style.right = "0px";
2440             sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)";
2441 
2442             this.frame.slider = sliderNode;
2443             this.frame.slider.style.padding = "10px";
2444             //this.frame.filter.style.backgroundColor = "#EFEFEF";
2445             this.frame.appendChild(sliderNode);
2446 
2447 
2448             var range = this._getRange();
2449             this.slider = new links.Network.Slider(sliderNode);
2450             this.slider.setLoop(false);
2451             this.slider.setRange(range.start, range.end);
2452 
2453             // create an event handler
2454             var me = this;
2455             var onchange = function () {
2456                 var timestamp = me.slider.getValue();
2457                 me.setTimestamp(timestamp);
2458                 // TODO: do only a redraw when the network is not still moving
2459                 me.redraw();
2460             };
2461             this.slider.setOnChangeCallback(onchange);
2462             onchange(); // perform the first update by hand.
2463         }
2464     }
2465     else {
2466         sliderNode = this.frame.slider;
2467         if (sliderNode !== undefined) {
2468             this.frame.removeChild(sliderNode);
2469             this.frame.slider = undefined;
2470             this.slider = undefined;
2471         }
2472     }
2473 };
2474 
2475 /**
2476  * Recalculate the best positions for all nodes
2477  */
2478 links.Network.prototype._reposition = function() {
2479     // TODO: implement function reposition
2480 
2481 
2482     /*
2483      var w = this.frame.canvas.clientWidth;
2484      var h = this.frame.canvas.clientHeight;
2485      for (var i = 0; i < this.nodes.length; i++) {
2486      if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random();
2487      if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random();
2488      }
2489      //*/
2490 
2491     //*
2492     // TODO
2493     var radius = this.constants.links.length * 2;
2494     var cx =  this.frame.canvas.clientWidth / 2;
2495     var cy =  this.frame.canvas.clientHeight / 2;
2496     for (var i = 0; i < this.nodes.length; i++) {
2497         var angle = 2*Math.PI * (i / this.nodes.length);
2498 
2499         if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle);
2500         if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle);
2501 
2502     }
2503     //*/
2504 
2505     /*
2506      // TODO
2507      var radius = this.constants.links.length * 2;
2508      var w = this.frame.canvas.clientWidth,
2509      h = this.frame.canvas.clientHeight;
2510      var cx =  this.frame.canvas.clientWidth / 2;
2511      var cy =  this.frame.canvas.clientHeight / 2;
2512      var s = Math.sqrt(this.nodes.length);
2513      for (var i = 0; i < this.nodes.length; i++) {
2514      //var angle = 2*Math.PI * (i / this.nodes.length);
2515 
2516      if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s);
2517      if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s);
2518      }
2519      //*/
2520 
2521 
2522     /*
2523      var cx =  this.frame.canvas.clientWidth / 2;
2524      var cy =  this.frame.canvas.clientHeight / 2;  
2525      for (var i = 0; i < this.nodes.length; i++) {
2526      this.nodes[i].x = cx;
2527      this.nodes[i].y = cy;
2528      }
2529 
2530      //*/
2531 
2532 };
2533 
2534 
2535 /**
2536  * Find a stable position for all nodes
2537  */
2538 links.Network.prototype._doStabilize = function() {
2539     var start = new Date();
2540 
2541     // find stable position
2542     var count = 0;
2543     var vmin = this.constants.minVelocity;
2544     var stable = false;
2545     while (!stable && count < this.constants.maxIterations) {
2546         this._calculateForces();
2547         this._discreteStepNodes();
2548         stable = !this.isMoving(vmin);
2549         count++;
2550     }
2551 
2552     var end = new Date();
2553 
2554     //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
2555 };
2556 
2557 /**
2558  * Calculate the external forces acting on the nodes
2559  * Forces are caused by: links, repulsing forces between nodes, gravity
2560  */
2561 links.Network.prototype._calculateForces = function(nodeId) {
2562     // create a local link to the nodes and links, that is faster
2563     var nodes = this.nodes,
2564         links = this.links;
2565 
2566     // gravity, add a small constant force to pull the nodes towards the center of 
2567     // the graph 
2568     // Also, the forces are reset to zero in this loop by using _setForce instead
2569     // of _addForce
2570     var gravity = 0.01,
2571         gx = this.frame.canvas.clientWidth / 2,
2572         gy = this.frame.canvas.clientHeight / 2;
2573     for (var n = 0; n < nodes.length; n++) {
2574         var dx = gx - nodes[n].x,
2575             dy = gy - nodes[n].y,
2576             angle = Math.atan2(dy, dx),
2577             fx = Math.cos(angle) * gravity,
2578             fy = Math.sin(angle) * gravity;
2579 
2580         this.nodes[n]._setForce(fx, fy);
2581     }
2582 
2583     // repulsing forces between nodes
2584     var minimumDistance = this.constants.nodes.distance,
2585         steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
2586     for (var n = 0; n < nodes.length; n++) {
2587         for (var n2 = n + 1; n2 < this.nodes.length; n2++) {
2588             //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
2589             //var dmin = (nodes[n].width + nodes[n2].width)/2  || minimumDistance, // TODO: dmin
2590             //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
2591 
2592             // calculate normally distributed force
2593             var dx = nodes[n2].x - nodes[n].x,
2594                 dy = nodes[n2].y - nodes[n].y,
2595                 distance = Math.sqrt(dx * dx + dy * dy),
2596                 angle = Math.atan2(dy, dx),
2597 
2598             // TODO: correct factor for repulsing force
2599             //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
2600             //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
2601                 repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
2602                 fx = Math.cos(angle) * repulsingforce,
2603                 fy = Math.sin(angle) * repulsingforce;
2604 
2605             this.nodes[n]._addForce(-fx, -fy);
2606             this.nodes[n2]._addForce(fx, fy);
2607         }
2608         /* TODO: re-implement repulsion of links
2609         for (var l = 0; l < links.length; l++) {
2610         	var lx = links[l].from.x+(links[l].to.x - links[l].from.x)/2,
2611         	    ly = links[l].from.y+(links[l].to.y - links[l].from.y)/2,
2612         	    
2613                 // calculate normally distributed force
2614                 dx = nodes[n].x - lx,
2615                 dy = nodes[n].y - ly,
2616                 distance = Math.sqrt(dx * dx + dy * dy),
2617                 angle = Math.atan2(dy, dx),
2618  
2619 
2620             // TODO: correct factor for repulsing force
2621             //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
2622             //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
2623                 repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
2624                 fx = Math.cos(angle) * repulsingforce,
2625                 fy = Math.sin(angle) * repulsingforce;
2626             nodes[n]._addForce(fx, fy);
2627             links[l].from._addForce(-fx/2,-fy/2);
2628             links[l].to._addForce(-fx/2,-fy/2);            
2629         }
2630         */
2631     }
2632 
2633     // forces caused by the links, modelled as springs
2634     for (var l = 0, lMax = links.length; l < lMax; l++) {
2635         var link = links[l],
2636 
2637             dx = (link.to.x - link.from.x),
2638             dy = (link.to.y - link.from.y),
2639         //linkLength = (link.from.width + link.from.height + link.to.width + link.to.height)/2 || link.length, // TODO: dmin
2640         //linkLength = (link.from.width + link.to.width)/2 || link.length, // TODO: dmin
2641         //linkLength = 20 + ((link.from.width + link.to.width) || 0) / 2,
2642             linkLength = link.length,
2643             length =  Math.sqrt(dx * dx + dy * dy),
2644             angle = Math.atan2(dy, dx),
2645 
2646             springforce = link.stiffness * (linkLength - length),
2647 
2648             fx = Math.cos(angle) * springforce,
2649             fy = Math.sin(angle) * springforce;
2650 
2651         link.from._addForce(-fx, -fy);
2652         link.to._addForce(fx, fy);
2653     }
2654 
2655     /* TODO: re-implement repulsion of links
2656     // repulsing forces between links
2657     var minimumDistance = this.constants.links.distance,
2658         steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
2659     for (var l = 0; l < links.length; l++) {
2660     	//Keep distance from other link centers
2661     	for (var l2 = l + 1; l2 < this.links.length; l2++) {
2662         	//var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
2663             //var dmin = (nodes[n].width + nodes[n2].width)/2  || minimumDistance, // TODO: dmin
2664             //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
2665         	var lx = links[l].from.x+(links[l].to.x - links[l].from.x)/2,
2666         		ly = links[l].from.y+(links[l].to.y - links[l].from.y)/2,
2667         		l2x = links[l2].from.x+(links[l2].to.x - links[l2].from.x)/2,
2668         		l2y = links[l2].from.y+(links[l2].to.y - links[l2].from.y)/2,
2669                          	
2670             // calculate normally distributed force
2671             dx = l2x - lx,
2672             dy = l2y - ly,
2673             distance = Math.sqrt(dx * dx + dy * dy),
2674             angle = Math.atan2(dy, dx),
2675  
2676 
2677             // TODO: correct factor for repulsing force
2678             //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
2679             //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
2680             repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
2681             fx = Math.cos(angle) * repulsingforce,
2682             fy = Math.sin(angle) * repulsingforce;
2683         		
2684         	links[l].from._addForce(-fx, -fy);
2685         	links[l].to._addForce(-fx, -fy);
2686         	links[l2].from._addForce(fx, fy);
2687         	links[l2].to._addForce(fx, fy);            
2688         }
2689     }
2690     */
2691 };
2692 
2693 
2694 /**
2695  * Check if any of the nodes is still moving
2696  * @param {number} vmin   the minimum velocity considered as "moving"
2697  * @return {boolean}      true if moving, false if non of the nodes is moving
2698  */
2699 links.Network.prototype.isMoving = function(vmin) {
2700     // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
2701     var nodes = this.nodes;
2702     for (var n = 0, nMax = nodes.length; n < nMax; n++) {
2703         if (nodes[n].isMoving(vmin)) {
2704             return true;
2705         }
2706     }
2707     return false;
2708 };
2709 
2710 
2711 /**
2712  * Perform one discrete step for all nodes
2713  */
2714 links.Network.prototype._discreteStepNodes = function() {
2715     var interval = this.refreshRate / 1000.0; // in seconds
2716     var nodes = this.nodes;
2717     for (var n = 0, nMax = nodes.length; n < nMax; n++) {
2718         nodes[n].discreteStep(interval);
2719     }
2720 };
2721 
2722 
2723 /**
2724  * Perform one discrete step for all packages
2725  */
2726 links.Network.prototype._discreteStepPackages = function() {
2727     var interval = this.refreshRate / 1000.0; // in seconds
2728     var packages = this.packages;
2729     for (var n = 0, nMax = packages.length; n < nMax; n++) {
2730         packages[n].discreteStep(interval);
2731     }
2732 };
2733 
2734 
2735 /**
2736  * Cleanup finished packages.
2737  * also checks if there are moving packages
2738  */
2739 links.Network.prototype._deleteFinishedPackages = function() {
2740     var n = 0;
2741     var hasMovingPackages = false;
2742     while (n < this.packages.length) {
2743         if (this.packages[n].isFinished()) {
2744             this.packages.splice(n, 1);
2745             n--;
2746         }
2747         else if (this.packages[n].isMoving()) {
2748             hasMovingPackages = true;
2749         }
2750         n++;
2751     }
2752 
2753     this.hasMovingPackages = hasMovingPackages;
2754 };
2755 
2756 /**
2757  * Start animating nodes, links, and packages.
2758  */
2759 links.Network.prototype.start = function() {
2760     if (this.hasMovingNodes) {
2761         this._calculateForces();
2762         this._discreteStepNodes();
2763 
2764         var vmin = this.constants.minVelocity;
2765         this.hasMovingNodes = this.isMoving(vmin);
2766     }
2767 
2768     if (this.hasMovingPackages) {
2769         this._discreteStepPackages();
2770         this._deleteFinishedPackages();
2771     }
2772 
2773     if (this.hasMovingNodes || this.hasMovingLinks || this.hasMovingPackages) {
2774         // start animation. only start timer if it is not already running
2775         if (!this.timer) {
2776             var network = this;
2777             this.timer = window.setTimeout(function () {
2778                 network.timer = undefined;
2779                 network.start();
2780                 network._redraw();
2781             }, this.refreshRate);
2782         }
2783     }
2784     else {
2785         this._redraw();
2786     }
2787 };
2788 
2789 /**
2790  * Stop animating nodes, links, and packages.
2791  */
2792 links.Network.prototype.stop = function () {
2793     if (this.timer) {
2794         window.clearInterval(this.timer);
2795         this.timer = undefined;
2796     }
2797 };
2798 
2799 
2800 
2801 /**--------------------------------------------------------------------------**/
2802 
2803 
2804 /**
2805  * Add and event listener. Works for all browsers
2806  * @param {Element}     element    An html element
2807  * @param {String}      action     The action, for example "click",
2808  *                                 without the prefix "on"
2809  * @param {function}    listener   The callback function to be executed
2810  * @param {boolean}     useCapture
2811  */
2812 links.Network.addEventListener = function (element, action, listener, useCapture) {
2813     if (element.addEventListener) {
2814         if (useCapture === undefined)
2815             useCapture = false;
2816 
2817         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
2818             action = "DOMMouseScroll";  // For Firefox
2819         }
2820 
2821         element.addEventListener(action, listener, useCapture);
2822     } else {
2823         element.attachEvent("on" + action, listener);  // IE browsers
2824     }
2825 };
2826 
2827 /**
2828  * Remove an event listener from an element
2829  * @param {Element}  element   An html dom element
2830  * @param {string}       action    The name of the event, for example "mousedown"
2831  * @param {function}     listener  The listener function
2832  * @param {boolean}      useCapture
2833  */
2834 links.Network.removeEventListener = function(element, action, listener, useCapture) {
2835     if (element.removeEventListener) {
2836         // non-IE browsers
2837         if (useCapture === undefined)
2838             useCapture = false;
2839 
2840         if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
2841             action = "DOMMouseScroll";  // For Firefox
2842         }
2843 
2844         element.removeEventListener(action, listener, useCapture);
2845     } else {
2846         // IE browsers
2847         element.detachEvent("on" + action, listener);
2848     }
2849 };
2850 
2851 
2852 /**
2853  * Stop event propagation
2854  */
2855 links.Network.stopPropagation = function (event) {
2856     if (!event)
2857         event = window.event;
2858 
2859     if (event.stopPropagation) {
2860         event.stopPropagation();  // non-IE browsers
2861     }
2862     else {
2863         event.cancelBubble = true;  // IE browsers
2864     }
2865 };
2866 
2867 
2868 /**
2869  * Cancels the event if it is cancelable, without stopping further propagation of the event.
2870  */
2871 links.Network.preventDefault = function (event) {
2872     if (!event)
2873         event = window.event;
2874 
2875     if (event.preventDefault) {
2876         event.preventDefault();  // non-IE browsers
2877     }
2878     else {
2879         event.returnValue = false;  // IE browsers
2880     }
2881 };
2882 
2883 /**
2884  * Retrieve the absolute left value of a DOM element
2885  * @param {Element} elem    A dom element, for example a div
2886  * @return {number} left        The absolute left position of this element
2887  *                              in the browser page.
2888  */
2889 links.Network._getAbsoluteLeft = function(elem) {
2890     var left = 0;
2891     while( elem != null ) {
2892         left += elem.offsetLeft;
2893         left -= elem.scrollLeft;
2894         elem = elem.offsetParent;
2895     }
2896     if (!document.body.scrollLeft && window.pageXOffset) {
2897         // FF
2898         left -= window.pageXOffset;
2899     }
2900     return left;
2901 };
2902 
2903 /**
2904  * Retrieve the absolute top value of a DOM element
2905  * @param {Element} elem    A dom element, for example a div
2906  * @return {number} top         The absolute top position of this element
2907  *                              in the browser page.
2908  */
2909 links.Network._getAbsoluteTop = function(elem) {
2910     var top = 0;
2911     while( elem != null ) {
2912         top += elem.offsetTop;
2913         top -= elem.scrollTop;
2914         elem = elem.offsetParent;
2915     }
2916     if (!document.body.scrollTop && window.pageYOffset) {
2917         // FF
2918         top -= window.pageYOffset;
2919     }
2920     return top;
2921 };
2922 
2923 
2924 
2925 /**--------------------------------------------------------------------------**/
2926 
2927 
2928 /**
2929  * @class Node
2930  * A node. A node can be connected to other nodes via one or multiple links.
2931  * @param {object} properties An object containing properties for the node. All
2932  *                            properties are optional, except for the id.
2933  *                              {number} id     Id of the node. Required
2934  *                              {string} text   Title for the node
2935  *                              {number} x      Horizontal position of the node
2936  *                              {number} y      Vertical position of the node
2937  *                              {string} style  Drawing style, available:
2938  *                                              "database", "circle", "rect",
2939  *                                              "image", "text", "dot", "star",
2940  *                                              "triangle", "triangleDown",
2941  *                                              "square"
2942  *                              {string} image  An image url
2943  *                              {string} title  An title text, can be HTML
2944  *                              {anytype} group A group name or number
2945  * @param {links.Network.Images} imagelist    A list with images. Only needed
2946  *                                            when the node has an image
2947  * @param {links.Network.Groups} grouplist    A list with groups. Needed for
2948  *                                            retrieving group properties
2949  * @param {Object}               constants    An object with default values for
2950  *                                            example for the color
2951  */
2952 links.Network.Node = function (properties, imagelist, grouplist, constants) {
2953     this.selected = false;
2954 
2955     this.links = []; // all links connected to this node
2956     this.group = constants.nodes.group;
2957 
2958     this.fontSize = constants.nodes.fontSize;
2959     this.fontFace = constants.nodes.fontFace;
2960     this.fontColor = constants.nodes.fontColor;
2961 
2962     this.borderColor = constants.nodes.borderColor;
2963     this.backgroundColor = constants.nodes.backgroundColor;
2964     this.highlightColor = constants.nodes.highlightColor;
2965 
2966     // set defaults for the properties
2967     this.id = undefined;
2968     this.style = constants.nodes.style;
2969     this.image = constants.nodes.image;
2970     this.x = 0;
2971     this.y = 0;
2972     this.xFixed = false;
2973     this.yFixed = false;
2974     this.radius = constants.nodes.radius;
2975     this.radiusFixed = false;
2976     this.radiusMin = constants.nodes.radiusMin;
2977     this.radiusMax = constants.nodes.radiusMax;
2978 
2979     this.imagelist = imagelist;
2980     this.grouplist = grouplist;
2981 
2982     this.setProperties(properties, constants);
2983 
2984     // mass, force, velocity
2985     this.mass = 50;  // kg (mass is adjusted for the number of connected edges)
2986     this.fx = 0.0;  // external force x
2987     this.fy = 0.0;  // external force y
2988     this.vx = 0.0;  // velocity x
2989     this.vy = 0.0;  // velocity y
2990     this.minForce = constants.minForce;
2991     this.damping = 0.9; // damping factor
2992 };
2993 
2994 /**
2995  * Attach a link to the node
2996  * @param {links.Network.Link} link
2997  */
2998 links.Network.Node.prototype.attachLink = function(link) {
2999     this.links.push(link);
3000     this._updateMass();
3001 };
3002 
3003 /**
3004  * Detach a link from the node
3005  * @param {links.Network.Link} link
3006  */
3007 links.Network.Node.prototype.detachLink = function(link) {
3008     var index = this.links.indexOf(link);
3009     if (index != -1) {
3010         this.links.splice(index, 1);
3011     }
3012     this._updateMass();
3013 };
3014 
3015 /**
3016  * Update the nodes mass, which is determined by the number of edges connecting
3017  * to it (more edges -> heavier node).
3018  * @private
3019  */
3020 links.Network.Node.prototype._updateMass = function() {
3021     this.mass = 50 + 20 * this.links.length; // kg
3022 };
3023 
3024 /**
3025  * Set or overwrite properties for the node
3026  * @param {Object} properties an object with properties
3027  * @param {Object} constants  and object with default, global properties
3028  */
3029 links.Network.Node.prototype.setProperties = function(properties, constants) {
3030     if (!properties) {
3031         return;
3032     }
3033 
3034     // basic properties
3035     if (properties.id != undefined)        {this.id = properties.id;}
3036     if (properties.text != undefined)      {this.text = properties.text;}
3037     if (properties.title != undefined)     {this.title = properties.title;}
3038     if (properties.group != undefined)     {this.group = properties.group;}
3039     if (properties.x != undefined)         {this.x = properties.x;}
3040     if (properties.y != undefined)         {this.y = properties.y;}
3041     if (properties.value != undefined)     {this.value = properties.value;}
3042     if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
3043 
3044     if (this.id === undefined) {
3045         throw "Node must have an id";
3046     }
3047 
3048     // copy group properties
3049     if (this.group) {
3050         var groupObj = this.grouplist.get(this.group);
3051         for (var prop in groupObj) {
3052             if (groupObj.hasOwnProperty(prop)) {
3053                 this[prop] = groupObj[prop];
3054             }
3055         }
3056     }
3057 
3058     // individual style properties
3059     if (properties.style != undefined)          {this.style = properties.style;}
3060     if (properties.image != undefined)          {this.image = properties.image;}
3061     if (properties.radius != undefined)         {this.radius = properties.radius;}
3062     if (properties.borderColor != undefined)    {this.borderColor = properties.borderColor;}
3063     if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;}
3064     if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;}
3065     if (properties.fontColor != undefined)      {this.fontColor = properties.fontColor;}
3066     if (properties.fontSize != undefined)       {this.fontSize = properties.fontSize;}
3067     if (properties.fontFace != undefined)       {this.fontFace = properties.fontFace;}
3068 
3069 
3070     if (this.image != undefined) {
3071         if (this.imagelist) {
3072             this.imageObj = this.imagelist.load(this.image);
3073         }
3074         else {
3075             throw "No imagelist provided";
3076         }
3077     }
3078 
3079     this.xFixed = this.xFixed || (properties.x != undefined);
3080     this.yFixed = this.yFixed || (properties.y != undefined);
3081     this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
3082 
3083     if (this.style == 'image') {
3084         this.radiusMin = constants.nodes.widthMin;
3085         this.radiusMax = constants.nodes.widthMax;
3086     }
3087 
3088     // choose draw method depending on the style
3089     var style = this.style;
3090     switch (style) {
3091         case 'database':      this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
3092         case 'rect':          this.draw = this._drawRect; this.resize = this._resizeRect; break;
3093         case 'circle':        this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
3094         // TODO: add ellipse shape
3095         // TODO: add diamond shape
3096         case 'image':         this.draw = this._drawImage; this.resize = this._resizeImage; break;
3097         case 'text':          this.draw = this._drawText; this.resize = this._resizeText; break;
3098         case 'dot':           this.draw = this._drawDot; this.resize = this._resizeShape; break;
3099         case 'square':        this.draw = this._drawSquare; this.resize = this._resizeShape; break;
3100         case 'triangle':      this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
3101         case 'triangleDown':  this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
3102         case 'star':          this.draw = this._drawStar; this.resize = this._resizeShape; break;
3103         default:              this.draw = this._drawRect; this.resize = this._resizeRect; break;
3104     }
3105 
3106     // reset the size of the node, this can be changed
3107     this._reset();
3108 };
3109 
3110 /**
3111  * select this node
3112  */
3113 links.Network.Node.prototype.select = function() {
3114     this.selected = true;
3115     this._reset();
3116 };
3117 
3118 /**
3119  * unselect this node
3120  */
3121 links.Network.Node.prototype.unselect = function() {
3122     this.selected = false;
3123     this._reset();
3124 };
3125 
3126 /**
3127  * Reset the calculated size of the node, forces it to recalculate its size
3128  */
3129 links.Network.Node.prototype._reset = function() {
3130     this.width = undefined;
3131     this.height = undefined;
3132 };
3133 
3134 /**
3135  * get the title of this node.
3136  * @return {string} title    The title of the node, or undefined when no title
3137  *                           has been set.
3138  */
3139 links.Network.Node.prototype.getTitle = function() {
3140     return this.title;
3141 };
3142 
3143 /**
3144  * Calculate the distance to the border of the Node
3145  * @param {CanvasRenderingContext2D}   ctx
3146  * @param {Number} angle        Angle in radians
3147  * @returns {number} distance   Distance to the border in pixels
3148  */
3149 links.Network.Node.prototype.distanceToBorder = function (ctx, angle) {
3150     var borderWidth = 1;
3151 
3152     if (!this.width) {
3153         this.resize(ctx);
3154     }
3155 
3156     //noinspection FallthroughInSwitchStatementJS
3157     switch (this.style) {
3158         case 'circle':
3159         case 'dot':
3160             return this.radius + borderWidth;
3161 
3162         // TODO: implement distanceToBorder for database
3163         // TODO: implement distanceToBorder for triangle
3164         // TODO: implement distanceToBorder for triangleDown
3165 
3166         case 'rect':
3167         case 'image':
3168         case 'text':
3169         default:
3170             if (this.width) {
3171                 return Math.min(
3172                     Math.abs(this.width / 2 / Math.cos(angle)),
3173                     Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
3174                 // TODO: reckon with border radius too in case of rect
3175             }
3176             else {
3177                 return 0;
3178             }
3179 
3180     }
3181 
3182     // TODO: implement calculation of distance to border for all shapes
3183 };
3184 
3185 /**
3186  * Set forces acting on the node
3187  * @param {number} fx   Force in horizontal direction
3188  * @param {number} fy   Force in vertical direction
3189  */
3190 links.Network.Node.prototype._setForce = function(fx, fy) {
3191     this.fx = fx;
3192     this.fy = fy;
3193 };
3194 
3195 /**
3196  * Add forces acting on the node
3197  * @param {number} fx   Force in horizontal direction
3198  * @param {number} fy   Force in vertical direction
3199  */
3200 links.Network.Node.prototype._addForce = function(fx, fy) {
3201     this.fx += fx;
3202     this.fy += fy;
3203 };
3204 
3205 /**
3206  * Perform one discrete step for the node
3207  * @param {number} interval    Time interval in seconds
3208  */
3209 links.Network.Node.prototype.discreteStep = function(interval) {
3210     if (!this.xFixed) {
3211         var dx   = -this.damping * this.vx;     // damping force
3212         var ax   = (this.fx + dx) / this.mass;  // acceleration
3213         this.vx += ax / interval;               // velocity
3214         this.x  += this.vx / interval;          // position
3215     }
3216 
3217     if (!this.yFixed) {
3218         var dy   = -this.damping * this.vy;     // damping force
3219         var ay   = (this.fy + dy) / this.mass;  // acceleration
3220         this.vy += ay / interval;               // velocity
3221         this.y  += this.vy / interval;          // position
3222     }
3223 };
3224 
3225 
3226 /**
3227  * Check if this node has a fixed x and y position
3228  * @return {boolean}      true if fixed, false if not
3229  */
3230 links.Network.Node.prototype.isFixed = function() {
3231     return (this.xFixed && this.yFixed);
3232 };
3233 
3234 /**
3235  * Check if this node is moving
3236  * @param {number} vmin   the minimum velocity considered as "moving"
3237  * @return {boolean}      true if moving, false if it has no velocity
3238  */
3239 // TODO: replace this method with calculating the kinetic energy
3240 links.Network.Node.prototype.isMoving = function(vmin) {
3241     return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
3242         (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
3243         (!this.yFixed && Math.abs(this.fy) > this.minForce));
3244 };
3245 
3246 /**
3247  * check if this node is selecte
3248  * @return {boolean} selected   True if node is selected, else false
3249  */
3250 links.Network.Node.prototype.isSelected = function() {
3251     return this.selected;
3252 };
3253 
3254 /**
3255  * Retrieve the value of the node. Can be undefined
3256  * @return {Number} value
3257  */
3258 links.Network.Node.prototype.getValue = function() {
3259     return this.value;
3260 };
3261 
3262 /**
3263  * Calculate the distance from the nodes location to the given location (x,y)
3264  * @param {Number} x
3265  * @param {Number} y
3266  * @return {Number} value
3267  */
3268 links.Network.Node.prototype.getDistance = function(x, y) {
3269     var dx = this.x - x,
3270         dy = this.y - y;
3271     return Math.sqrt(dx * dx + dy * dy);
3272 };
3273 
3274 
3275 /**
3276  * Adjust the value range of the node. The node will adjust it's radius
3277  * based on its value.
3278  * @param {Number} min
3279  * @param {Number} max
3280  */
3281 links.Network.Node.prototype.setValueRange = function(min, max) {
3282     if (!this.radiusFixed && this.value !== undefined) {
3283         var scale = (this.radiusMax - this.radiusMin) / (max - min);
3284         this.radius = (this.value - min) * scale + this.radiusMin;
3285     }
3286 };
3287 
3288 /**
3289  * Draw this node in the given canvas
3290  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
3291  * @param {CanvasRenderingContext2D}   ctx
3292  */
3293 links.Network.Node.prototype.draw = function(ctx) {
3294     throw "Draw method not initialized for node";
3295 };
3296 
3297 /**
3298  * Recalculate the size of this node in the given canvas
3299  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
3300  * @param {CanvasRenderingContext2D}   ctx
3301  */
3302 links.Network.Node.prototype.resize = function(ctx) {
3303     throw "Resize method not initialized for node";
3304 };
3305 
3306 /**
3307  * Check if this object is overlapping with the provided object
3308  * @param {Object} obj   an object with parameters left, top, right, bottom
3309  * @return {boolean}     True if location is located on node
3310  */
3311 links.Network.Node.prototype.isOverlappingWith = function(obj) {
3312     return (this.left          < obj.right &&
3313         this.left + this.width > obj.left &&
3314         this.top               < obj.bottom &&
3315         this.top + this.height > obj.top);
3316 };
3317 
3318 links.Network.Node.prototype._resizeImage = function (ctx) {
3319     // TODO: pre calculate the image size
3320     if (!this.width) {  // undefined or 0
3321         var width, height;
3322         if (this.value) {
3323             var scale = this.imageObj.height / this.imageObj.width;
3324             width = this.radius || this.imageObj.width;
3325             height = this.radius * scale || this.imageObj.height;
3326         }
3327         else {
3328             width = this.imageObj.width;
3329             height = this.imageObj.height;
3330         }
3331         this.width  = width;
3332         this.height = height;
3333     }
3334 };
3335 
3336 links.Network.Node.prototype._drawImage = function (ctx) {
3337     this._resizeImage(ctx);
3338 
3339     this.left   = this.x - this.width / 2;
3340     this.top    = this.y - this.height / 2;
3341 
3342     var yText;
3343     if (this.imageObj) {
3344         ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
3345         yText = this.y + this.height / 2;
3346     }
3347     else {
3348         // image still loading... just draw the text for now
3349         yText = this.y;
3350     }
3351 
3352     this._text(ctx, this.text, this.x, yText, undefined, "top");
3353 };
3354 
3355 
3356 links.Network.Node.prototype._resizeRect = function (ctx) {
3357     if (!this.width) {
3358         var margin = 5;
3359         var textSize = this.getTextSize(ctx);
3360         this.width = textSize.width + 2 * margin;
3361         this.height = textSize.height + 2 * margin;
3362     }
3363 };
3364 
3365 links.Network.Node.prototype._drawRect = function (ctx) {
3366     this._resizeRect(ctx);
3367 
3368     this.left = this.x - this.width / 2;
3369     this.top = this.y - this.height / 2;
3370 
3371     ctx.strokeStyle = this.borderColor;
3372     ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
3373     ctx.lineWidth = this.selected ? 2.0 : 1.0;
3374     ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
3375     ctx.fill();
3376     ctx.stroke();
3377 
3378     this._text(ctx, this.text, this.x, this.y);
3379 };
3380 
3381 
3382 links.Network.Node.prototype._resizeDatabase = function (ctx) {
3383     if (!this.width) {
3384         var margin = 5;
3385         var textSize = this.getTextSize(ctx);
3386         var size = textSize.width + 2 * margin;
3387         this.width = size;
3388         this.height = size;
3389     }
3390 };
3391 
3392 links.Network.Node.prototype._drawDatabase = function (ctx) {
3393     this._resizeDatabase(ctx);
3394     this.left = this.x - this.width / 2;
3395     this.top = this.y - this.height / 2;
3396 
3397     ctx.strokeStyle = this.borderColor;
3398     ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
3399     ctx.lineWidth = this.selected ? 2.0 : 1.0;
3400     ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
3401     ctx.fill();
3402     ctx.stroke();
3403 
3404     this._text(ctx, this.text, this.x, this.y);
3405 };
3406 
3407 
3408 links.Network.Node.prototype._resizeCircle = function (ctx) {
3409     if (!this.width) {
3410         var margin = 5;
3411         var textSize = this.getTextSize(ctx);
3412         var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
3413         this.radius = diameter / 2;
3414 
3415         this.width = diameter;
3416         this.height = diameter;
3417     }
3418 };
3419 
3420 links.Network.Node.prototype._drawCircle = function (ctx) {
3421     this._resizeCircle(ctx);
3422     this.left = this.x - this.width / 2;
3423     this.top = this.y - this.height / 2;
3424 
3425     ctx.strokeStyle = this.borderColor;
3426     ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
3427     ctx.lineWidth = this.selected ? 2.0 : 1.0;
3428     ctx.circle(this.x, this.y, this.radius);
3429     ctx.fill();
3430     ctx.stroke();
3431 
3432     this._text(ctx, this.text, this.x, this.y);
3433 };
3434 
3435 links.Network.Node.prototype._drawDot = function (ctx) {
3436     this._drawShape(ctx, 'circle');
3437 };
3438 
3439 links.Network.Node.prototype._drawTriangle = function (ctx) {
3440     this._drawShape(ctx, 'triangle');
3441 };
3442 
3443 links.Network.Node.prototype._drawTriangleDown = function (ctx) {
3444     this._drawShape(ctx, 'triangleDown');
3445 };
3446 
3447 links.Network.Node.prototype._drawSquare = function (ctx) {
3448     this._drawShape(ctx, 'square');
3449 };
3450 
3451 links.Network.Node.prototype._drawStar = function (ctx) {
3452     this._drawShape(ctx, 'star');
3453 };
3454 
3455 links.Network.Node.prototype._resizeShape = function (ctx) {
3456     if (!this.width) {
3457         var size = 2 * this.radius;
3458         this.width = size;
3459         this.height = size;
3460     }
3461 };
3462 
3463 links.Network.Node.prototype._drawShape = function (ctx, shape) {
3464     this._resizeShape(ctx);
3465 
3466     this.left = this.x - this.width / 2;
3467     this.top = this.y - this.height / 2;
3468 
3469     ctx.strokeStyle = this.borderColor;
3470     ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
3471     ctx.lineWidth = this.selected ? 2.0 : 1.0;
3472 
3473     ctx[shape](this.x, this.y, this.radius);
3474     ctx.fill();
3475     ctx.stroke();
3476 
3477     if (this.text) {
3478         this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top');
3479     }
3480 };
3481 
3482 links.Network.Node.prototype._resizeText = function (ctx) {
3483     if (!this.width) {
3484         var margin = 5;
3485         var textSize = this.getTextSize(ctx);
3486         this.width = textSize.width + 2 * margin;
3487         this.height = textSize.height + 2 * margin;
3488     }
3489 };
3490 
3491 links.Network.Node.prototype._drawText = function (ctx) {
3492     this._resizeText(ctx);
3493     this.left = this.x - this.width / 2;
3494     this.top = this.y - this.height / 2;
3495 
3496     this._text(ctx, this.text, this.x, this.y);
3497 };
3498 
3499 
3500 links.Network.Node.prototype._text = function (ctx, text, x, y, align, baseline) {
3501     if (text) {
3502         ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
3503         ctx.fillStyle = this.fontColor || "black";
3504         ctx.textAlign = align || "center";
3505         ctx.textBaseline = baseline || "middle";
3506 
3507         var lines = text.split('\n'),
3508             lineCount = lines.length,
3509             fontSize = (this.fontSize + 4),
3510             yLine = y + (1 - lineCount) / 2 * fontSize;
3511 
3512         for (var i = 0; i < lineCount; i++) {
3513             ctx.fillText(lines[i], x, yLine);
3514             yLine += fontSize;
3515         }
3516     }
3517 };
3518 
3519 
3520 links.Network.Node.prototype.getTextSize = function(ctx) {
3521     if (this.text != undefined) {
3522         ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
3523 
3524         var lines = this.text.split('\n'),
3525             height = (this.fontSize + 4) * lines.length,
3526             width = 0;
3527 
3528         for (var i = 0, iMax = lines.length; i < iMax; i++) {
3529             width = Math.max(width, ctx.measureText(lines[i]).width);
3530         }
3531 
3532         return {"width": width, "height": height};
3533     }
3534     else {
3535         return {"width": 0, "height": 0};
3536     }
3537 };
3538 
3539 
3540 
3541 /**--------------------------------------------------------------------------**/
3542 
3543 
3544 /**
3545  * @class Link
3546  *
3547  * A link connects two nodes
3548  * @param {Object} properties     Object with properties. Must contain
3549  *                                At least properties from and to.
3550  *                                Available properties: from (number),
3551  *                                to (number), color (string),
3552  *                                width (number), style (string),
3553  *                                length (number), title (string)
3554  * @param {links.Network} network A network object, used to find and link to
3555  *                                nodes.
3556  * @param {Object} constants      An object with default values for
3557  *                                example for the color
3558  */
3559 links.Network.Link = function (properties, network, constants) {
3560     if (!network) {
3561         throw "No network provided";
3562     }
3563     this.network = network;
3564 
3565     // initialize constants
3566     this.widthMin = constants.links.widthMin;
3567     this.widthMax = constants.links.widthMax;
3568 
3569     // initialize variables
3570     this.id     = undefined;
3571     this.style  = constants.links.style;
3572     this.title  = undefined;
3573     this.width  = constants.links.width;
3574     this.value  = undefined;
3575     this.length = constants.links.length;
3576     
3577     // Added to support dashed lines
3578     // David Jordan
3579     // 2012-08-08
3580     this.dashlength = constants.links.dashlength;
3581     this.dashgap = constants.links.dashgap;
3582     this.altdashlength  = constants.links.altdashlength;
3583     
3584     this.stiffness = undefined; // depends on the length of the link
3585     this.color  = constants.links.color;
3586     this.timestamp  = undefined;
3587     this.widthFixed = false;
3588     this.lengthFixed = false;
3589 
3590     this.setProperties(properties, constants);
3591 };
3592 
3593 /**
3594  * Set or overwrite properties for the link
3595  * @param {Object} properties  an object with properties
3596  * @param {Object} constants   and object with default, global properties
3597  */
3598 links.Network.Link.prototype.setProperties = function(properties, constants) {
3599     if (!properties) {
3600         return;
3601     }
3602 
3603     if (properties.from != undefined) {this.from = this.network._getNode(properties.from);}
3604     if (properties.to != undefined) {this.to = this.network._getNode(properties.to);}
3605 
3606     if (properties.id != undefined)         {this.id = properties.id;}
3607     if (properties.style != undefined)      {this.style = properties.style;}
3608     if (properties.text != undefined)       {this.text = properties.text;}
3609     if (this.text) {
3610         this.fontSize = constants.links.fontSize;
3611         this.fontFace = constants.links.fontFace;
3612         this.fontColor = constants.links.fontColor;
3613         if (properties.fontColor != undefined)  {this.fontColor = properties.fontColor;}
3614         if (properties.fontSize != undefined)   {this.fontSize = properties.fontSize;}
3615         if (properties.fontFace != undefined)   {this.fontFace = properties.fontFace;}
3616     }
3617     if (properties.title != undefined)      {this.title = properties.title;}
3618     if (properties.width != undefined)      {this.width = properties.width;}
3619     if (properties.value != undefined)      {this.value = properties.value;}
3620     if (properties.length != undefined)     {this.length = properties.length;}
3621 
3622     // Added to support dashed lines
3623     // David Jordan
3624     // 2012-08-08
3625     if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
3626     if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
3627     if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
3628     
3629     if (properties.color != undefined) {this.color = properties.color;}
3630     if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
3631 
3632     if (!this.from) {
3633         throw "Node with id " + properties.from + " not found";
3634     }
3635     if (!this.to) {
3636         throw "Node with id " + properties.to + " not found";
3637     }
3638 
3639     this.widthFixed = this.widthFixed || (properties.width != undefined);
3640     this.lengthFixed = this.lengthFixed || (properties.length != undefined);
3641 
3642     this.stiffness = 1 / this.length;
3643 
3644     // initialize animation
3645     if (this.style === 'arrow') {
3646         this.arrows = [0.5];
3647         this.animation = false;
3648     }
3649     else if (this.style === 'arrow-end') {
3650         this.animation = false;
3651     }
3652     else if (this.style === 'moving-arrows') {
3653         this.arrows = [];
3654         var arrowCount = 3; // TODO: make customizable
3655         for (var a = 0; a < arrowCount; a++) {
3656             this.arrows.push(a / arrowCount);
3657         }
3658         this.animation = true;
3659     }
3660     else if (this.style === 'moving-dot') {
3661         this.dot = 0.0;
3662         this.animation = true;
3663     }
3664     else {
3665         this.animation = false;
3666     }
3667 
3668     // set draw method based on style
3669     switch (this.style) {
3670         case 'line':          this.draw = this._drawLine; break;
3671         case 'arrow':         this.draw = this._drawArrow; break;
3672         case 'arrow-end':     this.draw = this._drawArrowEnd; break;
3673         case 'moving-arrows': this.draw = this._drawMovingArrows; break;
3674         case 'moving-dot':    this.draw = this._drawMovingDot; break;
3675         case 'dash-line':     this.draw = this._drawDashLine; break;
3676         default:              this.draw = this._drawLine; break;
3677     }
3678 };
3679 
3680 
3681 
3682 /**
3683  * Check if a node has an animating contents. If so, the graph needs to be
3684  * redrawn regularly
3685  * @return {boolean}  true if this link needs animation, else false
3686  */
3687 links.Network.Link.prototype.isMoving = function() {
3688     // TODO: be able to set the interval somehow
3689 
3690     return this.animation;
3691 };
3692 
3693 /**
3694  * get the title of this link.
3695  * @return {string} title    The title of the link, or undefined when no title
3696  *                           has been set.
3697  */
3698 links.Network.Link.prototype.getTitle = function() {
3699     return this.title;
3700 };
3701 
3702 
3703 /**
3704  * Retrieve the value of the link. Can be undefined
3705  * @return {Number} value
3706  */
3707 links.Network.Link.prototype.getValue = function() {
3708     return this.value;
3709 }
3710 
3711 /**
3712  * Adjust the value range of the link. The link will adjust it's width
3713  * based on its value.
3714  * @param {Number} min
3715  * @param {Number} max
3716  */
3717 links.Network.Link.prototype.setValueRange = function(min, max) {
3718     if (!this.widthFixed && this.value !== undefined) {
3719         var factor = (this.widthMax - this.widthMin) / (max - min);
3720         this.width = (this.value - min) * factor + this.widthMin;
3721     }
3722 };
3723 
3724 
3725 /**
3726  * Check if the length is fixed.
3727  * @return {boolean} lengthFixed   True if the length is fixed, else false
3728  */
3729 links.Network.Link.prototype.isLengthFixed = function() {
3730     return this.lengthFixed;
3731 };
3732 
3733 /**
3734  * Retrieve the length of the link. Can be undefined
3735  * @return {Number} length
3736  */
3737 links.Network.Link.prototype.getLength = function() {
3738     return this.length;
3739 };
3740 
3741 /**
3742  * Adjust the length of the link. This can only be done when the length
3743  * is not fixed (which is the case when the link is created with a length property)
3744  * @param {Number} length
3745  */
3746 links.Network.Link.prototype.setLength = function(length) {
3747     if (!this.lengthFixed) {
3748         this.length = length;
3749     }
3750 };
3751 
3752 /**
3753  * Retrieve the length of the links dashes. Can be undefined
3754  * @author David Jordan
3755  * @date 2012-08-08
3756  * @return {Number} dashlength
3757  */
3758 links.Network.Link.prototype.getDashLength = function() {
3759     return this.dashlength;
3760 };
3761 
3762 /**
3763  * Adjust the length of the links dashes. 
3764  * @author David Jordan
3765  * @date 2012-08-08
3766  * @param {Number} dashlength
3767  */
3768 links.Network.Link.prototype.setDashLength = function(dashlength) {
3769     this.dashlength = dashlength;
3770 };
3771 
3772 /**
3773  * Retrieve the length of the links dashes gaps. Can be undefined
3774  * @author David Jordan
3775  * @date 2012-08-08
3776  * @return {Number} dashgap
3777  */
3778 links.Network.Link.prototype.getDashGap = function() {
3779     return this.dashgap;
3780 };
3781 
3782 /**
3783  * Adjust the length of the links dashes gaps. 
3784  * @author David Jordan
3785  * @date 2012-08-08
3786  * @param {Number} dashgap
3787  */
3788 links.Network.Link.prototype.setDashGap = function(dashgap) {
3789     this.dashgap = dashgap;
3790 };
3791 
3792 /**
3793  * Retrieve the length of the links alternate dashes. Can be undefined
3794  * @author David Jordan
3795  * @date 2012-08-08
3796  * @return {Number} altdashlength
3797  */
3798 links.Network.Link.prototype.getAltDashLength = function() {
3799     return this.altdashlength;
3800 };
3801 
3802 /**
3803  * Adjust the length of the links alternate dashes.
3804  * @author David Jordan
3805  * @date 2012-08-08
3806  * @param {Number} altdashlength
3807  */
3808 links.Network.Link.prototype.setAltDashLength = function(altdashlength) {
3809     this.altdashlength = altdashlength;
3810 };
3811 
3812 
3813 
3814 /**
3815  * Redraw a link
3816  * Draw this link in the given canvas
3817  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
3818  * @param {CanvasRenderingContext2D}   ctx
3819  */
3820 links.Network.Link.prototype.draw = function(ctx) {
3821     throw "Method draw not initialized in link";
3822 };
3823 
3824 
3825 /**
3826  * Check if this object is overlapping with the provided object
3827  * @param {Object} obj   an object with parameters left, top
3828  * @return {boolean}     True if location is located on the link
3829  */
3830 links.Network.Link.prototype.isOverlappingWith = function(obj) {
3831     var distMax = 10;
3832 
3833     var xFrom = this.from.x;
3834     var yFrom = this.from.y;
3835     var xTo = this.to.x;
3836     var yTo = this.to.y;
3837     var xObj = obj.left;
3838     var yObj = obj.top;
3839 
3840 
3841     var dist = links.Network._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
3842 
3843     return (dist < distMax);
3844 };
3845 
3846 /**
3847  * Calculate the distance between a point (x3,y3) and a line segment from
3848  * (x1,y1) to (x2,y2).
3849  * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
3850  * @param {number} x1
3851  * @param {number} y1
3852  * @param {number} x2
3853  * @param {number} y2
3854  * @param {number} x3
3855  * @param {number} y3
3856  */
3857 links.Network._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
3858     var px = x2-x1,
3859         py = y2-y1,
3860         something = px*px + py*py,
3861         u =  ((x3 - x1) * px + (y3 - y1) * py) / something;
3862 
3863     if (u > 1) {
3864         u = 1;
3865     }
3866     else if (u < 0) {
3867         u = 0;
3868     }
3869 
3870     var x = x1 + u * px,
3871         y = y1 + u * py,
3872         dx = x - x3,
3873         dy = y - y3;
3874 
3875     //# Note: If the actual distance does not matter,
3876     //# if you only want to compare what this function
3877     //# returns to other results of this function, you
3878     //# can just return the squared distance instead
3879     //# (i.e. remove the sqrt) to gain a little performance
3880 
3881     return Math.sqrt(dx*dx + dy*dy);
3882 };
3883 
3884 /**
3885  * Redraw a link as a line
3886  * Draw this link in the given canvas
3887  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
3888  * @param {CanvasRenderingContext2D}   ctx
3889  */
3890 links.Network.Link.prototype._drawLine = function(ctx) {
3891     // set style
3892     ctx.strokeStyle = this.color;
3893     ctx.lineWidth = this._getLineWidth();
3894 
3895     var point;
3896     if (this.from != this.to) {
3897         // draw line
3898         this._line(ctx);
3899 
3900         // draw text
3901         if (this.text) {
3902             point = this._pointOnLine(0.5);
3903             this._text(ctx, this.text, point.x, point.y);
3904         }
3905     }
3906     else {
3907         var radius = this.length / 2 / Math.PI;
3908         var x, y;
3909         var node = this.from;
3910         if (!node.width) {
3911             node.resize(ctx);
3912         }
3913         if (node.width > node.height) {
3914             x = node.x + node.width / 2;
3915             y = node.y - radius;
3916         }
3917         else {
3918             x = node.x + radius;
3919             y = node.y - node.height / 2;
3920         }
3921         this._circle(ctx, x, y, radius);
3922         point = this._pointOnCircle(x, y, radius, 0.5);
3923         this._text(ctx, this.text, point.x, point.y);
3924     }
3925 };
3926 
3927 /**
3928  * Get the line width of the link. Depends on width and whether one of the
3929  * connected nodes is selected.
3930  * @return {Number} width
3931  * @private
3932  */
3933 links.Network.Link.prototype._getLineWidth = function() {
3934     if (this.from.selected || this.to.selected) {
3935         return Math.min(this.width * 2, this.widthMax);
3936     }
3937     else {
3938         return this.width;
3939     }
3940 };
3941 
3942 /**
3943  * Draw a line between two nodes
3944  * @param {CanvasRenderingContext2D} ctx
3945  * @private
3946  */
3947 links.Network.Link.prototype._line = function (ctx) {
3948     // draw a straight line
3949     ctx.beginPath();
3950     ctx.moveTo(this.from.x, this.from.y);
3951     ctx.lineTo(this.to.x, this.to.y);
3952     ctx.stroke();
3953 };
3954 
3955 /**
3956  * Draw a line from a node to itself, a circle
3957  * @param {CanvasRenderingContext2D} ctx
3958  * @param {Number} x
3959  * @param {Number} y
3960  * @param {Number} radius
3961  * @private
3962  */
3963 links.Network.Link.prototype._circle = function (ctx, x, y, radius) {
3964     // draw a circle
3965     ctx.beginPath();
3966     ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
3967     ctx.stroke();
3968 };
3969 
3970 /**
3971  * Draw text with white background and with the middle at (x, y)
3972  * @param {CanvasRenderingContext2D} ctx
3973  * @param {String} text
3974  * @param {Number} x
3975  * @param {Number} y
3976  */
3977 links.Network.Link.prototype._text = function (ctx, text, x, y) {
3978     if (text) {
3979         // TODO: cache the calculated size
3980         ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
3981             this.fontSize + "px " + this.fontFace;
3982         ctx.fillStyle = 'white';
3983         var width = ctx.measureText(this.text).width;
3984         var height = this.fontSize;
3985         var left = x - width / 2;
3986         var top = y - height / 2;
3987 
3988         ctx.fillRect(left, top, width, height);
3989 
3990         // draw text
3991         ctx.fillStyle = this.fontColor || "black";
3992         ctx.textAlign = "left";
3993         ctx.textBaseline = "top";
3994         ctx.fillText(this.text, left, top);
3995     }
3996 };
3997 
3998 /**
3999  * Sets up the dashedLine functionality for drawing
4000  * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
4001  * @author David Jordan
4002  * @date 2012-08-08
4003  */
4004 var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
4005 if (CP && CP.lineTo){
4006   CP.dashedLine = function(x,y,x2,y2,dashArray){
4007     if (!dashArray) dashArray=[10,5];
4008     if (dashLength==0) dashLength = 0.001; // Hack for Safari
4009     var dashCount = dashArray.length;
4010     this.moveTo(x, y);
4011     var dx = (x2-x), dy = (y2-y);
4012     var slope = dy/dx;
4013     var distRemaining = Math.sqrt( dx*dx + dy*dy );
4014     var dashIndex=0, draw=true;
4015     while (distRemaining>=0.1){
4016       var dashLength = dashArray[dashIndex++%dashCount];
4017       if (dashLength > distRemaining) dashLength = distRemaining;
4018       var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
4019       if (dx<0) xStep = -xStep;
4020       x += xStep
4021       y += slope*xStep;
4022       this[draw ? 'lineTo' : 'moveTo'](x,y);
4023       distRemaining -= dashLength;
4024       draw = !draw;
4025     }
4026   }
4027 }
4028 
4029 /**
4030  * Redraw a link as a dashed line
4031  * Draw this link in the given canvas
4032  * @author David Jordan
4033  * @date 2012-08-08
4034  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4035  * @param {CanvasRenderingContext2D}   ctx
4036  */
4037 links.Network.Link.prototype._drawDashLine = function(ctx) {
4038     // set style
4039     ctx.strokeStyle = this.color;
4040     ctx.lineWidth = this._getLineWidth();
4041 
4042     // draw dashed line
4043     ctx.beginPath();
4044     ctx.lineCap = 'round';
4045 	if (this.altdashlength != undefined) //If an alt dash value has been set add to the array this value
4046     {
4047     	ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]);
4048     }
4049     else if (this.dashlength != undefined && this.dashgap != undefined) //If a dash and gap value has been set add to the array this value
4050     {
4051     	ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]);
4052     }
4053     else //If all else fails draw a line
4054 	{
4055     	ctx.moveTo(this.from.x, this.from.y);
4056         ctx.lineTo(this.to.x, this.to.y);
4057 	}
4058     ctx.stroke();
4059 
4060     // draw text
4061     if (this.text) {
4062         var point = this._pointOnLine(0.5);
4063         this._text(ctx, this.text, point.x, point.y);
4064     }
4065 };
4066 
4067 /**
4068  * Get a point on a line
4069  * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
4070  * @return {Object} point
4071  * @private
4072  */
4073 links.Network.Link.prototype._pointOnLine = function (percentage) {
4074     return {
4075         x: (1 - percentage) * this.from.x + percentage * this.to.x,
4076         y: (1 - percentage) * this.from.y + percentage * this.to.y
4077     }
4078 };
4079 
4080 /**
4081  * Get a point on a circle
4082  * @param {Number} x
4083  * @param {Number} y
4084  * @param {Number} radius
4085  * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
4086  * @return {Object} point
4087  * @private
4088  */
4089 links.Network.Link.prototype._pointOnCircle = function (x, y, radius, percentage) {
4090     var angle = (percentage - 3/8) * 2 * Math.PI;
4091     return {
4092         x: x + radius * Math.cos(angle),
4093         y: y - radius * Math.sin(angle)
4094     }
4095 };
4096 
4097 /**
4098  * Redraw a link as a line with a moving arrow
4099  * Draw this link in the given canvas
4100  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4101  * @param {CanvasRenderingContext2D}   ctx
4102  */
4103 links.Network.Link.prototype._drawMovingArrows = function(ctx) {
4104     this._drawArrow(ctx);
4105 
4106     for (var a in this.arrows) {
4107         if (this.arrows.hasOwnProperty(a)) {
4108             this.arrows[a] += 0.02;  // TODO determine speed from interval
4109             if (this.arrows[a] > 1.0) this.arrows[a] = 0.0;
4110         }
4111     }
4112 };
4113 
4114 /**
4115  * Redraw a link as a line with a moving dot
4116  * Draw this link in the given canvas
4117  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4118  * @param {CanvasRenderingContext2D}   ctx
4119  */
4120 links.Network.Link.prototype._drawMovingDot = function(ctx) {
4121     // set style
4122     ctx.strokeStyle = this.color;
4123     ctx.fillStyle = this.color;
4124     ctx.lineWidth = this._getLineWidth();
4125 
4126     // draw line
4127     var point;
4128     if (this.from != this.to) {
4129         this._line(ctx);
4130 
4131         // draw dot
4132         var radius = 4 + this.width * 2;
4133         point = this._pointOnLine(this.dot);
4134         ctx.circle(point.x, point.y, radius);
4135         ctx.fill();
4136 
4137         // move the dot to the next position
4138         this.dot += 0.05;  // TODO determine speed from interval
4139         if (this.dot > 1.0) this.dot = 0.0;
4140 
4141         // draw text
4142         if (this.text) {
4143             point = this._pointOnLine(0.5);
4144             this._text(ctx, this.text, point.x, point.y);
4145         }
4146     }
4147     else {
4148         // TODO: moving dot for a circular edge
4149     }
4150 };
4151 
4152 
4153 /**
4154  * Redraw a link as a line with an arrow
4155  * Draw this link in the given canvas
4156  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4157  * @param {CanvasRenderingContext2D}   ctx
4158  */
4159 links.Network.Link.prototype._drawArrow = function(ctx) {
4160     var point;
4161     // set style
4162     ctx.strokeStyle = this.color;
4163     ctx.fillStyle = this.color;
4164     ctx.lineWidth = this._getLineWidth();
4165 
4166     if (this.from != this.to) {
4167         // draw line
4168         this._line(ctx);
4169 
4170         // draw all arrows
4171         var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
4172         var length = 10 + 5 * this.width; // TODO: make customizable?
4173         for (var a in this.arrows) {
4174             if (this.arrows.hasOwnProperty(a)) {
4175                 point = this._pointOnLine(this.arrows[a]);
4176                 ctx.arrow(point.x, point.y, angle, length);
4177                 ctx.fill();
4178                 ctx.stroke();
4179             }
4180         }
4181 
4182         // draw text
4183         if (this.text) {
4184             point = this._pointOnLine(0.5);
4185             this._text(ctx, this.text, point.x, point.y);
4186         }
4187     }
4188     else {
4189         // draw circle
4190         var radius = this.length / 2 / Math.PI;
4191         var x, y;
4192         var node = this.from;
4193         if (!node.width) {
4194             node.resize(ctx);
4195         }
4196         if (node.width > node.height) {
4197             x = node.x + node.width / 2;
4198             y = node.y - radius;
4199         }
4200         else {
4201             x = node.x + radius;
4202             y = node.y - node.height / 2;
4203         }
4204         this._circle(ctx, x, y, radius);
4205 
4206         // draw all arrows
4207         var angle = 0.2 * Math.PI;
4208         var length = 10 + 5 * this.width; // TODO: make customizable?
4209         for (var a in this.arrows) {
4210             if (this.arrows.hasOwnProperty(a)) {
4211                 point = this._pointOnCircle(x, y, radius, this.arrows[a]);
4212                 ctx.arrow(point.x, point.y, angle, length);
4213                 ctx.fill();
4214                 ctx.stroke();
4215             }
4216         }
4217 
4218         // draw text
4219         if (this.text) {
4220             point = this._pointOnCircle(x, y, radius, 0.5);
4221             this._text(ctx, this.text, point.x, point.y);
4222         }
4223     }
4224 };
4225 
4226 
4227 
4228 /**
4229  * Redraw a link as a line with an arrow
4230  * Draw this link in the given canvas
4231  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4232  * @param {CanvasRenderingContext2D}   ctx
4233  */
4234 links.Network.Link.prototype._drawArrowEnd = function(ctx) {
4235     // set style
4236     ctx.strokeStyle = this.color;
4237     ctx.fillStyle = this.color;
4238     ctx.lineWidth = this._getLineWidth();
4239 
4240     // draw line
4241     var angle, length;
4242     if (this.from != this.to) {
4243         // calculate length and angle of the line
4244         angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
4245         var dx = (this.to.x - this.from.x);
4246         var dy = (this.to.y - this.from.y);
4247         var lLink = Math.sqrt(dx * dx + dy * dy);
4248 
4249         var lFrom = this.to.distanceToBorder(ctx, angle + Math.PI);
4250         var pFrom = (lLink - lFrom) / lLink;
4251         var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
4252         var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
4253 
4254         var lTo = this.to.distanceToBorder(ctx, angle);
4255         var pTo = (lLink - lTo) / lLink;
4256         var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
4257         var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
4258 
4259         ctx.beginPath();
4260         ctx.moveTo(xFrom, yFrom);
4261         ctx.lineTo(xTo, yTo);
4262         ctx.stroke();
4263 
4264         // draw arrow at the end of the line
4265         length = 10 + 5 * this.width; // TODO: make customizable?
4266         ctx.arrow(xTo, yTo, angle, length);
4267         ctx.fill();
4268         ctx.stroke();
4269 
4270         // draw text
4271         if (this.text) {
4272             var point = this._pointOnLine(0.5);
4273             this._text(ctx, this.text, point.x, point.y);
4274         }
4275     }
4276     else {
4277         // draw circle
4278         var radius = this.length / 2 / Math.PI;
4279         var x, y, arrow;
4280         var node = this.from;
4281         if (!node.width) {
4282             node.resize(ctx);
4283         }
4284         if (node.width > node.height) {
4285             x = node.x + node.width / 2;
4286             y = node.y - radius;
4287             arrow = {
4288                 x: x,
4289                 y: node.y,
4290                 angle: 0.9 * Math.PI
4291             };
4292         }
4293         else {
4294             x = node.x + radius;
4295             y = node.y - node.height / 2;
4296             arrow = {
4297                 x: node.x,
4298                 y: y,
4299                 angle: 0.6 * Math.PI
4300             };
4301         }
4302         ctx.beginPath();
4303         // TODO: do not draw a circle, but an arc
4304         // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
4305         ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
4306         ctx.stroke();
4307 
4308         // draw all arrows
4309         length = 10 + 5 * this.width; // TODO: make customizable?
4310         ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
4311         ctx.fill();
4312         ctx.stroke();
4313 
4314         // draw text
4315         if (this.text) {
4316             point = this._pointOnCircle(x, y, radius, 0.5);
4317             this._text(ctx, this.text, point.x, point.y);
4318         }
4319     }
4320 
4321 };
4322 
4323 /**--------------------------------------------------------------------------**/
4324 
4325 
4326 /**
4327  * @class Images
4328  * This class loades images and keeps them stored.
4329  */
4330 links.Network.Images = function () {
4331     this.images = {};
4332 
4333     this.callback = undefined;
4334 };
4335 
4336 /**
4337  * Set an onload callback function. This will be called each time an image
4338  * is loaded
4339  * @param {function} callback
4340  */
4341 links.Network.Images.prototype.setOnloadCallback = function(callback) {
4342     this.callback = callback;
4343 };
4344 
4345 
4346 /**
4347  *
4348  * @param {string} url          Url of the image
4349  * @return {Image} img          The image object
4350  */
4351 links.Network.Images.prototype.load = function(url) {
4352     var img = this.images[url];
4353     if (img == undefined) {
4354         // create the image
4355         var images = this;
4356         img = new Image();
4357         this.images[url] = img;
4358         img.onload = function() {
4359             if (images.callback) {
4360                 images.callback(this);
4361             }
4362         };
4363         img.src = url;
4364     }
4365 
4366     return img;
4367 };
4368 
4369 
4370 /**--------------------------------------------------------------------------**/
4371 
4372 
4373 /**
4374  * @class Package
4375  * This class contains one package
4376  *
4377  * @param {number} properties  Properties for the package. Optional. Available
4378  *                             properties are: id {number}, title {string},
4379  *                             style {string} with available values "dot" and
4380  *                             "image", radius {number}, image {string},
4381  *                             color {string}, progress {number} with a value
4382  *                             between 0-1, duration {number}, timestamp {number
4383  *                             or Date}.
4384  * @param {links.Network}      network        The network object, used to find
4385  *                                            and link to nodes.
4386  * @param {links.Network.Images} imagelist    An Images object. Only needed
4387  *                                            when the package has style 'image'
4388  * @param {Object}               constants    An object with default values for
4389  *                                            example for the color
4390  */
4391 links.Network.Package = function (properties, network, imagelist, constants) {
4392     if (network == undefined) {
4393         throw "No network provided";
4394     }
4395 
4396     // constants
4397     this.radiusMin = constants.packages.radiusMin;
4398     this.radiusMax = constants.packages.radiusMax;
4399     this.imagelist = imagelist;
4400     this.network = network;
4401 
4402     // initialize variables
4403     this.id =        undefined;
4404     this.from =      undefined;
4405     this.to =        undefined;
4406     this.title =     undefined;
4407     this.style =     constants.packages.style;
4408     this.radius =    constants.packages.radius;
4409     this.color =     constants.packages.color;
4410     this.image =     constants.packages.image;
4411     this.value =     undefined;
4412     this.progress =  0.0;
4413     this.timestamp = undefined;
4414     this.duration = constants.packages.duration;
4415     this.autoProgress = true;
4416     this.radiusFixed = false;
4417 
4418     // set properties
4419     this.setProperties(properties, constants);
4420 };
4421 
4422 links.Network.Package.DEFAULT_DURATION = 1.0; // seconds
4423 
4424 /**
4425  * Set or overwrite properties for the package
4426  * @param {Object} properties an object with properties
4427  * @param {Object} constants  and object with default, global properties
4428  */
4429 links.Network.Package.prototype.setProperties = function(properties, constants) {
4430     if (!properties) {
4431         return;
4432     }
4433 
4434     // note that the provided properties can also be null, when they come from the Google DataTable
4435     if (properties.from != undefined) {this.from = this.network._getNode(properties.from);}
4436     if (properties.to != undefined) {this.to = this.network._getNode(properties.to);}
4437 
4438     if (!this.from) {
4439         throw "Node with id " + properties.from + " not found";
4440     }
4441     if (!this.to) {
4442         throw "Node with id " + properties.to + " not found";
4443     }
4444 
4445     if (properties.id != undefined) {this.id = properties.id;}
4446     if (properties.title != undefined) {this.title = properties.title;}
4447     if (properties.style != undefined) {this.style = properties.style;}
4448     if (properties.radius != undefined) {this.radius = properties.radius;}
4449     if (properties.value != undefined) {this.value = properties.value;}
4450     if (properties.image != undefined) {this.image = properties.image;}
4451     if (properties.color != undefined) {this.color = properties.color;}
4452     if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
4453     if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
4454     if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
4455     if (properties.progress != undefined) {this.progress = properties.progress;}
4456     if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
4457     if (properties.duration != undefined) {this.duration = properties.duration;}
4458 
4459     this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
4460     this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false;
4461 
4462     if (this.style == 'image') {
4463         this.radiusMin = constants.packages.widthMin;
4464         this.radiusMax = constants.packages.widthMax;
4465     }
4466 
4467     // handle progress
4468     if (this.progress < 0.0) {this.progress = 0.0;}
4469     if (this.progress > 1.0) {this.progress = 1.0;}
4470 
4471     // handle image
4472     if (this.image != undefined) {
4473         if (this.imagelist) {
4474             this.imageObj = this.imagelist.load(this.image);
4475         }
4476         else {
4477             throw "No imagelist provided";
4478         }
4479     }
4480 
4481     // choose draw method depending on the style
4482     switch (this.style) {
4483         // TODO: add more styles
4484         case 'dot':         this.draw = this._drawDot; break;
4485         case 'square':      this.draw = this._drawSquare; break;
4486         case 'triangle':    this.draw = this._drawTriangle; break;
4487         case 'triangleDown':this.draw = this._drawTriangleDown; break;
4488         case 'star':        this.draw = this._drawStar; break;
4489         case 'image':       this.draw = this._drawImage; break;
4490         default:            this.draw = this._drawDot; break;
4491     }
4492 };
4493 
4494 /**
4495  * Set a new value for the progress of the package
4496  * @param {number} progress    A value between 0 and 1
4497  */
4498 links.Network.Package.prototype.setProgress = function (progress) {
4499     this.progress = progress;
4500     this.autoProgress = false;
4501 };
4502 
4503 /**
4504  * Check if a package is finished, if it has reached its destination.
4505  * If so, the package can be removed.
4506  * Only packages with automatically animated progress can be finished
4507  * @return {boolean}    true if finished, else false.
4508  */
4509 links.Network.Package.prototype.isFinished = function () {
4510     return (this.autoProgress == true && this.progress >= 1.0);
4511 };
4512 
4513 /**
4514  * Check if this package is moving.
4515  * A packages moves when it has automatic progress and not yet reached its
4516  * destination.
4517  * @return {boolean}    true if moving, else false.
4518  */
4519 links.Network.Package.prototype.isMoving = function () {
4520     return (this.autoProgress || this.isFinished());
4521 };
4522 
4523 
4524 /**
4525  * Perform one discrete step for the package. Only applicable when the
4526  * package has no manually set, fixed progress.
4527  * @param {number} interval    Time interval in seconds
4528  */
4529 links.Network.Package.prototype.discreteStep = function(interval) {
4530     if (this.autoProgress == true) {
4531         this.progress += (parseFloat(interval) / this.duration);
4532 
4533         if (this.progress > 1.0)
4534             this.progress = 1.0;
4535     }
4536 };
4537 
4538 
4539 /**
4540  * Draw this package in the given canvas
4541  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4542  * @param {CanvasRenderingContext2D}   ctx
4543  */
4544 links.Network.Package.prototype.draw = function(ctx) {
4545     throw "Draw method not initialized for package";
4546 };
4547 
4548 
4549 /**
4550  * Check if this object is overlapping with the provided object
4551  * @param {Object} obj   an object with parameters left, top, right, bottom
4552  * @return {boolean}     True if location is located on node
4553  */
4554 links.Network.Package.prototype.isOverlappingWith = function(obj) {
4555     // radius minimum 10px else it is too hard to get your mouse at the exact right position
4556     var radius = Math.max(this.radius, 10);
4557     var pos = this._getPosition();
4558 
4559     return (pos.x - radius < obj.right &&
4560         pos.x + radius > obj.left &&
4561         pos.y - radius < obj.bottom &&
4562         pos.y + radius > obj.top);
4563 };
4564 
4565 /**
4566  * Calculate the current position of the package
4567  * @return {Object} position    The object has parameters x and y.
4568  */
4569 links.Network.Package.prototype._getPosition = function() {
4570     return {
4571         "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x,
4572         "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y
4573     };
4574 };
4575 
4576 
4577 /**
4578  * get the title of this package.
4579  * @return {string} title    The title of the package, or undefined when no
4580  *                           title has been set.
4581  */
4582 links.Network.Package.prototype.getTitle = function() {
4583     return this.title;
4584 };
4585 
4586 /**
4587  * Retrieve the value of the package. Can be undefined
4588  * @return {Number} value
4589  */
4590 links.Network.Package.prototype.getValue = function() {
4591     return this.value;
4592 };
4593 
4594 /**
4595  * Calculate the distance from the packages location to the given location (x,y)
4596  * @param {Number} x
4597  * @param {Number} y
4598  * @return {Number} value
4599  */
4600 links.Network.Package.prototype.getDistance = function(x, y) {
4601     var pos = this._getPosition(),
4602         dx = pos.x - x,
4603         dy = pos.y - y;
4604     return Math.sqrt(dx * dx + dy * dy);
4605 };
4606 
4607 /**
4608  * Adjust the value range of the package. The package will adjust it's radius
4609  * based on its value.
4610  * @param {Number} min
4611  * @param {Number} max
4612  */
4613 links.Network.Package.prototype.setValueRange = function(min, max) {
4614     if (!this.radiusFixed && this.value !== undefined) {
4615         var factor = (this.radiusMax - this.radiusMin) / (max - min);
4616         this.radius = (this.value - min) * factor + this.radiusMin;
4617     }
4618 };
4619 
4620 
4621 
4622 /**
4623  * Redraw a package as a dot
4624  * Draw this link in the given canvas
4625  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4626  * @param {CanvasRenderingContext2D}   ctx
4627  */
4628 /* TODO: cleanup
4629  links.Network.Package.prototype._drawDot = function(ctx) {
4630  // set style
4631  ctx.fillStyle = this.color;
4632  // draw dot
4633  var pos = this._getPosition();
4634  ctx.circle(pos.x, pos.y, this.radius);
4635  ctx.fill();
4636  }
4637  */
4638 
4639 links.Network.Package.prototype._drawDot = function (ctx) {
4640     this._drawShape(ctx, 'circle');
4641 };
4642 
4643 links.Network.Package.prototype._drawTriangle = function (ctx) {
4644     this._drawShape(ctx, 'triangle');
4645 };
4646 
4647 links.Network.Package.prototype._drawTriangleDown = function (ctx) {
4648     this._drawShape(ctx, 'triangleDown');
4649 };
4650 
4651 links.Network.Package.prototype._drawSquare = function (ctx) {
4652     this._drawShape(ctx, 'square');
4653 };
4654 
4655 links.Network.Package.prototype._drawStar = function (ctx) {
4656     this._drawShape(ctx, 'star');
4657 };
4658 
4659 links.Network.Package.prototype._drawShape = function (ctx, shape) {
4660     // set style
4661     ctx.fillStyle = this.color;
4662 
4663     // draw shape
4664     var pos = this._getPosition();
4665     ctx[shape](pos.x, pos.y, this.radius);
4666     ctx.fill();
4667 };
4668 
4669 /**
4670  * Redraw a package as an image
4671  * Draw this link in the given canvas
4672  * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
4673  * @param {CanvasRenderingContext2D}   ctx
4674  */
4675 links.Network.Package.prototype._drawImage = function (ctx) {
4676     if (this.imageObj) {
4677         var width, height;
4678         if (this.value) {
4679             var scale = this.imageObj.height / this.imageObj.width;
4680             width = this.radius || this.imageObj.width;
4681             height = this.radius * scale || this.imageObj.height;
4682         }
4683         else {
4684             width = this.imageObj.width;
4685             height = this.imageObj.height;
4686         }
4687         var pos = this._getPosition();
4688 
4689         ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height);
4690     }
4691     else {
4692         console.log("image still loading...");
4693     }
4694 };
4695 
4696 
4697 
4698 /**--------------------------------------------------------------------------**/
4699 
4700 
4701 /**
4702  * @class Groups
4703  * This class can store groups and properties specific for groups.
4704  */
4705 links.Network.Groups = function () {
4706     this.clear();
4707     this.defaultIndex = 0;
4708 };
4709 
4710 
4711 /**
4712  * default constants for group colors
4713  */
4714 links.Network.Groups.DEFAULT = [
4715     {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue
4716     {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow
4717     {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red
4718     {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green
4719     {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta
4720     {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple
4721     {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange
4722     {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue
4723     {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink
4724     {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"}  // mint
4725 ];
4726 
4727 
4728 /**
4729  * Clear all groups
4730  */
4731 links.Network.Groups.prototype.clear = function () {
4732     this.groups = {};
4733     this.groups.length = function()
4734     {
4735         var i = 0;
4736         for ( var p in this ) {
4737             if (this.hasOwnProperty(p)) {
4738                 i++;
4739             }
4740         }
4741         return i;
4742     }
4743 };
4744 
4745 
4746 /**
4747  * get group properties of a groupname. If groupname is not found, a new group
4748  * is added.
4749  * @param {*} groupname        Can be a number, string, Date, etc.
4750  * @return {Object} group      The created group, containing all group properties
4751  */
4752 links.Network.Groups.prototype.get = function (groupname) {
4753     var group = this.groups[groupname];
4754 
4755     if (group == undefined) {
4756         // create new group
4757         var index = this.defaultIndex % links.Network.Groups.DEFAULT.length;
4758         this.defaultIndex++;
4759         group = {};
4760         group.borderColor     = links.Network.Groups.DEFAULT[index].borderColor;
4761         group.backgroundColor = links.Network.Groups.DEFAULT[index].backgroundColor;
4762         group.highlightColor  = links.Network.Groups.DEFAULT[index].highlightColor;
4763         this.groups[groupname] = group;
4764     }
4765 
4766     return group;
4767 };
4768 
4769 /**
4770  * Add a custom group style
4771  * @param {String} groupname
4772  * @param {Object} style       An object containing borderColor,
4773  *                             backgroundColor, etc.
4774  * @return {Object} group      The created group object
4775  */
4776 links.Network.Groups.prototype.add = function (groupname, style) {
4777     this.groups[groupname] = style;
4778     return style;
4779 };
4780 
4781 /**
4782  * Check if given object is a Javascript Array
4783  * @param {*} obj
4784  * @return {Boolean} isArray    true if the given object is an array
4785  */
4786 // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
4787 links.Network.isArray = function (obj) {
4788     if (obj instanceof Array) {
4789         return true;
4790     }
4791     return (Object.prototype.toString.call(obj) === '[object Array]');
4792 };
4793 
4794 
4795 
4796 /**--------------------------------------------------------------------------**/
4797 
4798 
4799 /**
4800  * @class Slider
4801  *
4802  * An html slider control with start/stop/prev/next buttons
4803  * @param {Element} container  The element where the slider will be created
4804  */
4805 links.Network.Slider = function(container) {
4806     if (container === undefined) throw "Error: No container element defined";
4807 
4808     this.container = container;
4809 
4810     this.frame = document.createElement("DIV");
4811     //this.frame.style.backgroundColor = "#E5E5E5";
4812     this.frame.style.width = "100%";
4813     this.frame.style.position = "relative";
4814 
4815     this.title = document.createElement("DIV");
4816     this.title.style.margin = "2px";
4817     this.title.style.marginBottom = "5px";
4818     this.title.innerHTML = "";
4819     this.container.appendChild(this.title);
4820 
4821     this.frame.prev = document.createElement("INPUT");
4822     this.frame.prev.type = "BUTTON";
4823     this.frame.prev.value = "Prev";
4824     this.frame.appendChild(this.frame.prev);
4825 
4826     this.frame.play = document.createElement("INPUT");
4827     this.frame.play.type = "BUTTON";
4828     this.frame.play.value = "Play";
4829     this.frame.appendChild(this.frame.play);
4830 
4831     this.frame.next = document.createElement("INPUT");
4832     this.frame.next.type = "BUTTON";
4833     this.frame.next.value = "Next";
4834     this.frame.appendChild(this.frame.next);
4835 
4836     this.frame.bar = document.createElement("INPUT");
4837     this.frame.bar.type = "BUTTON";
4838     this.frame.bar.style.position = "absolute";
4839     this.frame.bar.style.border = "1px solid red";
4840     this.frame.bar.style.width = "100px";
4841     this.frame.bar.style.height = "6px";
4842     this.frame.bar.style.borderRadius = "2px";
4843     this.frame.bar.style.MozBorderRadius = "2px";
4844     this.frame.bar.style.border = "1px solid #7F7F7F";
4845     this.frame.bar.style.backgroundColor = "#E5E5E5";
4846     this.frame.appendChild(this.frame.bar);
4847 
4848     this.frame.slide = document.createElement("INPUT");
4849     this.frame.slide.type = "BUTTON";
4850     this.frame.slide.style.margin = "0px";
4851     this.frame.slide.value = " ";
4852     this.frame.slide.style.position = "relative";
4853     this.frame.slide.style.left = "-100px";
4854     this.frame.appendChild(this.frame.slide);
4855 
4856     // create events
4857     var me = this;
4858     this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
4859     this.frame.prev.onclick = function (event) {me.prev(event);};
4860     this.frame.play.onclick = function (event) {me.togglePlay(event);};
4861     this.frame.next.onclick = function (event) {me.next(event);};
4862 
4863     this.container.appendChild(this.frame);
4864 
4865     this.onChangeCallback = undefined;
4866 
4867     this.playTimeout = undefined;
4868     this.framerate = 20; // frames per second
4869     this.duration = 10; // seconds
4870     this.doLoop = true;
4871 
4872     this.start = 0;
4873     this.end = 0;
4874     this.value = 0;
4875     this.step = 0;
4876     this.rangeIsDate = false;
4877 
4878     this.redraw();
4879 };
4880 
4881 /**
4882  * Retrieve the step size, depending on the range, framerate, and duration
4883  */
4884 links.Network.Slider.prototype._updateStep = function() {
4885     var range = (this.end - this.start);
4886     var frameCount = this.duration * this.framerate;
4887 
4888     this.step = range / frameCount;
4889 };
4890 
4891 /**
4892  * Select the previous index
4893  */
4894 links.Network.Slider.prototype.prev = function() {
4895     this._setValue(this.value - this.step);
4896 };
4897 
4898 /**
4899  * Select the next index
4900  */
4901 links.Network.Slider.prototype.next = function() {
4902     this._setValue(this.value + this.step);
4903 };
4904 
4905 /**
4906  * Select the next index
4907  */
4908 links.Network.Slider.prototype.playNext = function() {
4909     var start = new Date();
4910 
4911     if (!this.leftButtonDown) {
4912         if (this.value + this.step < this.end) {
4913             this._setValue(this.value + this.step);
4914         }
4915         else {
4916             if (this.doLoop) {
4917                 this._setValue(this.start);
4918             }
4919             else {
4920                 this._setValue(this.end);
4921                 this.stop();
4922                 return;
4923             }
4924         }
4925     }
4926 
4927     var end = new Date();
4928     var diff = (end - start);
4929 
4930     // calculate how much time it to to set the index and to execute the callback
4931     // function.
4932     var interval = Math.max(1000 / this.framerate - diff, 0);
4933 
4934     var me = this;
4935     this.playTimeout = setTimeout(function() {me.playNext();}, interval);
4936 };
4937 
4938 /**
4939  * Toggle start or stop playing
4940  */
4941 links.Network.Slider.prototype.togglePlay = function() {
4942     if (this.playTimeout === undefined) {
4943         this.play();
4944     } else {
4945         this.stop();
4946     }
4947 };
4948 
4949 /**
4950  * Start playing
4951  */
4952 links.Network.Slider.prototype.play = function() {
4953     this.frame.play.value = "Stop";
4954 
4955     this.playNext();
4956 };
4957 
4958 /**
4959  * Stop playing
4960  */
4961 links.Network.Slider.prototype.stop = function() {
4962     this.frame.play.value = "Play";
4963 
4964     clearInterval(this.playTimeout);
4965     this.playTimeout = undefined;
4966 };
4967 
4968 /**
4969  * Set a callback function which will be triggered when the value of the
4970  * slider bar has changed.
4971  */
4972 links.Network.Slider.prototype.setOnChangeCallback = function(callback) {
4973     this.onChangeCallback = callback;
4974 };
4975 
4976 /**
4977  * Set the interval for playing the list
4978  * @param {number} framerate    Framerate in frames per second
4979  */
4980 links.Network.Slider.prototype.setFramerate = function(framerate) {
4981     this.framerate = framerate;
4982     this._updateStep();
4983 };
4984 
4985 /**
4986  * Retrieve the current framerate
4987  * @return {number} framerate in frames per second
4988  */
4989 links.Network.Slider.prototype.getFramerate = function() {
4990     return this.framerate;
4991 };
4992 
4993 /**
4994  * Set the duration for playing
4995  * @param {number} duration    Duration in seconds
4996  */
4997 links.Network.Slider.prototype.setDuration = function(duration) {
4998     this.duration = duration;
4999     this._updateStep();
5000 };
5001 
5002 /**
5003  * Set the time acceleration for playing the history. Only applicable when
5004  * the values are of type Date.
5005  * @param {number} acceleration    Acceleration, for example 10 means play
5006  *                                 ten times as fast as real time. A value
5007  *                                 of 1 will play the history in real time.
5008  */
5009 links.Network.Slider.prototype.setAcceleration = function(acceleration) {
5010     var durationRealtime = (this.end - this.start) / 1000; // in seconds
5011 
5012     this.duration = durationRealtime / acceleration;
5013     this._updateStep();
5014 };
5015 
5016 
5017 /**
5018  * Set looping on or off
5019  * @param {boolean} doLoop    If true, the slider will jump to the start when
5020  *                            the end is passed, and will jump to the end
5021  *                            when the start is passed.
5022  */
5023 links.Network.Slider.prototype.setLoop = function(doLoop) {
5024     this.doLoop = doLoop;
5025 };
5026 
5027 /**
5028  * Retrieve the current value of loop
5029  * @return {boolean} doLoop    If true, the slider will jump to the start when
5030  *                             the end is passed, and will jump to the end
5031  *                             when the start is passed.
5032  */
5033 links.Network.Slider.prototype.getLoop = function() {
5034     return this.doLoop;
5035 };
5036 
5037 
5038 /**
5039  * Execute the onchange callback function
5040  */
5041 links.Network.Slider.prototype.onChange = function() {
5042     if (this.onChangeCallback !== undefined) {
5043         this.onChangeCallback();
5044     }
5045 };
5046 
5047 /**
5048  * redraw the slider on the correct place
5049  */
5050 links.Network.Slider.prototype.redraw = function() {
5051     // resize the bar
5052     var barTop = (this.frame.clientHeight/2 -
5053         this.frame.bar.offsetHeight/2);
5054     var barWidth = (this.frame.clientWidth -
5055         this.frame.prev.clientWidth -
5056         this.frame.play.clientWidth -
5057         this.frame.next.clientWidth - 30);
5058     this.frame.bar.style.top = barTop + "px";
5059     this.frame.bar.style.width = barWidth + "px";
5060 
5061     // position the slider button
5062     this.frame.slide.title = this.getValue();
5063     this.frame.slide.style.left = this._valueToLeft(this.value) + "px";
5064 
5065     // set the title
5066     this.title.innerHTML = this.getValue();
5067 };
5068 
5069 
5070 /**
5071  * Set the range for the slider
5072  * @param {Date | Number} start  Start of the range
5073  * @param {Date | Number} end    End of the range
5074  */
5075 links.Network.Slider.prototype.setRange = function(start, end) {
5076     if (start === undefined || start === null || start === NaN) {
5077         this.start = 0;
5078         this.rangeIsDate = false;
5079     }
5080     else if (start instanceof Date) {
5081         this.start = start.getTime();
5082         this.rangeIsDate = true;
5083     }
5084     else {
5085         this.start = start;
5086         this.rangeIsDate = false;
5087     }
5088 
5089     if (end === undefined || end === null || end === NaN) {
5090         if (this.start instanceof Date) {
5091             this.end = new Date(this.start);
5092         }
5093         else {
5094             this.end = this.start;
5095         }
5096     }
5097     else if (end instanceof Date) {
5098         this.end = end.getTime();
5099     }
5100     else {
5101         this.end = end;
5102     }
5103 
5104     this.value = this.start;
5105 
5106     this._updateStep();
5107     this.redraw();
5108 };
5109 
5110 
5111 
5112 /**
5113  * Set a value for the slider. The value must be between start and end
5114  * When the range are Dates, the value will be translated to a date
5115  * @param {Number} value
5116  */
5117 links.Network.Slider.prototype._setValue = function(value) {
5118     this.value = this._limitValue(value);
5119     this.redraw();
5120 
5121     this.onChange();
5122 };
5123 
5124 /**
5125  * retrieve the current value in the correct type, Number or Date
5126  * @return {Date | Number} value
5127  */
5128 links.Network.Slider.prototype.getValue = function() {
5129     if (this.rangeIsDate) {
5130         return new Date(this.value);
5131     }
5132     else {
5133         return this.value;
5134     }
5135 };
5136 
5137 
5138 links.Network.Slider.prototype.offset = 3;
5139 
5140 links.Network.Slider.prototype._leftToValue = function (left) {
5141     var width = parseFloat(this.frame.bar.style.width) -
5142         this.frame.slide.clientWidth - 10;
5143     var x = left - this.offset;
5144 
5145     var range = this.end - this.start;
5146     var value = this._limitValue(x / width * range + this.start);
5147 
5148     return value;
5149 };
5150 
5151 links.Network.Slider.prototype._valueToLeft = function (value) {
5152     var width = parseFloat(this.frame.bar.style.width) -
5153         this.frame.slide.clientWidth - 10;
5154 
5155     var x;
5156     if (this.end > this.start) {
5157         x = (value - this.start) / (this.end - this.start) * width;
5158     }
5159     else {
5160         x = 0;
5161     }
5162     var left = x + this.offset;
5163 
5164     return left;
5165 };
5166 
5167 links.Network.Slider.prototype._limitValue = function(value) {
5168     if (value < this.start) {
5169         value = this.start
5170     }
5171     if (value > this.end) {
5172         value = this.end;
5173     }
5174 
5175     return value;
5176 };
5177 
5178 links.Network.Slider.prototype._onMouseDown = function(event) {
5179     // only react on left mouse button down
5180     this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
5181     if (!this.leftButtonDown) return;
5182 
5183     this.startClientX = event.clientX;
5184     this.startSlideX = parseFloat(this.frame.slide.style.left);
5185 
5186     this.frame.style.cursor = 'move';
5187 
5188     // add event listeners to handle moving the contents
5189     // we store the function onmousemove and onmouseup in the graph, so we can
5190     // remove the eventlisteners lateron in the function mouseUp()
5191     var me = this;
5192     this.onmousemove = function (event) {me._onMouseMove(event);};
5193     this.onmouseup   = function (event) {me._onMouseUp(event);};
5194     links.Network.addEventListener(document, "mousemove", this.onmousemove);
5195     links.Network.addEventListener(document, "mouseup",   this.onmouseup);
5196     links.Network.preventDefault(event);
5197 };
5198 
5199 
5200 links.Network.Slider.prototype._onMouseMove = function (event) {
5201     var diff = event.clientX - this.startClientX;
5202     var x = this.startSlideX + diff;
5203 
5204     var value = this._leftToValue(x);
5205     this._setValue(value);
5206 
5207     links.Network.preventDefault(event);
5208 };
5209 
5210 
5211 links.Network.Slider.prototype._onMouseUp = function (event) {
5212     this.frame.style.cursor = 'auto';
5213 
5214     this.leftButtonDown = false;
5215 
5216     // remove event listeners
5217     links.Network.removeEventListener(document, "mousemove", this.onmousemove);
5218     links.Network.removeEventListener(document, "mouseup", this.onmouseup);
5219 
5220     links.Network.preventDefault(event);
5221 };
5222 
5223 
5224 
5225 /**--------------------------------------------------------------------------**/
5226 
5227 
5228 /**
5229  * Popup is a class to create a popup window with some text
5230  * @param {Element}  container     The container object.
5231  * @param {Number} x
5232  * @param {Number} y
5233  * @param {String} text
5234  */
5235 links.Network.Popup = function (container, x, y, text) {
5236     if (container) {
5237         this.container = container;
5238     }
5239     else {
5240         this.container = document.body;
5241     }
5242     this.x = 0;
5243     this.y = 0;
5244     this.padding = 5;
5245 
5246     if (x !== undefined && y !== undefined ) {
5247         this.setPosition(x, y);
5248     }
5249     if (text !== undefined) {
5250         this.setText(text);
5251     }
5252 
5253     // create the frame
5254     this.frame = document.createElement("div");
5255     var style = this.frame.style;
5256     style.position = "absolute";
5257     style.visibility = "hidden";
5258     style.border = "1px solid #666";
5259     style.color = "black";
5260     style.padding = this.padding + "px";
5261     style.backgroundColor = "#FFFFC6";
5262     style.borderRadius = "3px";
5263     style.MozBorderRadius = "3px";
5264     style.WebkitBorderRadius = "3px";
5265     style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
5266     style.whiteSpace = "nowrap";
5267     this.container.appendChild(this.frame);
5268 };
5269 
5270 /**
5271  * @param {number} x   Horizontal position of the popup window
5272  * @param {number} y   Vertical position of the popup window
5273  */
5274 links.Network.Popup.prototype.setPosition = function(x, y) {
5275     this.x = parseInt(x);
5276     this.y = parseInt(y);
5277 };
5278 
5279 /**
5280  * Set the text for the popup window. This can be HTML code
5281  * @param {string} text
5282  */
5283 links.Network.Popup.prototype.setText = function(text) {
5284     this.frame.innerHTML = text;
5285 };
5286 
5287 /**
5288  * Show the popup window
5289  * @param {boolean} show    Optional. Show or hide the window
5290  */
5291 links.Network.Popup.prototype.show = function (show) {
5292     if (show === undefined) {
5293         show = true;
5294     }
5295 
5296     if (show) {
5297         var height = this.frame.clientHeight;
5298         var width =  this.frame.clientWidth;
5299         var maxHeight = this.frame.parentNode.clientHeight;
5300         var maxWidth = this.frame.parentNode.clientWidth;
5301 
5302         var top = (this.y - height);
5303         if (top + height + this.padding > maxHeight) {
5304             top = maxHeight - height - this.padding;
5305         }
5306         if (top < this.padding) {
5307             top = this.padding;
5308         }
5309 
5310         var left = this.x;
5311         if (left + width + this.padding > maxWidth) {
5312             left = maxWidth - width - this.padding;
5313         }
5314         if (left < this.padding) {
5315             left = this.padding;
5316         }
5317 
5318         this.frame.style.left = left + "px";
5319         this.frame.style.top = top + "px";
5320         this.frame.style.visibility = "visible";
5321     }
5322     else {
5323         this.hide();
5324     }
5325 };
5326 
5327 /**
5328  * Hide the popup window
5329  */
5330 links.Network.Popup.prototype.hide = function () {
5331     this.frame.style.visibility = "hidden";
5332 };
5333 
5334 
5335 
5336 /** ------------------------------------------------------------------------ **/
5337 
5338 
5339 /**
5340  * Event listener (singleton)
5341  */
5342 links.events = links.events || {
5343     'listeners': [],
5344 
5345     /**
5346      * Find a single listener by its object
5347      * @param {Object} object
5348      * @return {Number} index  -1 when not found
5349      */
5350     'indexOf': function (object) {
5351         var listeners = this.listeners;
5352         for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
5353             var listener = listeners[i];
5354             if (listener && listener.object == object) {
5355                 return i;
5356             }
5357         }
5358         return -1;
5359     },
5360 
5361     /**
5362      * Add an event listener
5363      * @param {Object} object
5364      * @param {String} event       The name of an event, for example 'select'
5365      * @param {function} callback  The callback method, called when the
5366      *                             event takes place
5367      */
5368     'addListener': function (object, event, callback) {
5369         var index = this.indexOf(object);
5370         var listener = this.listeners[index];
5371         if (!listener) {
5372             listener = {
5373                 'object': object,
5374                 'events': {}
5375             };
5376             this.listeners.push(listener);
5377         }
5378 
5379         var callbacks = listener.events[event];
5380         if (!callbacks) {
5381             callbacks = [];
5382             listener.events[event] = callbacks;
5383         }
5384 
5385         // add the callback if it does not yet exist
5386         if (callbacks.indexOf(callback) == -1) {
5387             callbacks.push(callback);
5388         }
5389     },
5390 
5391     /**
5392      * Remove an event listener
5393      * @param {Object} object
5394      * @param {String} event       The name of an event, for example 'select'
5395      * @param {function} callback  The registered callback method
5396      */
5397     'removeListener': function (object, event, callback) {
5398         var index = this.indexOf(object);
5399         var listener = this.listeners[index];
5400         if (listener) {
5401             var callbacks = listener.events[event];
5402             if (callbacks) {
5403                 var callbackIndex = callbacks.indexOf(callback);
5404                 if (callbackIndex != -1) {
5405                     callbacks.splice(callbackIndex, 1);
5406                 }
5407 
5408                 // remove the array when empty
5409                 if (callbacks.length == 0) {
5410                     delete listener.events[event];
5411                 }
5412             }
5413 
5414             // count the number of registered events. remove listener when empty
5415             var count = 0;
5416             var events = listener.events;
5417             for (var ev in events) {
5418                 if (events.hasOwnProperty(ev)) {
5419                     count++;
5420                 }
5421             }
5422             if (count == 0) {
5423                 delete this.listeners[index];
5424             }
5425         }
5426     },
5427 
5428     /**
5429      * Remove all registered event listeners
5430      */
5431     'removeAllListeners': function () {
5432         this.listeners = [];
5433     },
5434 
5435     /**
5436      * Trigger an event. All registered event handlers will be called
5437      * @param {Object} object
5438      * @param {String} event
5439      * @param {Object} properties (optional)
5440      */
5441     'trigger': function (object, event, properties) {
5442         var index = this.indexOf(object);
5443         var listener = this.listeners[index];
5444         if (listener) {
5445             var callbacks = listener.events[event];
5446             if (callbacks) {
5447                 for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
5448                     callbacks[i](properties);
5449                 }
5450             }
5451         }
5452     }
5453 };
5454 
5455 
5456 /**--------------------------------------------------------------------------**/
5457 
5458 
5459 /**
5460  * Draw a circle shape
5461  */
5462 CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
5463     this.beginPath();
5464     this.arc(x, y, r, 0, 2*Math.PI, false);
5465 };
5466 
5467 /**
5468  * Draw a square shape
5469  * @param {Number} x horizontal center
5470  * @param {Number} y vertical center
5471  * @param {Number} r   size, width and height of the square
5472  */
5473 CanvasRenderingContext2D.prototype.square = function(x, y, r) {
5474     this.beginPath();
5475     this.rect(x - r, y - r, r * 2, r * 2);
5476 };
5477 
5478 /**
5479  * Draw a triangle shape
5480  * @param {Number} x horizontal center
5481  * @param {Number} y vertical center
5482  * @param {Number} r   radius, half the length of the sides of the triangle
5483  */
5484 CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
5485     // http://en.wikipedia.org/wiki/Equilateral_triangle
5486     this.beginPath();
5487 
5488     var s = r * 2;
5489     var s2 = s / 2;
5490     var ir = Math.sqrt(3) / 6 * s;      // radius of inner circle
5491     var h = Math.sqrt(s * s - s2 * s2); // height
5492 
5493     this.moveTo(x, y - (h - ir));
5494     this.lineTo(x + s2, y + ir);
5495     this.lineTo(x - s2, y + ir);
5496     this.lineTo(x, y - (h - ir));
5497     this.closePath();
5498 };
5499 
5500 /**
5501  * Draw a triangle shape in downward orientation
5502  * @param {Number} x horizontal center
5503  * @param {Number} y vertical center
5504  * @param {Number} r radius
5505  */
5506 CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
5507     // http://en.wikipedia.org/wiki/Equilateral_triangle
5508     this.beginPath();
5509 
5510     var s = r * 2;
5511     var s2 = s / 2;
5512     var ir = Math.sqrt(3) / 6 * s;      // radius of inner circle
5513     var h = Math.sqrt(s * s - s2 * s2); // height
5514 
5515     this.moveTo(x, y + (h - ir));
5516     this.lineTo(x + s2, y - ir);
5517     this.lineTo(x - s2, y - ir);
5518     this.lineTo(x, y + (h - ir));
5519     this.closePath();
5520 };
5521 
5522 /**
5523  * Draw a star shape, a star with 5 points
5524  * @param {Number} x horizontal center
5525  * @param {Number} y vertical center
5526  * @param {Number} r   radius, half the length of the sides of the triangle
5527  */
5528 CanvasRenderingContext2D.prototype.star = function(x, y, r) {
5529     // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
5530     this.beginPath();
5531 
5532     for (var n = 0; n < 10; n++) {
5533         var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
5534         this.lineTo(
5535             x + radius * Math.sin(n * 2 * Math.PI / 10),
5536             y - radius * Math.cos(n * 2 * Math.PI / 10)
5537         );
5538     }
5539 
5540     this.closePath();
5541 };
5542 
5543 /**
5544  * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
5545  */
5546 CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
5547     var r2d = Math.PI/180;
5548     if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
5549     if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
5550     this.beginPath();
5551     this.moveTo(x+r,y);
5552     this.lineTo(x+w-r,y);
5553     this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
5554     this.lineTo(x+w,y+h-r);
5555     this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
5556     this.lineTo(x+r,y+h);
5557     this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
5558     this.lineTo(x,y+r);
5559     this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
5560 };
5561 
5562 /**
5563  * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
5564  */
5565 CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
5566     var kappa = .5522848,
5567         ox = (w / 2) * kappa, // control point offset horizontal
5568         oy = (h / 2) * kappa, // control point offset vertical
5569         xe = x + w,           // x-end
5570         ye = y + h,           // y-end
5571         xm = x + w / 2,       // x-middle
5572         ym = y + h / 2;       // y-middle
5573 
5574     this.beginPath();
5575     this.moveTo(x, ym);
5576     this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
5577     this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
5578     this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
5579     this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
5580 };
5581 
5582 
5583 
5584 /**
5585  * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
5586  */
5587 CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
5588     var f = 1/3;
5589     var wEllipse = w;
5590     var hEllipse = h * f;
5591 
5592     var kappa = .5522848,
5593         ox = (wEllipse / 2) * kappa, // control point offset horizontal
5594         oy = (hEllipse / 2) * kappa, // control point offset vertical
5595         xe = x + wEllipse,           // x-end
5596         ye = y + hEllipse,           // y-end
5597         xm = x + wEllipse / 2,       // x-middle
5598         ym = y + hEllipse / 2,       // y-middle
5599         ymb = y + (h - hEllipse/2),  // y-midlle, bottom ellipse 
5600         yeb = y + h;                 // y-end, bottom ellipse
5601 
5602     this.beginPath();
5603     this.moveTo(xe, ym);
5604 
5605     this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
5606     this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
5607 
5608     this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
5609     this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
5610 
5611     this.lineTo(xe, ymb);
5612 
5613     this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
5614     this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
5615 
5616     this.lineTo(x, ym);
5617 };
5618 
5619 
5620 /**
5621  * Draw an arrow point (no line)
5622  */
5623 CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
5624     // tail
5625     var xt = x - length * Math.cos(angle);
5626     var yt = y - length * Math.sin(angle);
5627 
5628     // inner tail
5629     // TODO: allow to customize different shapes
5630     var xi = x - length * 0.9 * Math.cos(angle);
5631     var yi = y - length * 0.9 * Math.sin(angle);
5632 
5633     // left
5634     var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
5635     var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
5636 
5637     // right
5638     var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
5639     var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
5640 
5641     this.beginPath();
5642     this.moveTo(x, y);
5643     this.lineTo(xl, yl);
5644     this.lineTo(xi, yi);
5645     this.lineTo(xr, yr);
5646     this.closePath();
5647 };
5648 
5649 
5650 // TODO: add diamond shape
5651 
5652 
5653 /*----------------------------------------------------------------------------*/
5654 
5655 // utility methods
5656 links.Network.util = {};
5657 
5658 /**
5659  * Parse a text source containing data in DOT language into a JSON object.
5660  * The object contains two lists: one with nodes and one with edges.
5661  * @param {String} data     Text containing a graph in DOT-notation
5662  * @return {Object} json    An object containing two parameters:
5663  *                          {Object[]} nodes
5664  *                          {Object[]} edges
5665  */
5666 links.Network.util.parseDOT = function (data) {
5667     /**
5668      * Test whether given character is a whitespace character
5669      * @param {String} c
5670      * @return {Boolean} isWhitespace
5671      */
5672     function isWhitespace(c) {
5673         return c == ' ' || c == '\t' || c == '\n' || c == '\r';
5674     }
5675 
5676     /**
5677      * Test whether given character is a delimeter
5678      * @param {String} c
5679      * @return {Boolean} isDelimeter
5680      */
5681     function isDelimeter(c) {
5682         return '[]{}();,=->'.indexOf(c) != -1;
5683     }
5684 
5685     var i = -1;  // current index in the data
5686     var c = '';  // current character in the data
5687 
5688     /**
5689      * Read the next character from the data
5690      */
5691     function next() {
5692         i++;
5693         c = data[i];
5694     }
5695 
5696     /**
5697      * Preview the next character in the data
5698      * @returns {String} nextChar
5699      */
5700     function previewNext () {
5701         return data[i + 1];
5702     }
5703 
5704     /**
5705      * Preview the next character in the data
5706      * @returns {String} nextChar
5707      */
5708     function previewPrevious () {
5709         return data[i + 1];
5710     }
5711 
5712     /**
5713      * Get a text description of the the current index in the data
5714      * @return {String} desc
5715      */
5716     function pos() {
5717         return '(char ' + i + ')';
5718     }
5719 
5720     /**
5721      * Skip whitespace and comments
5722      */
5723     function parseWhitespace() {
5724         // skip whitespace
5725         while (c && isWhitespace(c)) {
5726             next();
5727         }
5728 
5729         // test for comment
5730         var cNext = data[i + 1];
5731         var cPrev = data[i - 1];
5732         var c2 = c + cNext;
5733         if (c2 == '/*') {
5734             // block comment. skip until the block is closed
5735             while (c && !(c == '*' && data[i + 1] == '/')) {
5736                 next();
5737             }
5738             next();
5739             next();
5740 
5741             parseWhitespace();
5742         }
5743         else if (c2 == '//' || (c == '#' && cPrev == '\n')) {
5744             // line comment. skip until the next return
5745             while (c && c != '\n') {
5746                 next();
5747             }
5748             next();
5749             parseWhitespace();
5750         }
5751     }
5752 
5753     /**
5754      * Parse a string
5755      * The string may be enclosed by double quotes
5756      * @return {String | undefined} value
5757      */
5758     function parseString() {
5759         parseWhitespace();
5760 
5761         var name = '';
5762         if (c == '"') {
5763             next();
5764             while (c && c != '"') {
5765                 name += c;
5766                 next();
5767             }
5768             next(); // skip the closing quote
5769         }
5770         else {
5771             while (c && !isWhitespace(c) && !isDelimeter(c)) {
5772                 name += c;
5773                 next();
5774             }
5775 
5776             // cast string to number or boolean
5777             var number = Number(name);
5778             if (!isNaN(number)) {
5779                 name = number;
5780             }
5781             else if (name == 'true') {
5782                 name = true;
5783             }
5784             else if (name == 'false') {
5785                 name = false;
5786             }
5787             else if (name == 'null') {
5788                 name = null;
5789             }
5790         }
5791 
5792         return name;
5793     }
5794 
5795     /**
5796      * Parse a value, can be a string, number, or boolean.
5797      * The value may be enclosed by double quotes
5798      * @return {String | Number | Boolean | undefined} value
5799      */
5800     function parseValue() {
5801         parseWhitespace();
5802 
5803         if (c == '"') {
5804             return parseString();
5805         }
5806         else {
5807             var value = parseString();
5808             if (value != undefined) {
5809                 // cast string to number or boolean
5810                 var number = Number(value);
5811                 if (!isNaN(number)) {
5812                     value = number;
5813                 }
5814                 else if (value == 'true') {
5815                     value = true;
5816                 }
5817                 else if (value == 'false') {
5818                     value = false;
5819                 }
5820                 else if (value == 'null') {
5821                     value = null;
5822                 }
5823             }
5824             return value;
5825         }
5826     }
5827 
5828     /**
5829      * Parse a set with attributes,
5830      * for example [label="1.000", style=solid]
5831      * @return {Object | undefined} attr
5832      */
5833     function parseAttributes() {
5834         parseWhitespace();
5835 
5836         if (c == '[') {
5837             next();
5838             var attr = {};
5839             while (c && c != ']') {
5840                 parseWhitespace();
5841 
5842                 var name = parseString();
5843                 if (!name) {
5844                     throw new SyntaxError('Attribute name expected ' + pos());
5845                 }
5846 
5847                 parseWhitespace();
5848                 if (c != '=') {
5849                     throw new SyntaxError('Equal sign = expected ' + pos());
5850                 }
5851                 next();
5852 
5853                 var value = parseValue();
5854                 if (!value) {
5855                     throw new SyntaxError('Attribute value expected ' + pos());
5856                 }
5857                 attr[name] = value;
5858 
5859                 parseWhitespace();
5860 
5861                 if (c ==',') {
5862                     next();
5863                 }
5864             }
5865             next();
5866 
5867             return attr;
5868         }
5869         else {
5870             return undefined;
5871         }
5872     }
5873 
5874     /**
5875      * Parse a directed or undirected arrow '->' or '--'
5876      * @return {String | undefined} arrow
5877      */
5878     function parseArrow() {
5879         parseWhitespace();
5880 
5881         if (c == '-') {
5882             next();
5883             if (c == '>' || c == '-') {
5884                 var arrow = '-' + c;
5885                 next();
5886                 return arrow;
5887             }
5888             else {
5889                 throw new SyntaxError('Arrow "->" or "--" expected ' + pos());
5890             }
5891         }
5892 
5893         return undefined;
5894     }
5895 
5896     /**
5897      * Parse a line separator ';'
5898      * @return {String | undefined} separator
5899      */
5900     function parseSeparator() {
5901         parseWhitespace();
5902 
5903         if (c == ';') {
5904             next();
5905             return ';';
5906         }
5907 
5908         return undefined;
5909     }
5910 
5911     /**
5912      * Merge all properties of object b into object b
5913      * @param {Object} a
5914      * @param {Object} b
5915      */
5916     function merge (a, b) {
5917         if (a && b) {
5918             for (var name in b) {
5919                 if (b.hasOwnProperty(name)) {
5920                     a[name] = b[name];
5921                 }
5922             }
5923         }
5924     }
5925 
5926     var nodeMap = {};
5927     var edgeList = [];
5928 
5929     /**
5930      * Register a node with attributes
5931      * @param {String} id
5932      * @param {Object} [attr]
5933      */
5934     function addNode(id, attr) {
5935         var node = {
5936             id: String(id),
5937             attr: attr || {}
5938         };
5939         if (!nodeMap[id]) {
5940             nodeMap[id] = node;
5941         }
5942         else {
5943             merge(nodeMap[id].attr, node.attr);
5944         }
5945     }
5946 
5947     /**
5948      * Register an edge
5949      * @param {String} from
5950      * @param {String} to
5951      * @param {String} type    A string "->" or "--"
5952      * @param {Object} [attr]
5953      */
5954     function addEdge(from, to, type, attr) {
5955         edgeList.push({
5956             from: String(from),
5957             to: String(to),
5958             type: type,
5959             attr: attr || {}
5960         });
5961     }
5962 
5963     // find the opening curly bracket
5964     next();
5965     while (c && c != '{') {
5966         next();
5967     }
5968     if (c != '{') {
5969         throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos())
5970     }
5971     next();
5972 
5973     // parse all data until a closing curly bracket is encountered
5974     while (c && c != '}') {
5975         // parse node id and optional node attributes
5976         var id = parseString();
5977         if (id == undefined) {
5978             throw new SyntaxError('String with id expected ' + pos());
5979         }
5980         var attr = parseAttributes();
5981         addNode(id, attr);
5982 
5983         // TODO: parse global attributes "graph", "node", "edge"
5984 
5985         // parse arrow
5986         var type = parseArrow();
5987         while (type) {
5988             // parse node id
5989             var prevId = id;
5990             id = parseString();
5991             if (id == undefined) {
5992                 throw new SyntaxError('String with id expected ' + pos());
5993             }
5994             addNode(id);
5995 
5996             // parse edge attributes and register edge
5997             attr = parseAttributes();
5998             addEdge(prevId, id, type, attr);
5999 
6000             // parse next arrow (optional)
6001             type = parseArrow();
6002         }
6003 
6004         // parse separator (optional)
6005         parseSeparator();
6006 
6007         parseWhitespace();
6008     }
6009     if (c != '}') {
6010         throw new SyntaxError('Invalid data. Curly bracket } expected');
6011     }
6012 
6013     // crop data between the curly brackets
6014     var start = data.indexOf('{');
6015     var end = data.indexOf('}', start);
6016     var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined;
6017 
6018     if (!text) {
6019         throw new Error('Invalid data. no curly brackets containing data found');
6020     }
6021 
6022     // return the results
6023     var nodeList = [];
6024     for (id in nodeMap) {
6025         if (nodeMap.hasOwnProperty(id)) {
6026             nodeList.push(nodeMap[id]);
6027         }
6028     }
6029     return {
6030         nodes: nodeList,
6031         edges: edgeList
6032     }
6033 };
6034 
6035 /**
6036  * Convert a string containing a graph in DOT language into a map containing
6037  * with nodes and edges in the format of Network.
6038  * @param {String} data         Text containing a graph in DOT-notation
6039  * @return {Object} networkData
6040  */
6041 links.Network.util.DOTToNetwork = function (data) {
6042     // parse the DOT file
6043     var dotData = links.Network.util.parseDOT(data);
6044     var networkData = {
6045         nodes: [],
6046         edges: [],
6047         options: {
6048             nodes: {},
6049             links: {}
6050         }
6051     };
6052 
6053     /**
6054      * Merge the properties of object b into object a, and adjust properties
6055      * not supported by Network (for example replace "shape" with "style"
6056      * @param {Object} a
6057      * @param {Object} b
6058      * @param {Array} [ignore]   Optional array with property names to be ignored
6059      */
6060     function merge (a, b, ignore) {
6061         for (var prop in b) {
6062             if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) {
6063                 a[prop] = b[prop];
6064             }
6065         }
6066 
6067         // Convert aliases to configuration settings supported by Network
6068         if (a.label) {
6069             a.text = a.label;
6070             delete a.label;
6071         }
6072         if (a.shape) {
6073             a.style = a.shape;
6074             delete a.shape;
6075         }
6076     }
6077 
6078     dotData.nodes.forEach(function (node) {
6079         if (node.id.toLowerCase() == 'graph') {
6080             merge(networkData.options, node.attr);
6081         }
6082         else if (node.id.toLowerCase() == 'node') {
6083             merge(networkData.options.nodes, node.attr);
6084         }
6085         else if (node.id.toLowerCase() == 'edge') {
6086             merge(networkData.options.links, node.attr);
6087         }
6088         else {
6089             var networkNode = {};
6090             networkNode.id = node.id;
6091             networkNode.text = node.id;
6092             merge(networkNode, node.attr);
6093             networkData.nodes.push(networkNode);
6094         }
6095     });
6096 
6097     dotData.edges.forEach(function (edge) {
6098         var networkEdge = {};
6099         networkEdge.from = edge.from;
6100         networkEdge.to = edge.to;
6101         networkEdge.text = edge.id;
6102         networkEdge.style = (edge.type == '->') ? 'arrow-end' : 'line';
6103         merge(networkEdge, edge.attr);
6104         networkData.edges.push(networkEdge);
6105     });
6106 
6107     return networkData;
6108 };