Current File : /home/jvzmxxx/wiki1/extensions/InteractiveTimeline/chap-links-library/js/src/network/network.js
/**
 * network.js
 *
 * @brief
 * Links Network is an interactive chart to visualize networks.
 * It allows creating nodes, links between the nodes, and interactive packages
 * moving between nodes. The visualization supports custom styles, colors,
 * sizes, images, and more.
 *
 * The network visualization works smooth on any modern browser for up to a
 * few hundred nodes and connections.
 *
 * Network is developed as a Google Visualization Chart in javascript.
 * There is a GWT wrapper available to use the Network in GWT (Google Web
 * Toolkit). It runs on all modern browsers without additional requirements.
 * Network is tested on Firefox 3.6+, Safari 5.0+, Chrome 6.0+, Opera 10.6+,
 * Internet Explorer 9+.
 *
 * Links Network is part of the CHAP Links library.
 *
 * @license
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy
 * of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Copyright (c) 2011-2012 Almende B.V.
 *
 * @author 	Jos de Jong, <jos@almende.org>
 * @date    2013-04-26
 * @version 1.5.0
 */

/*
 internally rename radiusMin to widthMin and radiusMax to widthMax ?
 right now the default widthMin and widthMax are not taken in case of an image
 solve problem with nodes initially having no velocity, thus not starting animation
 let the strength of a link depend on the number of (intermediate) connections
 group clusters as one node when zooming out
 make a smarter "random" start position for the nodes, based on the links
 currently it does not work well to switch from realtime to history animation and vice versa
 when adding nodes/links in realtime, they have no nice start position, causing wild movements
 when a node is created/replaced/removed, adjust all references in the defined links and packages 
 setting the length of links extra long does not result in nice looking network... repulsion should be changed too. 
 Or better: repulsion must be stronger than gravity
 */

/**
 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 * "links"
 */
if (typeof links === 'undefined') {
    links = {};
    // important: do not use var, as "var links = {};" will overwrite 
    //            the existing links variable value with undefined in IE8, IE7.  
}


/**
 * Ensure the variable google exists
 */
if (typeof google === 'undefined') {
    google = undefined;
    // important: do not use var, as "var google = undefined;" will overwrite 
    //            the existing google variable value with undefined in IE8, IE7.
}


/**
 * @constructor links.Network
 *
 * <p>
 * Links Network is an interactive chart to visualize networks.
 * It allows creating nodes, links between the nodes, and interactive packages
 * moving between nodes. The visualization supports custom styles, colors,
 * sizes, images, and more.
 * </p>
 * <p>
 * The network visualization works smooth on any modern browser for up to a
 * few hundred nodes and connections.
 * </p>
 * <p>Network is developed as a Google Visualization Chart in javascript.
 * There is a GWT wrapper available to use the Network in GWT (Google Web
 * Toolkit). It runs on all modern browsers without additional requirements.
 * Network is tested on Firefox 3.6+, Safari 5.0+, Chrome 6.0+, Opera 10.6+,
 * Internet Explorer 9+.
 * </p>
 *
 * Usage:
 * <code><pre>
 *   var nodesTable = new google.visualization.DataTable();
 *   nodesTable.addColumn('number', 'id');
 *   nodesTable.addColumn('string', 'text');
 *   nodesTable.addRow([1, "Node 1"]);
 *   nodesTable.addRow([2, "Node 2"]);
 *   // ...
 *
 *   var linksTable = new google.visualization.DataTable();
 *   linksTable.addColumn('number', 'from');
 *   linksTable.addColumn('number', 'to');
 *   linksTable.addRow([1, 2]);
 *
 *   var packageTable = undefined;
 *
 *   var network = new links.Network(document.getElementById('network'));
 *   network.draw(nodesTable, linksTable, packagesTable, options);
 * </pre></code>
 * <br>
 *
 * @param {Element} container   The DOM element in which the Network will
 *                                  be created. Normally a div element.
 */
links.Network = function(container) {
    // create variables and set default values
    this.containerElement = container;
    this.width = "100%";
    this.height = "100%";
    this.refreshRate = 50; // milliseconds
    this.stabilize = true; // stabilize before displaying the network
    this.selectable = true;

    // set constant values
    this.constants = {
        "nodes": {
            "radiusMin": 5,
            "radiusMax": 20,
            "radius": 5,
            "distance": 100, // px
            "style": "rect",
            "image": undefined,
            "widthMin": 16, // px
            "widthMax": 64, // px
            "fontColor": "black",
            "fontSize": 14, // px
            //"fontFace": "verdana",
            "fontFace": "arial",
            "borderColor": "#2B7CE9",
            "backgroundColor": "#97C2FC",
            "highlightColor": "#D2E5FF",
            "group": undefined
        },
        "links": {
            "widthMin": 1,
            "widthMax": 15,
            "width": 1,
            "style": "line",
            "color": "#343434",
            "fontColor": "#343434",
            "fontSize": 14, // px
            "fontFace": "arial",
            //"distance": 100, //px
            "length": 100,   // px
            "dashlength": 10,
            "dashgap": 5
        },
        "packages": {
            "radius": 5,
            "radiusMin": 5,
            "radiusMax": 10,
            "style": "dot",
            "color": "#2B7CE9",
            "image": undefined,
            "widthMin": 16, // px
            "widthMax": 64, // px
            "duration": 1.0   // seconds
        },
        "minForce": 0.05,
        "minVelocity": 0.02,   // px/s
        "maxIterations": 1000  // maximum number of iteration to stabilize
    };

    this.nodes = [];     // array with Node objects
    this.links = [];     // array with Link objects
    this.packages = [];  // array with all Package packages
    this.images = new links.Network.Images();     // object with images
    this.groups = new links.Network.Groups();     // object with groups

    // properties of the data
    this.hasMovingLinks = false;    // True if one or more of the links or nodes have an animation
    this.hasMovingNodes = false;    // True if any of the nodes have an undefined position
    this.hasMovingPackages = false; // True if there are one or more packages

    this.selection = [];
    this.timer = undefined;

    // create a frame and canvas
    this._create();
};

/**
 * Main drawing logic. This is the function that needs to be called
 * in the html page, to draw the Network.
 * Note that Object DataTable is defined in google.visualization.DataTable
 *
 * A data table with the events must be provided, and an options table.
 * @param {google.visualization.DataTable | Array} [nodes]     The data containing the nodes.
 * @param {google.visualization.DataTable | Array} [links]     The data containing the links.
 * @param {google.visualization.DataTable | Array} [packages]  The data containing the packages
 * @param {Object} options                                     A name/value map containing settings
 */
links.Network.prototype.draw = function(nodes, links, packages, options) {
    var nodesTable, linksTable, packagesTable;
    // correctly read the parameters. links and packages are optional.
    if (options != undefined) {
        nodesTable = nodes;
        linksTable = links;
        packagesTable = packages;
    }
    else if (packages != undefined) {
        nodesTable = nodes;
        linksTable = links;
        packagesTable = undefined;
        options = packages;
    }
    else if (links != undefined) {
        nodesTable = nodes;
        linksTable = undefined;
        packagesTable = undefined;
        options = links;
    }
    else if (nodes != undefined) {
        nodesTable = undefined;
        linksTable = undefined;
        packagesTable = undefined;
        options = nodes;
    }

    if (options != undefined) {
        // retrieve parameter values
        if (options.width != undefined)           {this.width = options.width;}
        if (options.height != undefined)          {this.height = options.height;}
        if (options.stabilize != undefined)       {this.stabilize = options.stabilize;}
        if (options.selectable != undefined)      {this.selectable = options.selectable;}
        
        // TODO: work out these options and document them
        if (options.links) {
            for (var prop in options.links) {
                if (options.links.hasOwnProperty(prop)) {
                    this.constants.links[prop] = options.links[prop];
                }
            }

            if (options.links.length != undefined &&
                    options.nodes && options.nodes.distance == undefined) {
                this.constants.links.length   = options.links.length;
                this.constants.nodes.distance = options.links.length * 1.25;
            }

            if (!options.links.fontColor) {
                this.constants.links.fontColor = options.links.color;
            }

            // Added to support dashed lines
            // David Jordan
            // 2012-08-08
            if (options.links.dashlength != undefined) {
                this.constants.links.dashlength   = options.links.dashlength;
            }
            if (options.links.dashgap != undefined) {
                this.constants.links.dashgap   = options.links.dashgap;
            }
            if (options.links.altdashlength != undefined) {
                this.constants.links.altdashlength   = options.links.altdashlength;
            }
        }
        if (options.nodes) {
            for (prop in options.nodes) {
                if (options.nodes.hasOwnProperty(prop)) {
                    this.constants.nodes[prop] = options.nodes[prop];
                }
            }

            /*
             if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
             if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
             */
        }
        if (options.packages) {
            for (prop in options.packages) {
                if (options.packages.hasOwnProperty(prop)) {
                    this.constants.packages[prop] = options.packages[prop];
                }
            }

            /*
             if (options.packages.widthMin) this.constants.packages.radiusMin = options.packages.widthMin;
             if (options.packages.widthMax) this.constants.packages.radiusMax = options.packages.widthMax;
             */
        }

        if (options.groups) {
            for (var groupname in options.groups) {
                if (options.groups.hasOwnProperty(groupname)) {
                    var group = options.groups[groupname];
                    this.groups.add(groupname, group);
                }
            }
        }
    }

    this._setBackgroundColor(options.backgroundColor);

    this._setSize(this.width, this.height);
    this._setTranslation(0, 0);
    this._setScale(1.0);

    // set all data
    this.hasTimestamps = false;
    this.setNodes(nodesTable);
    this.setLinks(linksTable);
    this.setPackages(packagesTable);

    this._reposition(); // TODO: bad solution  
    if (this.stabilize) {
        this._doStabilize();
    }
    this.start();

    // create an onload callback method for the images
    var network = this;
    var callback = function () {
        network._redraw();
    };
    this.images.setOnloadCallback(callback);

    // fire the ready event
    this.trigger('ready');
};

/**
 * fire an event
 * @param {String} event   The name of an event, for example "select" or "ready"
 * @param {Object} params  Optional object with event parameters
 */
links.Network.prototype.trigger = function (event, params) {
    // trigger the links event bus
    links.events.trigger(this, event, params);

    // trigger the google event bus
    if (google && google.visualization && google.visualization.events) {
        google.visualization.events.trigger(this, event, params);
    }
};


/**
 * Create the main frame for the Network.
 * This function is executed once when a Network object is created. The frame
 * contains a canvas, and this canvas contains all objects like the axis and
 * nodes.
 */
links.Network.prototype._create = function () {
    // remove all elements from the container element.
    while (this.containerElement.hasChildNodes()) {
        this.containerElement.removeChild(this.containerElement.firstChild);
    }

    this.frame = document.createElement("div");
    this.frame.className = "network-frame";
    this.frame.style.position = "relative";
    this.frame.style.overflow = "hidden";

    // create the graph canvas (HTML canvas element)
    this.frame.canvas = document.createElement( "canvas" );
    this.frame.canvas.style.position = "relative";
    this.frame.appendChild(this.frame.canvas);
    if (!this.frame.canvas.getContext) {
        var noCanvas = document.createElement( "DIV" );
        noCanvas.style.color = "red";
        noCanvas.style.fontWeight =  "bold" ;
        noCanvas.style.padding =  "10px";
        noCanvas.innerHTML =  "Error: your browser does not support HTML canvas";
        this.frame.canvas.appendChild(noCanvas);
    }

    // create event listeners
    var me = this;
    var onmousedown = function (event) {me._onMouseDown(event);};
    var onmousemove = function (event) {me._onMouseMoveTitle(event);};
    var onmousewheel = function (event) {me._onMouseWheel(event);};
    var ontouchstart = function (event) {me._onTouchStart(event);};
    links.Network.addEventListener(this.frame.canvas, "mousedown", onmousedown);
    links.Network.addEventListener(this.frame.canvas, "mousemove", onmousemove);
    links.Network.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
    links.Network.addEventListener(this.frame.canvas, "touchstart", ontouchstart);

    // add the frame to the container element
    this.containerElement.appendChild(this.frame);
};

/**
 * Set the background  and border styling for the graph
 * @param {String | Object} backgroundColor
 */
links.Network.prototype._setBackgroundColor = function(backgroundColor) {
    var fill = "white";
    var stroke = "lightgray";
    var strokeWidth = 1;

    if (typeof(backgroundColor) == "string") {
        fill = backgroundColor;
        stroke = "none";
        strokeWidth = 0;
    }
    else if (typeof(backgroundColor) == "object") {
        if (backgroundColor.fill != undefined)        fill = backgroundColor.fill;
        if (backgroundColor.stroke != undefined)      stroke = backgroundColor.stroke;
        if (backgroundColor.strokeWidth != undefined) strokeWidth = backgroundColor.strokeWidth;
    }
    else if  (backgroundColor == undefined) {
        // use use defaults
    }
    else {
        throw "Unsupported type of backgroundColor";
    }

    this.frame.style.boxSizing = 'border-box';
    this.frame.style.backgroundColor = fill;
    this.frame.style.borderColor = stroke;
    this.frame.style.borderWidth = strokeWidth + "px";
    this.frame.style.borderStyle = "solid";
};


/**
 * handle on mouse down event
 */
links.Network.prototype._onMouseDown = function (event) {
    event = event || window.event;

    if (!this.selectable) {
        return;
    }

    // check if mouse is still down (may be up when focus is lost for example
    // in an iframe)
    if (this.leftButtonDown) {
        this._onMouseUp(event);
    }

    // only react on left mouse button down
    this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
    if (!this.leftButtonDown && !this.touchDown) {
        return;
    }

    // add event listeners to handle moving the contents
    // we store the function onmousemove and onmouseup in the timeline, so we can
    // remove the eventlisteners lateron in the function mouseUp()
    var me = this;
    if (!this.onmousemove) {
        this.onmousemove = function (event) {me._onMouseMove(event);};
        links.Network.addEventListener(document, "mousemove", me.onmousemove);
    }
    if (!this.onmouseup) {
        this.onmouseup = function (event) {me._onMouseUp(event);};
        links.Network.addEventListener(document, "mouseup", me.onmouseup);
    }
    links.Network.preventDefault(event);

    // store the start x and y position of the mouse
    this.startMouseX = event.clientX || event.targetTouches[0].clientX;
    this.startMouseY = event.clientY || event.targetTouches[0].clientY;
    this.startFrameLeft = links.Network._getAbsoluteLeft(this.frame.canvas);
    this.startFrameTop = links.Network._getAbsoluteTop(this.frame.canvas);
    this.startTranslation = this._getTranslation();

    this.ctrlKeyDown = event.ctrlKey;
    this.shiftKeyDown = event.shiftKey;

    var obj = {
        "left" :   this._xToCanvas(this.startMouseX - this.startFrameLeft),
        "top" :    this._yToCanvas(this.startMouseY - this.startFrameTop),
        "right" :  this._xToCanvas(this.startMouseX - this.startFrameLeft),
        "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop)
    };
    var overlappingNodes = this._getNodesOverlappingWith(obj);
    // if there are overlapping nodes, select the last one, this is the 
    // one which is drawn on top of the others
    this.startClickedObj = (overlappingNodes.length > 0) ?
        overlappingNodes[overlappingNodes.length - 1] : undefined;

    if (this.startClickedObj) {
        // move clicked node with the mouse

        // make the clicked node temporarily fixed, and store their original state
        var node = this.nodes[this.startClickedObj.row];
        this.startClickedObj.xFixed = node.xFixed;
        this.startClickedObj.yFixed = node.yFixed;
        node.xFixed = true;
        node.yFixed = true;

        if (!this.ctrlKeyDown || !node.isSelected()) {
            // select this node
            this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
        }
        else {
            // unselect this node
            this._unselectNodes([this.startClickedObj]);
        }

        if (!this.hasMovingNodes) {
            this._redraw();
        }
    }
    else if (this.shiftKeyDown) {
        // start selection of multiple nodes
    }
    else {
        // start moving the graph
        this.moved = false;
    }
};

/**
 * handle on mouse move event
 */
links.Network.prototype._onMouseMove = function (event) {
    event = event || window.event;

    if (!this.selectable) {
        return;
    }

    var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0;
    var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0;
    this.mouseX = mouseX;
    this.mouseY = mouseY;

    if (this.startClickedObj) {
        var node = this.nodes[this.startClickedObj.row];

        if (!this.startClickedObj.xFixed)
            node.x = this._xToCanvas(mouseX - this.startFrameLeft);

        if (!this.startClickedObj.yFixed)
            node.y = this._yToCanvas(mouseY - this.startFrameTop);

        // start animation if not yet running
        if (!this.hasMovingNodes) {
            this.hasMovingNodes = true;
            this.start();
        }
    }
    else if (this.shiftKeyDown) {
        // draw a rect from start mouse location to current mouse location
        if (this.frame.selRect == undefined) {
            this.frame.selRect = document.createElement("DIV");
            this.frame.appendChild(this.frame.selRect);

            this.frame.selRect.style.position = "absolute";
            this.frame.selRect.style.border = "1px dashed red";
        }

        var left =   Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
        var top =    Math.min(this.startMouseY, mouseY) - this.startFrameTop;
        var right =  Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
        var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;

        this.frame.selRect.style.left = left + "px";
        this.frame.selRect.style.top = top + "px";
        this.frame.selRect.style.width = (right - left) + "px";
        this.frame.selRect.style.height = (bottom - top) + "px";
    }
    else {
        // move the network
        var diffX = mouseX - this.startMouseX;
        var diffY = mouseY - this.startMouseY;

        this._setTranslation(
            this.startTranslation.x + diffX,
            this.startTranslation.y + diffY);
        this._redraw();

        this.moved = true;
    }

    links.Network.preventDefault(event);
};

/**
 * handle on mouse up event
 */
links.Network.prototype._onMouseUp = function (event) {
    event = event || window.event;

    if (!this.selectable) {
        return;
    }

    // remove event listeners here, important for Safari
    if (this.onmousemove) {
        links.Network.removeEventListener(document, "mousemove", this.onmousemove);
        this.onmousemove = undefined;
    }
    if (this.onmouseup) {
        links.Network.removeEventListener(document, "mouseup",   this.onmouseup);
        this.onmouseup = undefined;
    }
    links.Network.preventDefault(event);

    // check selected nodes
    var endMouseX = event.clientX || this.mouseX || 0;
    var endMouseY = event.clientY || this.mouseY || 0;

    var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;

    if (this.startClickedObj) {
        // restore the original fixed state
        var node = this.nodes[this.startClickedObj.row];
        node.xFixed = this.startClickedObj.xFixed;
        node.yFixed = this.startClickedObj.yFixed;
    }
    else if (this.shiftKeyDown) {
        // select nodes inside selection area
        var obj = {
            "left":   this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
            "top":    this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
            "right":  this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
            "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
        };
        var overlappingNodes = this._getNodesOverlappingWith(obj);
        this._selectNodes(overlappingNodes, ctrlKey);
        this.redraw();

        // remove the selection rectangle
        if (this.frame.selRect) {
            this.frame.removeChild(this.frame.selRect);
            this.frame.selRect = undefined;
        }
    }
    else {
        if (!this.ctrlKeyDown && !this.moved) {
            // remove selection
            this._unselectNodes();
            this._redraw();
        }
    }

    this.leftButtonDown = false;
    this.ctrlKeyDown = false;
};


/**
 * Event handler for mouse wheel event, used to zoom the timeline
 * Code from http://adomas.org/javascript-mouse-wheel/
 * @param {event}  event   The event
 */
links.Network.prototype._onMouseWheel = function(event) {
    event = event || window.event;
    var mouseX = event.clientX;
    var mouseY = event.clientY;

    // retrieve delta    
    var delta = 0;
    if (event.wheelDelta) { /* IE/Opera. */
        delta = event.wheelDelta/120;
    } else if (event.detail) { /* Mozilla case. */
        // In Mozilla, sign of delta is different than in IE.
        // Also, delta is multiple of 3.
        delta = -event.detail/3;
    }

    // If delta is nonzero, handle it.
    // Basically, delta is now positive if wheel was scrolled up,
    // and negative, if wheel was scrolled down.
    if (delta) {
        // determine zoom factor, and adjust the zoom factor such that zooming in
        // and zooming out correspond wich each other
        var zoom = delta / 10;
        if (delta < 0) {
            zoom = zoom / (1 - zoom);
        }

        var scaleOld = this._getScale();
        var scaleNew = scaleOld * (1 + zoom);
        if (scaleNew < 0.01) {
            scaleNew = 0.01;
        }
        if (scaleNew > 10) {
            scaleNew = 10;
        }

        var frameLeft = links.Network._getAbsoluteLeft(this.frame.canvas);
        var frameTop = links.Network._getAbsoluteTop(this.frame.canvas);
        var x = mouseX - frameLeft;
        var y = mouseY - frameTop;

        var translation = this._getTranslation();
        var scaleFrac = scaleNew / scaleOld;
        var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
        var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;

        this._setScale(scaleNew);
        this._setTranslation(tx, ty);
        this._redraw();
    }

    // Prevent default actions caused by mouse wheel.
    // That might be ugly, but we handle scrolls somehow
    // anyway, so don't bother here...
    links.Network.preventDefault(event);
};


/**
 * Mouse move handler for checking whether the title moves over a node or
 * package with a title.
 */
links.Network.prototype._onMouseMoveTitle = function (event) {
    event = event || window.event;

    var startMouseX = event.clientX;
    var startMouseY = event.clientY;
    this.startFrameLeft = this.startFrameLeft || links.Network._getAbsoluteLeft(this.frame.canvas);
    this.startFrameTop = this.startFrameTop || links.Network._getAbsoluteTop(this.frame.canvas);

    var x = startMouseX - this.startFrameLeft;
    var y = startMouseY - this.startFrameTop;

    // check if the previously selected node is still selected
    if (this.popupNode) {
        this._checkHidePopup(x, y);
    }

    // start a timeout that will check if the mouse is positioned above 
    // an element
    var me = this;
    var checkShow = function() {
        me._checkShowPopup(x, y);
    };
    if (this.popupTimer) {
        clearInterval(this.popupTimer); // stop any running timer
    }
    if (!this.leftButtonDown) {
        this.popupTimer = setTimeout(checkShow, 300);
    }
};

/**
 * Check if there is an element on the given position in the network (
 * (a node, package, or link). If so, and if this element has a title,
 * show a popup window with its title.
 *
 * @param {number} x
 * @param {number} y
 */
links.Network.prototype._checkShowPopup = function (x, y) {
    var obj = {
        "left" : this._xToCanvas(x),
        "top" : this._yToCanvas(y),
        "right" : this._xToCanvas(x),
        "bottom" : this._yToCanvas(y)
    };

    var i, len;
    var lastPopupNode = this.popupNode;

    if (this.popupNode == undefined) {
        // search the packages for overlap

        for (i = 0, len = this.packages.length; i < len; i++) {
            var p = this.packages[i];
            if (p.getTitle() != undefined && p.isOverlappingWith(obj)) {
                this.popupNode = p;
                break;
            }
        }
    }

    if (this.popupNode == undefined) {
        // search the nodes for overlap, select the top one in case of multiple nodes
        var nodes = this.nodes;
        for (i = nodes.length - 1; i >= 0; i--) {
            var node = nodes[i];
            if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
                this.popupNode = node;
                break;
            }
        }
    }

    if (this.popupNode == undefined) {
        // search the links for overlap
        var allLinks = this.links;
        for (i = 0, len = allLinks.length; i < len; i++) {
            var link = allLinks[i];
            if (link.getTitle() != undefined && link.isOverlappingWith(obj)) {
                this.popupNode = link;
                break;
            }
        }
    }

    if (this.popupNode) {
        // show popup message window
        if (this.popupNode != lastPopupNode) {
            var me = this;
            if (!me.popup) {
                me.popup = new links.Network.Popup(me.frame);
            }

            // adjust a small offset such that the mouse cursor is located in the
            // bottom left location of the popup, and you can easily move over the
            // popup area
            me.popup.setPosition(x - 3, y - 3);
            me.popup.setText(me.popupNode.getTitle());
            me.popup.show();
        }
    }
    else {
        if (this.popup) {
            this.popup.hide();
        }
    }
};

/**
 * Check if the popup must be hided, which is the case when the mouse is no
 * longer hovering on the object
 * @param {number} x
 * @param {number} y
 */
links.Network.prototype._checkHidePopup = function (x, y) {
    var obj = {
        "left" : x,
        "top" : y,
        "right" : x,
        "bottom" : y
    };

    if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
        this.popupNode = undefined;
        if (this.popup) {
            this.popup.hide();
        }
    }
};

/**
 * Event handler for touchstart event on mobile devices
 */
links.Network.prototype._onTouchStart = function(event) {
    links.Network.preventDefault(event);

    if (this.touchDown) {
        // if already moving, return
        return;
    }
    this.touchDown = true;

    var me = this;
    if (!this.ontouchmove) {
        this.ontouchmove = function (event) {me._onTouchMove(event);};
        links.Network.addEventListener(document, "touchmove", this.ontouchmove);
    }
    if (!this.ontouchend) {
        this.ontouchend   = function (event) {me._onTouchEnd(event);};
        links.Network.addEventListener(document, "touchend", this.ontouchend);
    }

    this._onMouseDown(event);
};

/**
 * Event handler for touchmove event on mobile devices
 */
links.Network.prototype._onTouchMove = function(event) {
    links.Network.preventDefault(event);
    this._onMouseMove(event);
};

/**
 * Event handler for touchend event on mobile devices
 */
links.Network.prototype._onTouchEnd = function(event) {
    links.Network.preventDefault(event);

    this.touchDown = false;

    if (this.ontouchmove) {
        links.Network.removeEventListener(document, "touchmove", this.ontouchmove);
        this.ontouchmove = undefined;
    }
    if (this.ontouchend) {
        links.Network.removeEventListener(document, "touchend", this.ontouchend);
        this.ontouchend = undefined;
    }

    this._onMouseUp(event);
};


/**
 * Unselect selected nodes. If no selection array is provided, all nodes
 * are unselected
 * @param {Object[]} selection     Array with selection objects, each selection
 *                                 object has a parameter row. Optional
 * @param {Boolean} triggerSelect  If true (default), the select event
 *                                 is triggered when nodes are unselected
 * @return {Boolean} changed       True if the selection is changed
 */
links.Network.prototype._unselectNodes = function(selection, triggerSelect) {
    var changed = false;
    var i, iMax, row;

    if (selection) {
        // remove provided selections
        for (i = 0, iMax = selection.length; i < iMax; i++) {
            row = selection[i].row;
            this.nodes[row].unselect();

            var j = 0;
            while (j < this.selection.length) {
                if (this.selection[j].row == row) {
                    this.selection.splice(j, 1);
                    changed = true;
                }
                else {
                    j++;
                }
            }
        }
    }
    else if (this.selection && this.selection.length) {
        // remove all selections
        for (i = 0, iMax = this.selection.length; i < iMax; i++) {
            row = this.selection[i].row;
            this.nodes[row].unselect();
            changed = true;
        }
        this.selection = [];
    }

    if (changed && (triggerSelect == true || triggerSelect == undefined)) {
        // fire the select event
        this.trigger('select');
    }

    return changed;
};

/**
 * select all nodes on given location x, y
 * @param {Array} selection   an array with selection objects. Each selection
 *                            object has a parameter row
 * @param {boolean} append    If true, the new selection will be appended to the
 *                            current selection (except for duplicate entries)
 * @return {Boolean} changed  True if the selection is changed
 */
links.Network.prototype._selectNodes = function(selection, append) {
    var changed = false;
    var i, iMax;

    // TODO: the selectNodes method is a little messy, rework this

    // check if the current selection equals the desired selection
    var selectionAlreadyDone = true;
    if (selection.length != this.selection.length) {
        selectionAlreadyDone = false;
    }
    else {
        for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
            if (selection[i].row != this.selection[i].row) {
                selectionAlreadyDone = false;
                break;
            }
        }
    }
    if (selectionAlreadyDone) {
        return changed;
    }

    if (append == undefined || append == false) {
        // first deselect any selected node
        var triggerSelect = false;
        changed = this._unselectNodes(undefined, triggerSelect);
    }

    for (i = 0, iMax = selection.length; i < iMax; i++) {
        // add each of the new selections, but only when they are not duplicate
        var row = selection[i].row;
        var isDuplicate = false;
        for (var j = 0, jMax = this.selection.length; j < jMax; j++) {
            if (this.selection[j].row == row) {
                isDuplicate = true;
                break;
            }
        }

        if (!isDuplicate) {
            this.nodes[row].select();
            this.selection.push(selection[i]);
            changed = true;
        }
    }

    if (changed) {
        // fire the select event
        this.trigger('select');
    }

    return changed;
};

/**
 * retrieve all nodes overlapping with given object
 * @param {Object} obj  An object with parameters left, top, right, bottom
 * @return {Object[]}   An array with selection objects containing
 *                      the parameter row.
 */
links.Network.prototype._getNodesOverlappingWith = function (obj) {
    var overlappingNodes = [];

    for (var i = 0; i < this.nodes.length; i++) {
        if (this.nodes[i].isOverlappingWith(obj)) {
            var sel = {"row": i};
            overlappingNodes.push(sel);
        }
    }

    return overlappingNodes;
};

/**
 * retrieve the currently selected nodes
 * @return {Object[]} an array with zero or more objects. Each object
 *                              contains the parameter row
 */
links.Network.prototype.getSelection = function() {
    var selection = [];

    for (var i = 0; i < this.selection.length; i++) {
        var row = this.selection[i].row;
        selection.push({"row": row});
    }

    return selection;
};

/**
 * select zero or more nodes
 * @param {object[]} selection  an array with zero or more objects. Each object
 *                              contains the parameter row
 */
links.Network.prototype.setSelection = function(selection) {
    var i, iMax, row;

    if (selection.length == undefined)
        throw "Selection must be an array with objects";

    // first unselect any selected node
    for (i = 0, iMax = this.selection.length; i < iMax; i++) {
        row = this.selection[i].row;
        this.nodes[row].unselect();
    }

    this.selection = [];

    for (i = 0, iMax = selection.length; i < iMax; i++) {
        row = selection[i].row;

        if (row == undefined)
            throw "Parameter row missing in selection object";
        if (row > this.nodes.length-1)
            throw "Parameter row out of range";

        var sel = {"row": row};
        this.selection.push(sel);
        this.nodes[row].select();
    }

    this.redraw();
};


/**
 * Temporary method to test calculating a hub value for the nodes
 * @param {number} level        Maximum number links between two nodes in order
 *                              to call them connected. Optional, 1 by default
 * @return {Number[]} connectioncount array with the connection count
 *                                    for each node
 */
links.Network.prototype._getConnectionCount = function(level) {
    var conn = this.links;
    if (level == undefined) {
        level = 1;
    }

    // get the nodes connected to given nodes
    function getConnectedNodes(nodes) {
        var connectedNodes = [];

        for (var j = 0, jMax = nodes.length; j < jMax; j++) {
            var node = nodes[j];

            // find all nodes connected to this node
            for (var i = 0, iMax = conn.length; i < iMax; i++) {
                var other = null;

                // check if connected
                if (conn[i].from == node)
                    other = conn[i].to;
                else if (conn[i].to == node)
                    other = conn[i].from;

                // check if the other node is not already in the list with nodes
                var k, kMax;
                if (other) {
                    for (k = 0, kMax = nodes.length; k < kMax; k++) {
                        if (nodes[k] == other) {
                            other = null;
                            break;
                        }
                    }
                }
                if (other) {
                    for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
                        if (connectedNodes[k] == other) {
                            other = null;
                            break;
                        }
                    }
                }

                if (other)
                    connectedNodes.push(other);
            }
        }

        return connectedNodes;
    }

    var connections = [];
    var level0 = [];
    var nodes = this.nodes;
    var i, iMax;
    for (i = 0, iMax = nodes.length; i < iMax; i++) {
        var c = [nodes[i]];
        for (var l = 0; l < level; l++) {
            c = c.concat(getConnectedNodes(c));
        }
        connections.push(c);
    }

    var hubs = [];
    for (i = 0, len = connections.length; i < len; i++) {
        hubs.push(connections[i].length);
    }

    return hubs;
};


/**
 * Set a new size for the network
 * @param {string} width   Width in pixels or percentage (for example "800px"
 *                         or "50%")
 * @param {string} height  Height in pixels or percentage  (for example "400px"
 *                         or "30%")
 */
links.Network.prototype._setSize = function(width, height) {
    this.frame.style.width = width;
    this.frame.style.height = height;

    this.frame.canvas.style.width = "100%";
    this.frame.canvas.style.height = "100%";

    this.frame.canvas.width = this.frame.canvas.clientWidth;
    this.frame.canvas.height = this.frame.canvas.clientHeight;

    if (this.slider) {
        this.slider.redraw();
    }
};

/**
 * Convert a Google DataTable to a Javascript Array
 * @param {google.visualization.DataTable} table
 * @return {Array} array
 */
links.Network.tableToArray = function(table) {
    var array = [];
    var col;

    // read the column names
    var colCount = table.getNumberOfColumns();
    var cols = {};
    for (col = 0; col < colCount; col++) {
        var label = table.getColumnLabel(col);
        cols[label] = col;
    }

    var rowCount = table.getNumberOfRows();
    for (var i = 0; i < rowCount; i++) {
        // copy all properties from the table columns to an object
        var properties = {};
        for (col in cols) {
            if (cols.hasOwnProperty(col)) {
                properties[col] = table.getValue(i, cols[col]);
            }
        }

        array.push(properties);
    }

    return array;
};


/**
 * Append nodes
 * Nodes with a duplicate id will be replaced
 * @param {google.visualization.DataTable | Array} nodesTable  The data containing the nodes.
 */
links.Network.prototype.addNodes = function(nodesTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        nodesTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(nodesTable);
    }
    else if (links.Network.isArray(nodesTable)){
        // Javascript Array
        table = nodesTable;
    }
    else {
        return;
    }

    var hasValues = false;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.value != undefined) {
            hasValues = true;
        }
        if (properties.id == undefined) {
            throw "Column 'id' missing in table with nodes (row " + i + ")";
        }

        this._createNode(properties);
    }

    // calculate scaling function when value is provided
    if (hasValues) {
        this._updateValueRange(this.nodes);
    }

    this.start();
};

/**
 * Load all nodes by reading the data table nodesTable
 * Note that Object DataTable is defined in google.visualization.DataTable
 * @param {google.visualization.DataTable | Array} nodesTable    The data containing the nodes.
 */
links.Network.prototype.setNodes = function(nodesTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        nodesTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(nodesTable);
    }
    else if (links.Network.isArray(nodesTable)){
        // Javascript Array
        table = nodesTable;
    }
    else {
        return;
    }

    this.hasMovingNodes = false;
    this.nodesTable = table;
    this.nodes = [];
    this.selection = [];

    var hasValues = false;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.value != undefined) {
            hasValues = true;
        }
        if (properties.timestamp) {
            this.hasTimestamps = this.hasTimestamps || properties.timestamp;
        }
        if (properties.id == undefined) {
            throw "Column 'id' missing in table with nodes (row " + i + ")";
        }
        this._createNode(properties);
    }

    // calculate scaling function when value is provided
    if (hasValues) {
        this._updateValueRange(this.nodes);
    }
};

/**
 * Filter the current nodes table for nodes with a timestamp older than given
 * timestamp. Can only be used for nodes added via setNodes(), not via
 * addNodes().
 * @param {*} [timestamp]    If timestamp is undefined, all nodes are shown
 */
links.Network.prototype._filterNodes = function(timestamp) {
    if (this.nodesTable == undefined) {
        return;
    }

    // remove existing nodes with a too new timestamp
    if (timestamp !== undefined) {
        var ns = this.nodes;
        var n = 0;
        while (n < ns.length) {
            var t = ns[n].timestamp;
            if (t !== undefined && t > timestamp) {
                // remove this node
                ns.splice(n, 1);
            }
            else {
                n++;
            }
        }
    }

    // add all nodes with an old enough timestamp
    var table = this.nodesTable;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        // copy all properties
        var properties = table[i];

        if (properties.id === undefined) {
            throw "Column 'id' missing in table with nodes (row " + i + ")";
        }

        // check what the timestamp is
        var ts = properties.timestamp ? properties.timestamp : undefined;

        var visible = true;
        if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
            visible = false;
        }

        if (visible) {
            // create or update the node
            this._createNode(properties);
        }
    }

    this.start();
};

/**
 * Create a node with the given properties
 * If the new node has an id identical to an existing package, the existing
 * node will be overwritten.
 * The properties can contain a property "action", which can have values
 * "create", "update", or "delete"
 * @param {Object} properties  An object with properties
 */
links.Network.prototype._createNode = function(properties) {
    var action = properties.action ? properties.action : "update";
    var id, index, newNode, oldNode;

    if (action === "create") {
        // create the node
        newNode = new links.Network.Node(properties, this.images, this.groups, this.constants);
        id = properties.id;
        index = (id !== undefined) ? this._findNode(id) : undefined;

        if (index !== undefined) {
            // replace node
            oldNode = this.nodes[index];
            this.nodes[index] = newNode;

            // remove selection of old node
            if (oldNode.selected) {
                this._unselectNodes([{'row': index}], false);
            }

            /* TODO: implement this? -> will give performance issues, searching all links and node...
             // update links linking to this node
             var linksTable = this.links;
             for (var i = 0, iMax = linksTable.length; i < iMax; i++) {
             var link = linksTable[i];
             if (link.from == oldNode) {
             link.from = newNode;
             }
             if (link.to == oldNode) {
             link.to = newNode;
             }
             }

             // update packages linking to this node
             var packagesTable = this.packages;
             for (var i = 0, iMax = packagesTable.length; i < iMax; i++) {
             var package = packagesTable[i];
             if (package.from == oldNode) {
             package.from = newNode;
             }
             if (package.to == oldNode) {
             package.to = newNode;
             }
             }
             */
        }
        else {
            // add new node
            this.nodes.push(newNode);
        }

        if (!newNode.isFixed()) {
            // note: no not use node.isMoving() here, as that gives the current
            // velocity of the node, which is zero after creation of the node.
            this.hasMovingNodes = true;
        }
    }
    else if (action === "update") {
        // update existing node, or create it when not yet existing
        id = properties.id;
        if (id === undefined) {
            throw "Cannot update a node without id";
        }

        index = this._findNode(id);
        if (index !== undefined) {
            // update node
            this.nodes[index].setProperties(properties, this.constants);
        }
        else {
            // create node
            newNode = new links.Network.Node(properties, this.images, this.groups, this.constants);
            this.nodes.push(newNode);

            if (!newNode.isFixed()) {
                // note: no not use node.isMoving() here, as that gives the current
                // velocity of the node, which is zero after creation of the node.
                this.hasMovingNodes = true;
            }
        }
    }
    else if (action === "delete") {
        // delete existing node
        id = properties.id;
        if (id === undefined) {
            throw "Cannot delete node without its id";
        }

        index = this._findNode(id);
        if (index !== undefined) {
            oldNode = this.nodes[index];
            // remove selection of old node
            if (oldNode.selected) {
                this._unselectNodes([{'row': index}], false);
            }
            this.nodes.splice(index, 1);
        }
        else {
            throw "Node with id " + id + " not found";
        }
    }
    else {
        throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
    }
};

/**
 * Find a node by its id
 * @param {Number} id       Id of the node
 * @return {Number} index   Index of the node in the array this.nodes, or
 *                          undefined when not found. *
 */
links.Network.prototype._findNode = function (id) {
    var nodes = this.nodes;
    for (var n = 0, len = nodes.length; n < len; n++) {
        if (nodes[n].id === id) {
            return n;
        }
    }

    return undefined;
};

/**
 * Find a node by its rowNumber
 * @param {Number} row                   Row number of the node
 * @return {links.Network.Node} node     The node with the given row number, or
 *                                       undefined when not found.
 */
links.Network.prototype._findNodeByRow = function (row) {
    return this.nodes[row];
};

/**
 * Load links by reading the data table
 * Note that Object DataTable is defined in google.visualization.DataTable
 * @param {google.visualization.DataTable | Array}      linksTable    The data containing the links.
 */
links.Network.prototype.setLinks = function(linksTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        linksTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(linksTable);
    }
    else if (links.Network.isArray(linksTable)){
        // Javascript Array
        table = linksTable;
    }
    else {
        return;
    }

    this.linksTable = table;
    this.links = [];
    this.hasMovingLinks = false;

    var hasValues = false;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with links (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with links (row " + i + ")";
        }
        if (properties.timestamp != undefined) {
            this.hasTimestamps = this.hasTimestamps || properties.timestamp;
        }
        if (properties.value != undefined) {
            hasValues = true;
        }

        this._createLink(properties);
    }

    // calculate scaling function when value is provided
    if (hasValues) {
        this._updateValueRange(this.links);
    }
};


/**
 * Load links by reading the data table
 * Note that Object DataTable is defined in google.visualization.DataTable
 * @param {google.visualization.DataTable | Array}      linksTable    The data containing the links.
 */
links.Network.prototype.addLinks = function(linksTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        linksTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(linksTable);
    }
    else if (links.Network.isArray(linksTable)){
        // Javascript Array
        table = linksTable;
    }
    else {
        return;
    }

    var hasValues = false;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        // copy all properties
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with links (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with links (row " + i + ")";
        }
        if (properties.value != undefined) {
            hasValues = true;
        }

        this._createLink(properties);
    }

    // calculate scaling function when value is provided
    if (hasValues) {
        this._updateValueRange(this.links);
    }

    this.start();
};


/**
 * Filter the current links table for links with a timestamp below given
 * timestamp. Can only be used for links added via setLinks(), not via
 * addLinks().
 * @param {*} [timestamp]  If timestamp is undefined, all links are shown
 */
links.Network.prototype._filterLinks = function(timestamp) {
    if (this.linksTable == undefined) {
        return;
    }

    // remove existing packages with a too new timestamp
    if (timestamp !== undefined) {
        var ls = this.links;
        var l = 0;
        while (l < ls.length) {
            var t = ls[l].timestamp;
            if (t !== undefined && t > timestamp) {
                // remove this link
                ls.splice(l, 1);
            }
            else {
                l++;
            }
        }
    }

    // add all links with an old enough timestamp
    var table = this.linksTable;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with links (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with links (row " + i + ")";
        }

        // check what the timestamp is
        var ts = properties.timestamp ? properties.timestamp : undefined;

        var visible = true;
        if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
            visible = false;
        }

        if (visible) {
            // create or update the link
            this._createLink(properties);
        }
    }

    this.start();
};

/**
 * Create a link with the given properties
 * If the new link has an id identical to an existing link, the existing
 * link will be overwritten or updated.
 * The properties can contain a property "action", which can have values
 * "create", "update", or "delete"
 * @param {Object} properties   An object with properties
 */
links.Network.prototype._createLink = function(properties) {
    var action = properties.action ? properties.action : "create";
    var id, index, link, oldLink, newLink;

    if (action === "create") {
        // create the link, or replace it if already existing
        id = properties.id;
        index = (id !== undefined) ? this._findLink(id) : undefined;
        link = new links.Network.Link(properties, this, this.constants);

        if (index !== undefined) {
            // replace existing link
            oldLink = this.links[index];
            oldLink.from.detachLink(oldLink);
            oldLink.to.detachLink(oldLink);
            this.links[index] = link;
        }
        else {
            // add new link
            this.links.push(link);
        }
        link.from.attachLink(link);
        link.to.attachLink(link);

        if (link.isMoving()) {
            this.hasMovingLinks = true;
        }
    }
    else if (action === "update") {
        // update existing link, or create the link if not existing
        id = properties.id;
        if (id === undefined) {
            throw "Cannot update a link without id";
        }

        index = this._findLink(id);
        if (index !== undefined) {
            // update link
            link = this.links[index];
            link.from.detachLink(link);
            link.to.detachLink(link);

            link.setProperties(properties, this.constants);
            link.from.attachLink(link);
            link.to.attachLink(link);
        }
        else {
            // add new link
            link = new links.Network.Link(properties, this, this.constants);
            link.from.attachLink(link);
            link.to.attachLink(link);
            this.links.push(link);
            if (link.isMoving()) {
                this.hasMovingLinks = true;
            }
        }
    }
    else if (action === "delete") {
        // delete existing link
        id = properties.id;
        if (id === undefined) {
            throw "Cannot delete link without its id";
        }

        index = this._findLink(id);
        if (index !== undefined) {
            oldLink = this.links[index];
            link.from.detachLink(oldLink);
            link.to.detachLink(oldLink);
            this.links.splice(index, 1);
        }
        else {
            throw "Link with id " + id + " not found";
        }
    }
    else {
        throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
    }
};

/**
 * Update the link to oldNode in all links and packages.
 * @param {Node} oldNode
 * @param {Node} newNode
 */
// TODO: start utilizing this method _updateNodeReferences
links.Network.prototype._updateNodeReferences = function(oldNode, newNode) {
    var arrays = [this.links, this.packages];
    for (var a = 0, aMax = arrays.length; a < aMax; a++) {
        var array = arrays[a];
        for (var i = 0, iMax = array.length; i < iMax; i++) {
            if (array.from === oldNode) {
                array.from = newNode;
            }
            if (array.to === oldNode) {
                array.to = newNode;
            }
        }
    }
};

/**
 * Find a link by its id
 * @param {Number} id       Id of the link
 * @return {Number} index   Index of the link in the array this.links, or
 *                          undefined when not found. *
 */
links.Network.prototype._findLink = function (id) {
    var links = this.links;
    for (var n = 0, len = links.length; n < len; n++) {
        if (links[n].id === id) {
            return n;
        }
    }

    return undefined;
};

/**
 * Find a link by its row
 * @param {Number} row          Row of the link
 * @return {links.Network.Link} the found link, or undefined when not found
 */
links.Network.prototype._findLinkByRow = function (row) {
    return this.links[row];
};

/**
 * Append packages
 * Packages with a duplicate id will be replaced
 * Note that Object DataTable is defined in google.visualization.DataTable
 * @param {google.visualization.DataTable | Array}   packagesTable    The data containing the packages.
 */
links.Network.prototype.addPackages = function(packagesTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        packagesTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(packagesTable);
    }
    else if (links.Network.isArray(packagesTable)){
        // Javascript Array
        table = packagesTable;
    }
    else {
        return;
    }

    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with packages (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with packages (row " + i + ")";
        }

        this._createPackage(properties);
    }

    // calculate scaling function when value is provided
    this._updateValueRange(this.packages);

    this.start();
};

/**
 * Set a new packages table
 * Packages with a duplicate id will be replaced
 * Note that Object DataTable is defined in google.visualization.DataTable
 * @param {google.visualization.DataTable | Array}   packagesTable    The data containing the packages.
 */
links.Network.prototype.setPackages = function(packagesTable) {
    var table;
    if (google && google.visualization && google.visualization.DataTable &&
        packagesTable instanceof google.visualization.DataTable) {
        // Google DataTable. 
        // Convert to a Javascript Array
        table = links.Network.tableToArray(packagesTable);
    }
    else if (links.Network.isArray(packagesTable)){
        // Javascript Array
        table = packagesTable;
    }
    else {
        return;
    }

    this.packagesTable = table;
    this.packages = [];

    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with packages (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with packages (row " + i + ")";
        }
        if (properties.timestamp) {
            this.hasTimestamps = this.hasTimestamps || properties.timestamp;
        }

        this._createPackage(properties);
    }

    // calculate scaling function when value is provided
    this._updateValueRange(this.packages);

    /* TODO: adjust examples and documentation for this?
     this.start();
     */
};

/**
 * Filter the current package table for packages with a timestamp below given
 * timestamp. Can only be used for packages added via setPackages(), not via
 * addPackages().
 * @param {*} [timestamp] If timestamp is undefined, all packages are shown
 */
links.Network.prototype._filterPackages = function(timestamp) {
    if (this.packagesTable == undefined) {
        return;
    }

    // remove all current packages
    this.packages = [];

    /* TODO: cleanup
     // remove existing packages with a too new timestamp
     if (timestamp !== undefined) {
     var packages = this.packages;
     var p = 0;
     while (p < packages.length) {
     var package = packages[p];
     var t = package.timestamp;

     if (t !== undefined &&  t > timestamp ) {
     // remove this package
     packages.splice(p, 1);
     }
     else {
     p++;
     }
     }
     }
     */

    // add all packages with an old enough timestamp
    var table = this.packagesTable;
    var rowCount = table.length;
    for (var i = 0; i < rowCount; i++) {
        var properties = table[i];

        if (properties.from === undefined) {
            throw "Column 'from' missing in table with packages (row " + i + ")";
        }
        if (properties.to === undefined) {
            throw "Column 'to' missing in table with packages (row " + i + ")";
        }
        // check what the timestamp is
        var pTimestamp = properties.timestamp ? properties.timestamp : undefined;

        var visible = true;
        if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) {
            visible = false;
        }

        if (visible === true) {
            if (properties.progress == undefined) {
                // when no progress is provided, we need to add our own progress
                var duration = properties.duration || this.constants.packages.duration; // seconds

                var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds
                if (diff < duration) {
                    // copy the properties, and fill in the current progress based on the
                    // timestamp and the duration
                    var original = properties;
                    properties = {};
                    for (var j in original) {
                        if (original.hasOwnProperty(j)) {
                            properties[j] = original[j];
                        }
                    }

                    properties.progress = diff / duration;  // scale 0-1
                }
                else {
                    visible = false;
                }
            }
        }

        if (visible === true) {
            // create or update the package
            this._createPackage(properties);
        }
    }

    this.start();
};

/**
 * Create a package with the given properties
 * If the new package has an id identical to an existing package, the existing
 * package will be overwritten.
 * The properties can contain a property "action", which can have values
 * "create", "update", or "delete"
 * @param {Object} properties   An object with properties
 */
links.Network.prototype._createPackage = function(properties) {
    var action = properties.action ? properties.action : "create";
    var id, index, newPackage;

    if (action === "create") {
        // create the package
        id = properties.id;
        index = (id !== undefined) ? this._findPackage(id) : undefined;
        newPackage = new links.Network.Package(properties, this, this.images, this.constants);

        if (index !== undefined) {
            // replace existing package
            this.packages[index] = newPackage;
        }
        else {
            // add new package
            this.packages.push(newPackage);
        }

        if (newPackage.isMoving()) {
            this.hasMovingPackages = true;
        }
    }
    else if (action === "update") {
        // update a package, or create it when not existing
        id = properties.id;
        if (id === undefined) {
            throw "Cannot update a link without id";
        }

        index = this._findPackage(id);
        if (index !== undefined) {
            // update existing package 
            this.packages[index].setProperties(properties, this.constants);
        }
        else {
            // add new package
            newPackage = new links.Network.Package(properties, this, this.images, this.constants);
            this.packages.push(newPackage);
            if (newPackage.isMoving()) {
                this.hasMovingPackages = true;
            }
        }
    }
    else if (action === "delete") {
        // delete existing package
        id = properties.id;
        if (id === undefined) {
            throw "Cannot delete package without its id";
        }

        index = this._findPackage(id);
        if (index !== undefined) {
            this.packages.splice(index, 1);
        }
        else {
            throw "Package with id " + id + " not found";
        }
    }
    else {
        throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
    }
};

/**
 * Find a package by its id.
 * @param {Number} id
 * @return {Number} index    Index of the package in the array this.packages,
 *                           or undefined when not found
 */
links.Network.prototype._findPackage = function (id) {
    var packages = this.packages;
    for (var n = 0, len = packages.length; n < len; n++) {
        if (packages[n].id === id) {
            return n;
        }
    }

    return undefined;
};

/**
 * Find a package by its row
 * @param {Number} row          Row of the package
 * @return {links.Network.Package} the found package, or undefined when not found
 */
links.Network.prototype._findPackageByRow = function (row) {
    return this.packages[row];
};

/**
 * Retrieve an object which maps the column ids by their names
 * For example a table with columns [id, name, value] will return an
 * object {"id": 0, "name": 1, "value": 2}
 * @param {google.visualization.DataTable} table    A google datatable
 * @return {Object} columnIds   An object
 */
// TODO: cleanup this unused method
links.Network.prototype._getColumnNames = function (table) {
    var colCount = table.getNumberOfColumns();
    var cols = {};
    for (var col = 0; col < colCount; col++) {
        var label = table.getColumnLabel(col);
        cols[label] = col;
    }
    return cols;
};


/**
 * Update the values of all object in the given array according to the current
 * value range of the objects in the array.
 * @param {Array} array.  An array with objects like Links, Nodes, or Packages
 *                        The objects must have a method getValue() and
 *                        setValueRange(min, max).
 */
links.Network.prototype._updateValueRange = function(array) {
    var count = array.length;
    var i;

    // determine the range of the node values
    var valueMin = undefined;
    var valueMax = undefined;
    for (i = 0; i < count; i++) {
        var value = array[i].getValue();
        if (value !== undefined) {
            valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
            valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
        }
    }

    // adjust the range of all nodes
    if (valueMin !== undefined && valueMax !== undefined) {
        for (i = 0; i < count; i++) {
            array[i].setValueRange(valueMin, valueMax);
        }
    }
};


/**
 * Set the current timestamp. All packages with a timestamp smaller or equal
 * than the given timestamp will be drawn.
 * @param {Date | Number} timestamp
 */
links.Network.prototype.setTimestamp = function(timestamp) {
    this._filterNodes(timestamp);
    this._filterLinks(timestamp);
    this._filterPackages(timestamp);
};


/**
 * Get the range of all timestamps defined in the nodes, links and packages
 * @return {Object}   A range object, containing parameters start and end.
 */
links.Network.prototype._getRange = function() {
    // range is stored as number. at the end of the method, it is converted to
    // Date when needed.
    var range = {
        "start": undefined,
        "end": undefined
    };

    var tables = [this.nodesTable, this.linksTable];
    for (var t = 0, tMax = tables.length; t < tMax; t++) {
        var table = tables[t];

        if (table !== undefined) {
            for (var i = 0, iMax = table.length; i < iMax; i++) {
                var timestamp = table[i].timestamp;
                if (timestamp) {
                    // to long
                    if (timestamp instanceof Date) {
                        timestamp = timestamp.getTime();
                    }

                    // calculate new range
                    range.start = range.start ? Math.min(timestamp, range.start) : timestamp;
                    range.end = range.end ? Math.max(timestamp, range.end) : timestamp;
                }
            }
        }
    }

    // calculate the range for the packagesTable by hand. In case of packages
    // without a progress provided, we need to calculate the end time by hand.
    if (this.packagesTable) {
        var packagesTable = this.packagesTable;
        for (var row = 0, len = packagesTable.length; row < len; row ++) {
            var pkg = packagesTable[row],
                timestamp = pkg.timestamp,
                progress = pkg.progress,
                duration = pkg.duration || this.constants.packages.duration;

            // convert to number
            if (timestamp instanceof Date) {
                timestamp = timestamp.getTime();
            }

            if (timestamp != undefined) {
                var start = timestamp,
                    end = progress ? timestamp : (timestamp + duration * 1000);

                range.start = range.start ? Math.min(start, range.start) : start;
                range.end = range.end ? Math.max(end, range.end) : end;
            }
        }
    }

    // convert to the right type: number or date
    var rangeFormat = {
        "start": new Date(range.start),
        "end": new Date(range.end)
    };

    return rangeFormat;
};

/**
 * Start animation.
 * Only applicable when packages with a timestamp are available
 */
links.Network.prototype.animationStart = function() {
    if (this.slider) {
        this.slider.play();
    }
};

/**
 * Start animation.
 * Only applicable when packages with a timestamp are available
 */
links.Network.prototype.animationStop = function() {
    if (this.slider) {
        this.slider.stop();
    }
};

/**
 * Set framerate for the animation.
 * Only applicable when packages with a timestamp are available
 * @param {number} framerate    The framerate in frames per second
 */
links.Network.prototype.setAnimationFramerate = function(framerate) {
    if (this.slider) {
        this.slider.setFramerate(framerate);
    }
}

/**
 * Set the duration of playing the whole package history
 * Only applicable when packages with a timestamp are available
 * @param {number} duration    The duration in seconds
 */
links.Network.prototype.setAnimationDuration = function(duration) {
    if (this.slider) {
        this.slider.setDuration(duration);
    }
};

/**
 * Set the time acceleration for playing the history.
 * Only applicable when packages with a timestamp are available
 * @param {number} acceleration    Acceleration, for example 10 means play
 *                                 ten times as fast as real time. A value
 *                                 of 1 will play the history in real time.
 */
links.Network.prototype.setAnimationAcceleration = function(acceleration) {
    if (this.slider) {
        this.slider.setAcceleration(acceleration);
    }
};

/**
 * Redraw the network with the current data
 * chart will be resized too.
 */
links.Network.prototype.redraw = function() {
    this._setSize(this.width, this.height);

    this._redraw();
};

/**
 * Redraw the network with the current data
 */
links.Network.prototype._redraw = function() {
    var ctx = this.frame.canvas.getContext("2d");

    // clear the canvas
    var w = this.frame.canvas.width;
    var h = this.frame.canvas.height;
    ctx.clearRect(0, 0, w, h);

    // set scaling and translation
    ctx.save();
    ctx.translate(this.translation.x, this.translation.y);
    ctx.scale(this.scale, this.scale);

    this._drawLinks(ctx);
    this._drawNodes(ctx);
    this._drawPackages(ctx);
    this._drawSlider();

    // restore original scaling and translation
    ctx.restore();
};

/**
 * Set the translation of the network
 * @param {Number} offsetX    Horizontal offset
 * @param {Number} offsetY    Vertical offset
 */
links.Network.prototype._setTranslation = function(offsetX, offsetY) {
    if (this.translation === undefined) {
        this.translation = {
            "x": 0,
            "y": 0
        };
    }

    if (offsetX !== undefined) {
        this.translation.x = offsetX;
    }
    if (offsetY !== undefined) {
        this.translation.y = offsetY;
    }
};

/**
 * Get the translation of the network
 * @return {Object} translation    An object with parameters x and y, both a number
 */
links.Network.prototype._getTranslation = function() {
    return {
        "x": this.translation.x,
        "y": this.translation.y
    };
};

/**
 * Scale the network
 * @param {Number} scale   Scaling factor 1.0 is unscaled
 */
links.Network.prototype._setScale = function(scale) {
    this.scale = scale;
};
/**
 * Get the current scale of  the network
 * @return {Number} scale   Scaling factor 1.0 is unscaled
 */
links.Network.prototype._getScale = function() {
    return this.scale;
};

links.Network.prototype._xToCanvas = function(x) {
    return (x - this.translation.x) / this.scale;
};

links.Network.prototype._canvasToX = function(x) {
    return x * this.scale + this.translation.x;
};

links.Network.prototype._yToCanvas = function(y) {
    return (y - this.translation.y) / this.scale;
};

links.Network.prototype._canvasToY = function(y) {
    return y * this.scale + this.translation.y ;
};



/**
 * Get a node by its id
 * @param {number} id
 * @return {Node}  node, or null if not found
 */
links.Network.prototype._getNode = function(id) {
    for (var i = 0; i < this.nodes.length; i++) {
        if (this.nodes[i].id == id)
            return this.nodes[i];
    }

    return null;
};

/**
 * Redraw all nodes
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.prototype._drawNodes = function(ctx) {
    // first draw the unselected nodes
    var nodes = this.nodes;
    var selected = [];
    for (var i = 0, iMax = nodes.length; i < iMax; i++) {
        if (nodes[i].isSelected()) {
            selected.push(i);
        }
        else {
            nodes[i].draw(ctx);
        }
    }

    // draw the selected nodes on top
    for (var s = 0, sMax = selected.length; s < sMax; s++) {
        nodes[selected[s]].draw(ctx);
    }
};

/**
 * Redraw all links
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.prototype._drawLinks = function(ctx) {
    var links = this.links;
    for (var i = 0, iMax = links.length; i < iMax; i++) {
        links[i].draw(ctx);
    }
};

/**
 * Redraw all packages
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.prototype._drawPackages = function(ctx) {
    var packages = this.packages;
    for (var i = 0, iMax = packages.length; i < iMax; i++) {
        packages[i].draw(ctx);
    }
};


/**
 * Redraw the filter
 */
links.Network.prototype._drawSlider = function() {
    var sliderNode;
    if (this.hasTimestamps) {
        sliderNode = this.frame.slider;
        if (sliderNode === undefined) {
            sliderNode = document.createElement( "div" );
            sliderNode.style.position = "absolute";
            sliderNode.style.bottom = "0px";
            sliderNode.style.left = "0px";
            sliderNode.style.right = "0px";
            sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)";

            this.frame.slider = sliderNode;
            this.frame.slider.style.padding = "10px";
            //this.frame.filter.style.backgroundColor = "#EFEFEF";
            this.frame.appendChild(sliderNode);


            var range = this._getRange();
            this.slider = new links.Network.Slider(sliderNode);
            this.slider.setLoop(false);
            this.slider.setRange(range.start, range.end);

            // create an event handler
            var me = this;
            var onchange = function () {
                var timestamp = me.slider.getValue();
                me.setTimestamp(timestamp);
                // TODO: do only a redraw when the network is not still moving
                me.redraw();
            };
            this.slider.setOnChangeCallback(onchange);
            onchange(); // perform the first update by hand.
        }
    }
    else {
        sliderNode = this.frame.slider;
        if (sliderNode !== undefined) {
            this.frame.removeChild(sliderNode);
            this.frame.slider = undefined;
            this.slider = undefined;
        }
    }
};

/**
 * Recalculate the best positions for all nodes
 */
links.Network.prototype._reposition = function() {
    // TODO: implement function reposition


    /*
     var w = this.frame.canvas.clientWidth;
     var h = this.frame.canvas.clientHeight;
     for (var i = 0; i < this.nodes.length; i++) {
     if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random();
     if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random();
     }
     //*/

    //*
    // TODO
    var radius = this.constants.links.length * 2;
    var cx =  this.frame.canvas.clientWidth / 2;
    var cy =  this.frame.canvas.clientHeight / 2;
    for (var i = 0; i < this.nodes.length; i++) {
        var angle = 2*Math.PI * (i / this.nodes.length);

        if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle);
        if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle);

    }
    //*/

    /*
     // TODO
     var radius = this.constants.links.length * 2;
     var w = this.frame.canvas.clientWidth,
     h = this.frame.canvas.clientHeight;
     var cx =  this.frame.canvas.clientWidth / 2;
     var cy =  this.frame.canvas.clientHeight / 2;
     var s = Math.sqrt(this.nodes.length);
     for (var i = 0; i < this.nodes.length; i++) {
     //var angle = 2*Math.PI * (i / this.nodes.length);

     if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s);
     if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s);
     }
     //*/


    /*
     var cx =  this.frame.canvas.clientWidth / 2;
     var cy =  this.frame.canvas.clientHeight / 2;  
     for (var i = 0; i < this.nodes.length; i++) {
     this.nodes[i].x = cx;
     this.nodes[i].y = cy;
     }

     //*/

};


/**
 * Find a stable position for all nodes
 */
links.Network.prototype._doStabilize = function() {
    var start = new Date();

    // find stable position
    var count = 0;
    var vmin = this.constants.minVelocity;
    var stable = false;
    while (!stable && count < this.constants.maxIterations) {
        this._calculateForces();
        this._discreteStepNodes();
        stable = !this.isMoving(vmin);
        count++;
    }

    var end = new Date();

    //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
};

/**
 * Calculate the external forces acting on the nodes
 * Forces are caused by: links, repulsing forces between nodes, gravity
 */
links.Network.prototype._calculateForces = function(nodeId) {
    // create a local link to the nodes and links, that is faster
    var nodes = this.nodes,
        links = this.links;

    // gravity, add a small constant force to pull the nodes towards the center of 
    // the graph 
    // Also, the forces are reset to zero in this loop by using _setForce instead
    // of _addForce
    var gravity = 0.01,
        gx = this.frame.canvas.clientWidth / 2,
        gy = this.frame.canvas.clientHeight / 2;
    for (var n = 0; n < nodes.length; n++) {
        var dx = gx - nodes[n].x,
            dy = gy - nodes[n].y,
            angle = Math.atan2(dy, dx),
            fx = Math.cos(angle) * gravity,
            fy = Math.sin(angle) * gravity;

        this.nodes[n]._setForce(fx, fy);
    }

    // repulsing forces between nodes
    var minimumDistance = this.constants.nodes.distance,
        steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
    for (var n = 0; n < nodes.length; n++) {
        for (var n2 = n + 1; n2 < this.nodes.length; n2++) {
            //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
            //var dmin = (nodes[n].width + nodes[n2].width)/2  || minimumDistance, // TODO: dmin
            //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),

            // calculate normally distributed force
            var dx = nodes[n2].x - nodes[n].x,
                dy = nodes[n2].y - nodes[n].y,
                distance = Math.sqrt(dx * dx + dy * dy),
                angle = Math.atan2(dy, dx),

            // TODO: correct factor for repulsing force
            //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
            //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
                repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
                fx = Math.cos(angle) * repulsingforce,
                fy = Math.sin(angle) * repulsingforce;

            this.nodes[n]._addForce(-fx, -fy);
            this.nodes[n2]._addForce(fx, fy);
        }
        /* TODO: re-implement repulsion of links
        for (var l = 0; l < links.length; l++) {
        	var lx = links[l].from.x+(links[l].to.x - links[l].from.x)/2,
        	    ly = links[l].from.y+(links[l].to.y - links[l].from.y)/2,
        	    
                // calculate normally distributed force
                dx = nodes[n].x - lx,
                dy = nodes[n].y - ly,
                distance = Math.sqrt(dx * dx + dy * dy),
                angle = Math.atan2(dy, dx),
 

            // TODO: correct factor for repulsing force
            //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
            //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
                repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
                fx = Math.cos(angle) * repulsingforce,
                fy = Math.sin(angle) * repulsingforce;
            nodes[n]._addForce(fx, fy);
            links[l].from._addForce(-fx/2,-fy/2);
            links[l].to._addForce(-fx/2,-fy/2);            
        }
        */
    }

    // forces caused by the links, modelled as springs
    for (var l = 0, lMax = links.length; l < lMax; l++) {
        var link = links[l],

            dx = (link.to.x - link.from.x),
            dy = (link.to.y - link.from.y),
        //linkLength = (link.from.width + link.from.height + link.to.width + link.to.height)/2 || link.length, // TODO: dmin
        //linkLength = (link.from.width + link.to.width)/2 || link.length, // TODO: dmin
        //linkLength = 20 + ((link.from.width + link.to.width) || 0) / 2,
            linkLength = link.length,
            length =  Math.sqrt(dx * dx + dy * dy),
            angle = Math.atan2(dy, dx),

            springforce = link.stiffness * (linkLength - length),

            fx = Math.cos(angle) * springforce,
            fy = Math.sin(angle) * springforce;

        link.from._addForce(-fx, -fy);
        link.to._addForce(fx, fy);
    }

    /* TODO: re-implement repulsion of links
    // repulsing forces between links
    var minimumDistance = this.constants.links.distance,
        steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
    for (var l = 0; l < links.length; l++) {
    	//Keep distance from other link centers
    	for (var l2 = l + 1; l2 < this.links.length; l2++) {
        	//var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
            //var dmin = (nodes[n].width + nodes[n2].width)/2  || minimumDistance, // TODO: dmin
            //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
        	var lx = links[l].from.x+(links[l].to.x - links[l].from.x)/2,
        		ly = links[l].from.y+(links[l].to.y - links[l].from.y)/2,
        		l2x = links[l2].from.x+(links[l2].to.x - links[l2].from.x)/2,
        		l2y = links[l2].from.y+(links[l2].to.y - links[l2].from.y)/2,
                         	
            // calculate normally distributed force
            dx = l2x - lx,
            dy = l2y - ly,
            distance = Math.sqrt(dx * dx + dy * dy),
            angle = Math.atan2(dy, dx),
 

            // TODO: correct factor for repulsing force
            //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
            //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
            repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
            fx = Math.cos(angle) * repulsingforce,
            fy = Math.sin(angle) * repulsingforce;
        		
        	links[l].from._addForce(-fx, -fy);
        	links[l].to._addForce(-fx, -fy);
        	links[l2].from._addForce(fx, fy);
        	links[l2].to._addForce(fx, fy);            
        }
    }
    */
};


/**
 * Check if any of the nodes is still moving
 * @param {number} vmin   the minimum velocity considered as "moving"
 * @return {boolean}      true if moving, false if non of the nodes is moving
 */
links.Network.prototype.isMoving = function(vmin) {
    // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
    var nodes = this.nodes;
    for (var n = 0, nMax = nodes.length; n < nMax; n++) {
        if (nodes[n].isMoving(vmin)) {
            return true;
        }
    }
    return false;
};


/**
 * Perform one discrete step for all nodes
 */
links.Network.prototype._discreteStepNodes = function() {
    var interval = this.refreshRate / 1000.0; // in seconds
    var nodes = this.nodes;
    for (var n = 0, nMax = nodes.length; n < nMax; n++) {
        nodes[n].discreteStep(interval);
    }
};


/**
 * Perform one discrete step for all packages
 */
links.Network.prototype._discreteStepPackages = function() {
    var interval = this.refreshRate / 1000.0; // in seconds
    var packages = this.packages;
    for (var n = 0, nMax = packages.length; n < nMax; n++) {
        packages[n].discreteStep(interval);
    }
};


/**
 * Cleanup finished packages.
 * also checks if there are moving packages
 */
links.Network.prototype._deleteFinishedPackages = function() {
    var n = 0;
    var hasMovingPackages = false;
    while (n < this.packages.length) {
        if (this.packages[n].isFinished()) {
            this.packages.splice(n, 1);
            n--;
        }
        else if (this.packages[n].isMoving()) {
            hasMovingPackages = true;
        }
        n++;
    }

    this.hasMovingPackages = hasMovingPackages;
};

/**
 * Start animating nodes, links, and packages.
 */
links.Network.prototype.start = function() {
    if (this.hasMovingNodes) {
        this._calculateForces();
        this._discreteStepNodes();

        var vmin = this.constants.minVelocity;
        this.hasMovingNodes = this.isMoving(vmin);
    }

    if (this.hasMovingPackages) {
        this._discreteStepPackages();
        this._deleteFinishedPackages();
    }

    if (this.hasMovingNodes || this.hasMovingLinks || this.hasMovingPackages) {
        // start animation. only start timer if it is not already running
        if (!this.timer) {
            var network = this;
            this.timer = window.setTimeout(function () {
                network.timer = undefined;
                network.start();
                network._redraw();
            }, this.refreshRate);
        }
    }
    else {
        this._redraw();
    }
};

/**
 * Stop animating nodes, links, and packages.
 */
links.Network.prototype.stop = function () {
    if (this.timer) {
        window.clearInterval(this.timer);
        this.timer = undefined;
    }
};



/**--------------------------------------------------------------------------**/


/**
 * Add and event listener. Works for all browsers
 * @param {Element}     element    An html element
 * @param {String}      action     The action, for example "click",
 *                                 without the prefix "on"
 * @param {function}    listener   The callback function to be executed
 * @param {boolean}     useCapture
 */
links.Network.addEventListener = function (element, action, listener, useCapture) {
    if (element.addEventListener) {
        if (useCapture === undefined)
            useCapture = false;

        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
            action = "DOMMouseScroll";  // For Firefox
        }

        element.addEventListener(action, listener, useCapture);
    } else {
        element.attachEvent("on" + action, listener);  // IE browsers
    }
};

/**
 * Remove an event listener from an element
 * @param {Element}  element   An html dom element
 * @param {string}       action    The name of the event, for example "mousedown"
 * @param {function}     listener  The listener function
 * @param {boolean}      useCapture
 */
links.Network.removeEventListener = function(element, action, listener, useCapture) {
    if (element.removeEventListener) {
        // non-IE browsers
        if (useCapture === undefined)
            useCapture = false;

        if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
            action = "DOMMouseScroll";  // For Firefox
        }

        element.removeEventListener(action, listener, useCapture);
    } else {
        // IE browsers
        element.detachEvent("on" + action, listener);
    }
};


/**
 * Stop event propagation
 */
links.Network.stopPropagation = function (event) {
    if (!event)
        event = window.event;

    if (event.stopPropagation) {
        event.stopPropagation();  // non-IE browsers
    }
    else {
        event.cancelBubble = true;  // IE browsers
    }
};


/**
 * Cancels the event if it is cancelable, without stopping further propagation of the event.
 */
links.Network.preventDefault = function (event) {
    if (!event)
        event = window.event;

    if (event.preventDefault) {
        event.preventDefault();  // non-IE browsers
    }
    else {
        event.returnValue = false;  // IE browsers
    }
};

/**
 * Retrieve the absolute left value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {number} left        The absolute left position of this element
 *                              in the browser page.
 */
links.Network._getAbsoluteLeft = function(elem) {
    var left = 0;
    while( elem != null ) {
        left += elem.offsetLeft;
        left -= elem.scrollLeft;
        elem = elem.offsetParent;
    }
    if (!document.body.scrollLeft && window.pageXOffset) {
        // FF
        left -= window.pageXOffset;
    }
    return left;
};

/**
 * Retrieve the absolute top value of a DOM element
 * @param {Element} elem    A dom element, for example a div
 * @return {number} top         The absolute top position of this element
 *                              in the browser page.
 */
links.Network._getAbsoluteTop = function(elem) {
    var top = 0;
    while( elem != null ) {
        top += elem.offsetTop;
        top -= elem.scrollTop;
        elem = elem.offsetParent;
    }
    if (!document.body.scrollTop && window.pageYOffset) {
        // FF
        top -= window.pageYOffset;
    }
    return top;
};



/**--------------------------------------------------------------------------**/


/**
 * @class Node
 * A node. A node can be connected to other nodes via one or multiple links.
 * @param {object} properties An object containing properties for the node. All
 *                            properties are optional, except for the id.
 *                              {number} id     Id of the node. Required
 *                              {string} text   Title for the node
 *                              {number} x      Horizontal position of the node
 *                              {number} y      Vertical position of the node
 *                              {string} style  Drawing style, available:
 *                                              "database", "circle", "rect",
 *                                              "image", "text", "dot", "star",
 *                                              "triangle", "triangleDown",
 *                                              "square"
 *                              {string} image  An image url
 *                              {string} title  An title text, can be HTML
 *                              {anytype} group A group name or number
 * @param {links.Network.Images} imagelist    A list with images. Only needed
 *                                            when the node has an image
 * @param {links.Network.Groups} grouplist    A list with groups. Needed for
 *                                            retrieving group properties
 * @param {Object}               constants    An object with default values for
 *                                            example for the color
 */
links.Network.Node = function (properties, imagelist, grouplist, constants) {
    this.selected = false;

    this.links = []; // all links connected to this node
    this.group = constants.nodes.group;

    this.fontSize = constants.nodes.fontSize;
    this.fontFace = constants.nodes.fontFace;
    this.fontColor = constants.nodes.fontColor;

    this.borderColor = constants.nodes.borderColor;
    this.backgroundColor = constants.nodes.backgroundColor;
    this.highlightColor = constants.nodes.highlightColor;

    // set defaults for the properties
    this.id = undefined;
    this.style = constants.nodes.style;
    this.image = constants.nodes.image;
    this.x = 0;
    this.y = 0;
    this.xFixed = false;
    this.yFixed = false;
    this.radius = constants.nodes.radius;
    this.radiusFixed = false;
    this.radiusMin = constants.nodes.radiusMin;
    this.radiusMax = constants.nodes.radiusMax;

    this.imagelist = imagelist;
    this.grouplist = grouplist;

    this.setProperties(properties, constants);

    // mass, force, velocity
    this.mass = 50;  // kg (mass is adjusted for the number of connected edges)
    this.fx = 0.0;  // external force x
    this.fy = 0.0;  // external force y
    this.vx = 0.0;  // velocity x
    this.vy = 0.0;  // velocity y
    this.minForce = constants.minForce;
    this.damping = 0.9; // damping factor
};

/**
 * Attach a link to the node
 * @param {links.Network.Link} link
 */
links.Network.Node.prototype.attachLink = function(link) {
    this.links.push(link);
    this._updateMass();
};

/**
 * Detach a link from the node
 * @param {links.Network.Link} link
 */
links.Network.Node.prototype.detachLink = function(link) {
    var index = this.links.indexOf(link);
    if (index != -1) {
        this.links.splice(index, 1);
    }
    this._updateMass();
};

/**
 * Update the nodes mass, which is determined by the number of edges connecting
 * to it (more edges -> heavier node).
 * @private
 */
links.Network.Node.prototype._updateMass = function() {
    this.mass = 50 + 20 * this.links.length; // kg
};

/**
 * Set or overwrite properties for the node
 * @param {Object} properties an object with properties
 * @param {Object} constants  and object with default, global properties
 */
links.Network.Node.prototype.setProperties = function(properties, constants) {
    if (!properties) {
        return;
    }

    // basic properties
    if (properties.id != undefined)        {this.id = properties.id;}
    if (properties.text != undefined)      {this.text = properties.text;}
    if (properties.title != undefined)     {this.title = properties.title;}
    if (properties.group != undefined)     {this.group = properties.group;}
    if (properties.x != undefined)         {this.x = properties.x;}
    if (properties.y != undefined)         {this.y = properties.y;}
    if (properties.value != undefined)     {this.value = properties.value;}
    if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}

    if (this.id === undefined) {
        throw "Node must have an id";
    }

    // copy group properties
    if (this.group) {
        var groupObj = this.grouplist.get(this.group);
        for (var prop in groupObj) {
            if (groupObj.hasOwnProperty(prop)) {
                this[prop] = groupObj[prop];
            }
        }
    }

    // individual style properties
    if (properties.style != undefined)          {this.style = properties.style;}
    if (properties.image != undefined)          {this.image = properties.image;}
    if (properties.radius != undefined)         {this.radius = properties.radius;}
    if (properties.borderColor != undefined)    {this.borderColor = properties.borderColor;}
    if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;}
    if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;}
    if (properties.fontColor != undefined)      {this.fontColor = properties.fontColor;}
    if (properties.fontSize != undefined)       {this.fontSize = properties.fontSize;}
    if (properties.fontFace != undefined)       {this.fontFace = properties.fontFace;}


    if (this.image != undefined) {
        if (this.imagelist) {
            this.imageObj = this.imagelist.load(this.image);
        }
        else {
            throw "No imagelist provided";
        }
    }

    this.xFixed = this.xFixed || (properties.x != undefined);
    this.yFixed = this.yFixed || (properties.y != undefined);
    this.radiusFixed = this.radiusFixed || (properties.radius != undefined);

    if (this.style == 'image') {
        this.radiusMin = constants.nodes.widthMin;
        this.radiusMax = constants.nodes.widthMax;
    }

    // choose draw method depending on the style
    var style = this.style;
    switch (style) {
        case 'database':      this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
        case 'rect':          this.draw = this._drawRect; this.resize = this._resizeRect; break;
        case 'circle':        this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
        // TODO: add ellipse shape
        // TODO: add diamond shape
        case 'image':         this.draw = this._drawImage; this.resize = this._resizeImage; break;
        case 'text':          this.draw = this._drawText; this.resize = this._resizeText; break;
        case 'dot':           this.draw = this._drawDot; this.resize = this._resizeShape; break;
        case 'square':        this.draw = this._drawSquare; this.resize = this._resizeShape; break;
        case 'triangle':      this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
        case 'triangleDown':  this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
        case 'star':          this.draw = this._drawStar; this.resize = this._resizeShape; break;
        default:              this.draw = this._drawRect; this.resize = this._resizeRect; break;
    }

    // reset the size of the node, this can be changed
    this._reset();
};

/**
 * select this node
 */
links.Network.Node.prototype.select = function() {
    this.selected = true;
    this._reset();
};

/**
 * unselect this node
 */
links.Network.Node.prototype.unselect = function() {
    this.selected = false;
    this._reset();
};

/**
 * Reset the calculated size of the node, forces it to recalculate its size
 */
links.Network.Node.prototype._reset = function() {
    this.width = undefined;
    this.height = undefined;
};

/**
 * get the title of this node.
 * @return {string} title    The title of the node, or undefined when no title
 *                           has been set.
 */
links.Network.Node.prototype.getTitle = function() {
    return this.title;
};

/**
 * Calculate the distance to the border of the Node
 * @param {CanvasRenderingContext2D}   ctx
 * @param {Number} angle        Angle in radians
 * @returns {number} distance   Distance to the border in pixels
 */
links.Network.Node.prototype.distanceToBorder = function (ctx, angle) {
    var borderWidth = 1;

    if (!this.width) {
        this.resize(ctx);
    }

    //noinspection FallthroughInSwitchStatementJS
    switch (this.style) {
        case 'circle':
        case 'dot':
            return this.radius + borderWidth;

        // TODO: implement distanceToBorder for database
        // TODO: implement distanceToBorder for triangle
        // TODO: implement distanceToBorder for triangleDown

        case 'rect':
        case 'image':
        case 'text':
        default:
            if (this.width) {
                return Math.min(
                    Math.abs(this.width / 2 / Math.cos(angle)),
                    Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
                // TODO: reckon with border radius too in case of rect
            }
            else {
                return 0;
            }

    }

    // TODO: implement calculation of distance to border for all shapes
};

/**
 * Set forces acting on the node
 * @param {number} fx   Force in horizontal direction
 * @param {number} fy   Force in vertical direction
 */
links.Network.Node.prototype._setForce = function(fx, fy) {
    this.fx = fx;
    this.fy = fy;
};

/**
 * Add forces acting on the node
 * @param {number} fx   Force in horizontal direction
 * @param {number} fy   Force in vertical direction
 */
links.Network.Node.prototype._addForce = function(fx, fy) {
    this.fx += fx;
    this.fy += fy;
};

/**
 * Perform one discrete step for the node
 * @param {number} interval    Time interval in seconds
 */
links.Network.Node.prototype.discreteStep = function(interval) {
    if (!this.xFixed) {
        var dx   = -this.damping * this.vx;     // damping force
        var ax   = (this.fx + dx) / this.mass;  // acceleration
        this.vx += ax / interval;               // velocity
        this.x  += this.vx / interval;          // position
    }

    if (!this.yFixed) {
        var dy   = -this.damping * this.vy;     // damping force
        var ay   = (this.fy + dy) / this.mass;  // acceleration
        this.vy += ay / interval;               // velocity
        this.y  += this.vy / interval;          // position
    }
};


/**
 * Check if this node has a fixed x and y position
 * @return {boolean}      true if fixed, false if not
 */
links.Network.Node.prototype.isFixed = function() {
    return (this.xFixed && this.yFixed);
};

/**
 * Check if this node is moving
 * @param {number} vmin   the minimum velocity considered as "moving"
 * @return {boolean}      true if moving, false if it has no velocity
 */
// TODO: replace this method with calculating the kinetic energy
links.Network.Node.prototype.isMoving = function(vmin) {
    return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
        (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
        (!this.yFixed && Math.abs(this.fy) > this.minForce));
};

/**
 * check if this node is selecte
 * @return {boolean} selected   True if node is selected, else false
 */
links.Network.Node.prototype.isSelected = function() {
    return this.selected;
};

/**
 * Retrieve the value of the node. Can be undefined
 * @return {Number} value
 */
links.Network.Node.prototype.getValue = function() {
    return this.value;
};

/**
 * Calculate the distance from the nodes location to the given location (x,y)
 * @param {Number} x
 * @param {Number} y
 * @return {Number} value
 */
links.Network.Node.prototype.getDistance = function(x, y) {
    var dx = this.x - x,
        dy = this.y - y;
    return Math.sqrt(dx * dx + dy * dy);
};


/**
 * Adjust the value range of the node. The node will adjust it's radius
 * based on its value.
 * @param {Number} min
 * @param {Number} max
 */
links.Network.Node.prototype.setValueRange = function(min, max) {
    if (!this.radiusFixed && this.value !== undefined) {
        var diff = (max - min);
        if (diff) {
            var scale = (this.radiusMax - this.radiusMin) / diff;
            this.radius = (this.value - min) * scale + this.radiusMin;
        }
        else {
            this.radius = this.radiusMin;
        }
    }
};

/**
 * Draw this node in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Node.prototype.draw = function(ctx) {
    throw "Draw method not initialized for node";
};

/**
 * Recalculate the size of this node in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Node.prototype.resize = function(ctx) {
    throw "Resize method not initialized for node";
};

/**
 * Check if this object is overlapping with the provided object
 * @param {Object} obj   an object with parameters left, top, right, bottom
 * @return {boolean}     True if location is located on node
 */
links.Network.Node.prototype.isOverlappingWith = function(obj) {
    return (this.left          < obj.right &&
        this.left + this.width > obj.left &&
        this.top               < obj.bottom &&
        this.top + this.height > obj.top);
};

links.Network.Node.prototype._resizeImage = function (ctx) {
    // TODO: pre calculate the image size
    if (!this.width) {  // undefined or 0
        var width, height;
        if (this.value) {
            var scale = this.imageObj.height / this.imageObj.width;
            width = this.radius || this.imageObj.width;
            height = this.radius * scale || this.imageObj.height;
        }
        else {
            width = this.imageObj.width;
            height = this.imageObj.height;
        }
        this.width  = width;
        this.height = height;
    }
};

links.Network.Node.prototype._drawImage = function (ctx) {
    this._resizeImage(ctx);

    this.left   = this.x - this.width / 2;
    this.top    = this.y - this.height / 2;

    var yText;
    if (this.imageObj) {
        ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
        yText = this.y + this.height / 2;
    }
    else {
        // image still loading... just draw the text for now
        yText = this.y;
    }

    this._text(ctx, this.text, this.x, yText, undefined, "top");
};


links.Network.Node.prototype._resizeRect = function (ctx) {
    if (!this.width) {
        var margin = 5;
        var textSize = this.getTextSize(ctx);
        this.width = textSize.width + 2 * margin;
        this.height = textSize.height + 2 * margin;
    }
};

links.Network.Node.prototype._drawRect = function (ctx) {
    this._resizeRect(ctx);

    this.left = this.x - this.width / 2;
    this.top = this.y - this.height / 2;

    ctx.strokeStyle = this.borderColor;
    ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
    ctx.lineWidth = this.selected ? 2.0 : 1.0;
    ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
    ctx.fill();
    ctx.stroke();

    this._text(ctx, this.text, this.x, this.y);
};


links.Network.Node.prototype._resizeDatabase = function (ctx) {
    if (!this.width) {
        var margin = 5;
        var textSize = this.getTextSize(ctx);
        var size = textSize.width + 2 * margin;
        this.width = size;
        this.height = size;
    }
};

links.Network.Node.prototype._drawDatabase = function (ctx) {
    this._resizeDatabase(ctx);
    this.left = this.x - this.width / 2;
    this.top = this.y - this.height / 2;

    ctx.strokeStyle = this.borderColor;
    ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
    ctx.lineWidth = this.selected ? 2.0 : 1.0;
    ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
    ctx.fill();
    ctx.stroke();

    this._text(ctx, this.text, this.x, this.y);
};


links.Network.Node.prototype._resizeCircle = function (ctx) {
    if (!this.width) {
        var margin = 5;
        var textSize = this.getTextSize(ctx);
        var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
        this.radius = diameter / 2;

        this.width = diameter;
        this.height = diameter;
    }
};

links.Network.Node.prototype._drawCircle = function (ctx) {
    this._resizeCircle(ctx);
    this.left = this.x - this.width / 2;
    this.top = this.y - this.height / 2;

    ctx.strokeStyle = this.borderColor;
    ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
    ctx.lineWidth = this.selected ? 2.0 : 1.0;
    ctx.circle(this.x, this.y, this.radius);
    ctx.fill();
    ctx.stroke();

    this._text(ctx, this.text, this.x, this.y);
};

links.Network.Node.prototype._drawDot = function (ctx) {
    this._drawShape(ctx, 'circle');
};

links.Network.Node.prototype._drawTriangle = function (ctx) {
    this._drawShape(ctx, 'triangle');
};

links.Network.Node.prototype._drawTriangleDown = function (ctx) {
    this._drawShape(ctx, 'triangleDown');
};

links.Network.Node.prototype._drawSquare = function (ctx) {
    this._drawShape(ctx, 'square');
};

links.Network.Node.prototype._drawStar = function (ctx) {
    this._drawShape(ctx, 'star');
};

links.Network.Node.prototype._resizeShape = function (ctx) {
    if (!this.width) {
        var size = 2 * this.radius;
        this.width = size;
        this.height = size;
    }
};

links.Network.Node.prototype._drawShape = function (ctx, shape) {
    this._resizeShape(ctx);

    this.left = this.x - this.width / 2;
    this.top = this.y - this.height / 2;

    ctx.strokeStyle = this.borderColor;
    ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
    ctx.lineWidth = this.selected ? 2.0 : 1.0;

    ctx[shape](this.x, this.y, this.radius);
    ctx.fill();
    ctx.stroke();

    if (this.text) {
        this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top');
    }
};

links.Network.Node.prototype._resizeText = function (ctx) {
    if (!this.width) {
        var margin = 5;
        var textSize = this.getTextSize(ctx);
        this.width = textSize.width + 2 * margin;
        this.height = textSize.height + 2 * margin;
    }
};

links.Network.Node.prototype._drawText = function (ctx) {
    this._resizeText(ctx);
    this.left = this.x - this.width / 2;
    this.top = this.y - this.height / 2;

    this._text(ctx, this.text, this.x, this.y);
};


links.Network.Node.prototype._text = function (ctx, text, x, y, align, baseline) {
    if (text) {
        ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
        ctx.fillStyle = this.fontColor || "black";
        ctx.textAlign = align || "center";
        ctx.textBaseline = baseline || "middle";

        var lines = text.split('\n'),
            lineCount = lines.length,
            fontSize = (this.fontSize + 4),
            yLine = y + (1 - lineCount) / 2 * fontSize;

        for (var i = 0; i < lineCount; i++) {
            ctx.fillText(lines[i], x, yLine);
            yLine += fontSize;
        }
    }
};


links.Network.Node.prototype.getTextSize = function(ctx) {
    if (this.text != undefined) {
        ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;

        var lines = this.text.split('\n'),
            height = (this.fontSize + 4) * lines.length,
            width = 0;

        for (var i = 0, iMax = lines.length; i < iMax; i++) {
            width = Math.max(width, ctx.measureText(lines[i]).width);
        }

        return {"width": width, "height": height};
    }
    else {
        return {"width": 0, "height": 0};
    }
};



/**--------------------------------------------------------------------------**/


/**
 * @class Link
 *
 * A link connects two nodes
 * @param {Object} properties     Object with properties. Must contain
 *                                At least properties from and to.
 *                                Available properties: from (number),
 *                                to (number), color (string),
 *                                width (number), style (string),
 *                                length (number), title (string)
 * @param {links.Network} network A network object, used to find and link to
 *                                nodes.
 * @param {Object} constants      An object with default values for
 *                                example for the color
 */
links.Network.Link = function (properties, network, constants) {
    if (!network) {
        throw "No network provided";
    }
    this.network = network;

    // initialize constants
    this.widthMin = constants.links.widthMin;
    this.widthMax = constants.links.widthMax;

    // initialize variables
    this.id     = undefined;
    this.style  = constants.links.style;
    this.title  = undefined;
    this.width  = constants.links.width;
    this.value  = undefined;
    this.length = constants.links.length;
    
    // Added to support dashed lines
    // David Jordan
    // 2012-08-08
    this.dashlength = constants.links.dashlength;
    this.dashgap = constants.links.dashgap;
    this.altdashlength  = constants.links.altdashlength;
    
    this.stiffness = undefined; // depends on the length of the link
    this.color  = constants.links.color;
    this.timestamp  = undefined;
    this.widthFixed = false;
    this.lengthFixed = false;

    this.setProperties(properties, constants);
};

/**
 * Set or overwrite properties for the link
 * @param {Object} properties  an object with properties
 * @param {Object} constants   and object with default, global properties
 */
links.Network.Link.prototype.setProperties = function(properties, constants) {
    if (!properties) {
        return;
    }

    if (properties.from != undefined) {this.from = this.network._getNode(properties.from);}
    if (properties.to != undefined) {this.to = this.network._getNode(properties.to);}

    if (properties.id != undefined)         {this.id = properties.id;}
    if (properties.style != undefined)      {this.style = properties.style;}
    if (properties.text != undefined)       {this.text = properties.text;}
    if (this.text) {
        this.fontSize = constants.links.fontSize;
        this.fontFace = constants.links.fontFace;
        this.fontColor = constants.links.fontColor;
        if (properties.fontColor != undefined)  {this.fontColor = properties.fontColor;}
        if (properties.fontSize != undefined)   {this.fontSize = properties.fontSize;}
        if (properties.fontFace != undefined)   {this.fontFace = properties.fontFace;}
    }
    if (properties.title != undefined)      {this.title = properties.title;}
    if (properties.width != undefined)      {this.width = properties.width;}
    if (properties.value != undefined)      {this.value = properties.value;}
    if (properties.length != undefined)     {this.length = properties.length;}

    // Added to support dashed lines
    // David Jordan
    // 2012-08-08
    if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
    if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
    if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
    
    if (properties.color != undefined) {this.color = properties.color;}
    if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}

    if (!this.from) {
        throw "Node with id " + properties.from + " not found";
    }
    if (!this.to) {
        throw "Node with id " + properties.to + " not found";
    }

    this.widthFixed = this.widthFixed || (properties.width != undefined);
    this.lengthFixed = this.lengthFixed || (properties.length != undefined);

    this.stiffness = 1 / this.length;

    // initialize animation
    if (this.style === 'arrow') {
        this.arrows = [0.5];
        this.animation = false;
    }
    else if (this.style === 'arrow-end') {
        this.animation = false;
    }
    else if (this.style === 'moving-arrows') {
        this.arrows = [];
        var arrowCount = 3; // TODO: make customizable
        for (var a = 0; a < arrowCount; a++) {
            this.arrows.push(a / arrowCount);
        }
        this.animation = true;
    }
    else if (this.style === 'moving-dot') {
        this.dot = 0.0;
        this.animation = true;
    }
    else {
        this.animation = false;
    }

    // set draw method based on style
    switch (this.style) {
        case 'line':          this.draw = this._drawLine; break;
        case 'arrow':         this.draw = this._drawArrow; break;
        case 'arrow-end':     this.draw = this._drawArrowEnd; break;
        case 'moving-arrows': this.draw = this._drawMovingArrows; break;
        case 'moving-dot':    this.draw = this._drawMovingDot; break;
        case 'dash-line':     this.draw = this._drawDashLine; break;
        default:              this.draw = this._drawLine; break;
    }
};



/**
 * Check if a node has an animating contents. If so, the graph needs to be
 * redrawn regularly
 * @return {boolean}  true if this link needs animation, else false
 */
links.Network.Link.prototype.isMoving = function() {
    // TODO: be able to set the interval somehow

    return this.animation;
};

/**
 * get the title of this link.
 * @return {string} title    The title of the link, or undefined when no title
 *                           has been set.
 */
links.Network.Link.prototype.getTitle = function() {
    return this.title;
};


/**
 * Retrieve the value of the link. Can be undefined
 * @return {Number} value
 */
links.Network.Link.prototype.getValue = function() {
    return this.value;
}

/**
 * Adjust the value range of the link. The link will adjust it's width
 * based on its value.
 * @param {Number} min
 * @param {Number} max
 */
links.Network.Link.prototype.setValueRange = function(min, max) {
    if (!this.widthFixed && this.value !== undefined) {
        var factor = (this.widthMax - this.widthMin) / (max - min);
        this.width = (this.value - min) * factor + this.widthMin;
    }
};


/**
 * Check if the length is fixed.
 * @return {boolean} lengthFixed   True if the length is fixed, else false
 */
links.Network.Link.prototype.isLengthFixed = function() {
    return this.lengthFixed;
};

/**
 * Retrieve the length of the link. Can be undefined
 * @return {Number} length
 */
links.Network.Link.prototype.getLength = function() {
    return this.length;
};

/**
 * Adjust the length of the link. This can only be done when the length
 * is not fixed (which is the case when the link is created with a length property)
 * @param {Number} length
 */
links.Network.Link.prototype.setLength = function(length) {
    if (!this.lengthFixed) {
        this.length = length;
    }
};

/**
 * Retrieve the length of the links dashes. Can be undefined
 * @author David Jordan
 * @date 2012-08-08
 * @return {Number} dashlength
 */
links.Network.Link.prototype.getDashLength = function() {
    return this.dashlength;
};

/**
 * Adjust the length of the links dashes. 
 * @author David Jordan
 * @date 2012-08-08
 * @param {Number} dashlength
 */
links.Network.Link.prototype.setDashLength = function(dashlength) {
    this.dashlength = dashlength;
};

/**
 * Retrieve the length of the links dashes gaps. Can be undefined
 * @author David Jordan
 * @date 2012-08-08
 * @return {Number} dashgap
 */
links.Network.Link.prototype.getDashGap = function() {
    return this.dashgap;
};

/**
 * Adjust the length of the links dashes gaps. 
 * @author David Jordan
 * @date 2012-08-08
 * @param {Number} dashgap
 */
links.Network.Link.prototype.setDashGap = function(dashgap) {
    this.dashgap = dashgap;
};

/**
 * Retrieve the length of the links alternate dashes. Can be undefined
 * @author David Jordan
 * @date 2012-08-08
 * @return {Number} altdashlength
 */
links.Network.Link.prototype.getAltDashLength = function() {
    return this.altdashlength;
};

/**
 * Adjust the length of the links alternate dashes.
 * @author David Jordan
 * @date 2012-08-08
 * @param {Number} altdashlength
 */
links.Network.Link.prototype.setAltDashLength = function(altdashlength) {
    this.altdashlength = altdashlength;
};



/**
 * Redraw a link
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype.draw = function(ctx) {
    throw "Method draw not initialized in link";
};


/**
 * Check if this object is overlapping with the provided object
 * @param {Object} obj   an object with parameters left, top
 * @return {boolean}     True if location is located on the link
 */
links.Network.Link.prototype.isOverlappingWith = function(obj) {
    var distMax = 10;

    var xFrom = this.from.x;
    var yFrom = this.from.y;
    var xTo = this.to.x;
    var yTo = this.to.y;
    var xObj = obj.left;
    var yObj = obj.top;


    var dist = links.Network._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);

    return (dist < distMax);
};

/**
 * Calculate the distance between a point (x3,y3) and a line segment from
 * (x1,y1) to (x2,y2).
 * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
 * @param {number} x1
 * @param {number} y1
 * @param {number} x2
 * @param {number} y2
 * @param {number} x3
 * @param {number} y3
 */
links.Network._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
    var px = x2-x1,
        py = y2-y1,
        something = px*px + py*py,
        u =  ((x3 - x1) * px + (y3 - y1) * py) / something;

    if (u > 1) {
        u = 1;
    }
    else if (u < 0) {
        u = 0;
    }

    var x = x1 + u * px,
        y = y1 + u * py,
        dx = x - x3,
        dy = y - y3;

    //# Note: If the actual distance does not matter,
    //# if you only want to compare what this function
    //# returns to other results of this function, you
    //# can just return the squared distance instead
    //# (i.e. remove the sqrt) to gain a little performance

    return Math.sqrt(dx*dx + dy*dy);
};

/**
 * Redraw a link as a line
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawLine = function(ctx) {
    // set style
    ctx.strokeStyle = this.color;
    ctx.lineWidth = this._getLineWidth();

    var point;
    if (this.from != this.to) {
        // draw line
        this._line(ctx);

        // draw text
        if (this.text) {
            point = this._pointOnLine(0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }
    else {
        var radius = this.length / 2 / Math.PI;
        var x, y;
        var node = this.from;
        if (!node.width) {
            node.resize(ctx);
        }
        if (node.width > node.height) {
            x = node.x + node.width / 2;
            y = node.y - radius;
        }
        else {
            x = node.x + radius;
            y = node.y - node.height / 2;
        }
        this._circle(ctx, x, y, radius);
        point = this._pointOnCircle(x, y, radius, 0.5);
        this._text(ctx, this.text, point.x, point.y);
    }
};

/**
 * Get the line width of the link. Depends on width and whether one of the
 * connected nodes is selected.
 * @return {Number} width
 * @private
 */
links.Network.Link.prototype._getLineWidth = function() {
    if (this.from.selected || this.to.selected) {
        return Math.min(this.width * 2, this.widthMax);
    }
    else {
        return this.width;
    }
};

/**
 * Draw a line between two nodes
 * @param {CanvasRenderingContext2D} ctx
 * @private
 */
links.Network.Link.prototype._line = function (ctx) {
    // draw a straight line
    ctx.beginPath();
    ctx.moveTo(this.from.x, this.from.y);
    ctx.lineTo(this.to.x, this.to.y);
    ctx.stroke();
};

/**
 * Draw a line from a node to itself, a circle
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x
 * @param {Number} y
 * @param {Number} radius
 * @private
 */
links.Network.Link.prototype._circle = function (ctx, x, y, radius) {
    // draw a circle
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    ctx.stroke();
};

/**
 * Draw text with white background and with the middle at (x, y)
 * @param {CanvasRenderingContext2D} ctx
 * @param {String} text
 * @param {Number} x
 * @param {Number} y
 */
links.Network.Link.prototype._text = function (ctx, text, x, y) {
    if (text) {
        // TODO: cache the calculated size
        ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
            this.fontSize + "px " + this.fontFace;
        ctx.fillStyle = 'white';
        var width = ctx.measureText(this.text).width;
        var height = this.fontSize;
        var left = x - width / 2;
        var top = y - height / 2;

        ctx.fillRect(left, top, width, height);

        // draw text
        ctx.fillStyle = this.fontColor || "black";
        ctx.textAlign = "left";
        ctx.textBaseline = "top";
        ctx.fillText(this.text, left, top);
    }
};

/**
 * Sets up the dashedLine functionality for drawing
 * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
 * @author David Jordan
 * @date 2012-08-08
 */
var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
if (CP && CP.lineTo){
  CP.dashedLine = function(x,y,x2,y2,dashArray){
    if (!dashArray) dashArray=[10,5];
    if (dashLength==0) dashLength = 0.001; // Hack for Safari
    var dashCount = dashArray.length;
    this.moveTo(x, y);
    var dx = (x2-x), dy = (y2-y);
    var slope = dy/dx;
    var distRemaining = Math.sqrt( dx*dx + dy*dy );
    var dashIndex=0, draw=true;
    while (distRemaining>=0.1){
      var dashLength = dashArray[dashIndex++%dashCount];
      if (dashLength > distRemaining) dashLength = distRemaining;
      var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
      if (dx<0) xStep = -xStep;
      x += xStep
      y += slope*xStep;
      this[draw ? 'lineTo' : 'moveTo'](x,y);
      distRemaining -= dashLength;
      draw = !draw;
    }
  }
}

/**
 * Redraw a link as a dashed line
 * Draw this link in the given canvas
 * @author David Jordan
 * @date 2012-08-08
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawDashLine = function(ctx) {
    // set style
    ctx.strokeStyle = this.color;
    ctx.lineWidth = this._getLineWidth();

    // draw dashed line
    ctx.beginPath();
    ctx.lineCap = 'round';
	if (this.altdashlength != undefined) //If an alt dash value has been set add to the array this value
    {
    	ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]);
    }
    else if (this.dashlength != undefined && this.dashgap != undefined) //If a dash and gap value has been set add to the array this value
    {
    	ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]);
    }
    else //If all else fails draw a line
	{
    	ctx.moveTo(this.from.x, this.from.y);
        ctx.lineTo(this.to.x, this.to.y);
	}
    ctx.stroke();

    // draw text
    if (this.text) {
        var point = this._pointOnLine(0.5);
        this._text(ctx, this.text, point.x, point.y);
    }
};

/**
 * Get a point on a line
 * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
 * @return {Object} point
 * @private
 */
links.Network.Link.prototype._pointOnLine = function (percentage) {
    return {
        x: (1 - percentage) * this.from.x + percentage * this.to.x,
        y: (1 - percentage) * this.from.y + percentage * this.to.y
    }
};

/**
 * Get a point on a circle
 * @param {Number} x
 * @param {Number} y
 * @param {Number} radius
 * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
 * @return {Object} point
 * @private
 */
links.Network.Link.prototype._pointOnCircle = function (x, y, radius, percentage) {
    var angle = (percentage - 3/8) * 2 * Math.PI;
    return {
        x: x + radius * Math.cos(angle),
        y: y - radius * Math.sin(angle)
    }
};

/**
 * Redraw a link as a line with a moving arrow
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawMovingArrows = function(ctx) {
    this._drawArrow(ctx);

    for (var a in this.arrows) {
        if (this.arrows.hasOwnProperty(a)) {
            this.arrows[a] += 0.02;  // TODO determine speed from interval
            if (this.arrows[a] > 1.0) this.arrows[a] = 0.0;
        }
    }
};

/**
 * Redraw a link as a line with a moving dot
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawMovingDot = function(ctx) {
    // set style
    ctx.strokeStyle = this.color;
    ctx.fillStyle = this.color;
    ctx.lineWidth = this._getLineWidth();

    // draw line
    var point;
    if (this.from != this.to) {
        this._line(ctx);

        // draw dot
        var radius = 4 + this.width * 2;
        point = this._pointOnLine(this.dot);
        ctx.circle(point.x, point.y, radius);
        ctx.fill();

        // move the dot to the next position
        this.dot += 0.05;  // TODO determine speed from interval
        if (this.dot > 1.0) this.dot = 0.0;

        // draw text
        if (this.text) {
            point = this._pointOnLine(0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }
    else {
        // TODO: moving dot for a circular edge
    }
};


/**
 * Redraw a link as a line with an arrow
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawArrow = function(ctx) {
    var point;
    // set style
    ctx.strokeStyle = this.color;
    ctx.fillStyle = this.color;
    ctx.lineWidth = this._getLineWidth();

    if (this.from != this.to) {
        // draw line
        this._line(ctx);

        // draw all arrows
        var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
        var length = 10 + 5 * this.width; // TODO: make customizable?
        for (var a in this.arrows) {
            if (this.arrows.hasOwnProperty(a)) {
                point = this._pointOnLine(this.arrows[a]);
                ctx.arrow(point.x, point.y, angle, length);
                ctx.fill();
                ctx.stroke();
            }
        }

        // draw text
        if (this.text) {
            point = this._pointOnLine(0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }
    else {
        // draw circle
        var radius = this.length / 2 / Math.PI;
        var x, y;
        var node = this.from;
        if (!node.width) {
            node.resize(ctx);
        }
        if (node.width > node.height) {
            x = node.x + node.width / 2;
            y = node.y - radius;
        }
        else {
            x = node.x + radius;
            y = node.y - node.height / 2;
        }
        this._circle(ctx, x, y, radius);

        // draw all arrows
        var angle = 0.2 * Math.PI;
        var length = 10 + 5 * this.width; // TODO: make customizable?
        for (var a in this.arrows) {
            if (this.arrows.hasOwnProperty(a)) {
                point = this._pointOnCircle(x, y, radius, this.arrows[a]);
                ctx.arrow(point.x, point.y, angle, length);
                ctx.fill();
                ctx.stroke();
            }
        }

        // draw text
        if (this.text) {
            point = this._pointOnCircle(x, y, radius, 0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }
};



/**
 * Redraw a link as a line with an arrow
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Link.prototype._drawArrowEnd = function(ctx) {
    // set style
    ctx.strokeStyle = this.color;
    ctx.fillStyle = this.color;
    ctx.lineWidth = this._getLineWidth();

    // draw line
    var angle, length;
    if (this.from != this.to) {
        // calculate length and angle of the line
        angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
        var dx = (this.to.x - this.from.x);
        var dy = (this.to.y - this.from.y);
        var lLink = Math.sqrt(dx * dx + dy * dy);

        var lFrom = this.to.distanceToBorder(ctx, angle + Math.PI);
        var pFrom = (lLink - lFrom) / lLink;
        var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
        var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;

        var lTo = this.to.distanceToBorder(ctx, angle);
        var pTo = (lLink - lTo) / lLink;
        var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
        var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;

        ctx.beginPath();
        ctx.moveTo(xFrom, yFrom);
        ctx.lineTo(xTo, yTo);
        ctx.stroke();

        // draw arrow at the end of the line
        length = 10 + 5 * this.width; // TODO: make customizable?
        ctx.arrow(xTo, yTo, angle, length);
        ctx.fill();
        ctx.stroke();

        // draw text
        if (this.text) {
            var point = this._pointOnLine(0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }
    else {
        // draw circle
        var radius = this.length / 2 / Math.PI;
        var x, y, arrow;
        var node = this.from;
        if (!node.width) {
            node.resize(ctx);
        }
        if (node.width > node.height) {
            x = node.x + node.width / 2;
            y = node.y - radius;
            arrow = {
                x: x,
                y: node.y,
                angle: 0.9 * Math.PI
            };
        }
        else {
            x = node.x + radius;
            y = node.y - node.height / 2;
            arrow = {
                x: node.x,
                y: y,
                angle: 0.6 * Math.PI
            };
        }
        ctx.beginPath();
        // TODO: do not draw a circle, but an arc
        // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
        ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
        ctx.stroke();

        // draw all arrows
        length = 10 + 5 * this.width; // TODO: make customizable?
        ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
        ctx.fill();
        ctx.stroke();

        // draw text
        if (this.text) {
            point = this._pointOnCircle(x, y, radius, 0.5);
            this._text(ctx, this.text, point.x, point.y);
        }
    }

};

/**--------------------------------------------------------------------------**/


/**
 * @class Images
 * This class loades images and keeps them stored.
 */
links.Network.Images = function () {
    this.images = {};

    this.callback = undefined;
};

/**
 * Set an onload callback function. This will be called each time an image
 * is loaded
 * @param {function} callback
 */
links.Network.Images.prototype.setOnloadCallback = function(callback) {
    this.callback = callback;
};


/**
 *
 * @param {string} url          Url of the image
 * @return {Image} img          The image object
 */
links.Network.Images.prototype.load = function(url) {
    var img = this.images[url];
    if (img == undefined) {
        // create the image
        var images = this;
        img = new Image();
        this.images[url] = img;
        img.onload = function() {
            if (images.callback) {
                images.callback(this);
            }
        };
        img.src = url;
    }

    return img;
};


/**--------------------------------------------------------------------------**/


/**
 * @class Package
 * This class contains one package
 *
 * @param {number} properties  Properties for the package. Optional. Available
 *                             properties are: id {number}, title {string},
 *                             style {string} with available values "dot" and
 *                             "image", radius {number}, image {string},
 *                             color {string}, progress {number} with a value
 *                             between 0-1, duration {number}, timestamp {number
 *                             or Date}.
 * @param {links.Network}      network        The network object, used to find
 *                                            and link to nodes.
 * @param {links.Network.Images} imagelist    An Images object. Only needed
 *                                            when the package has style 'image'
 * @param {Object}               constants    An object with default values for
 *                                            example for the color
 */
links.Network.Package = function (properties, network, imagelist, constants) {
    if (network == undefined) {
        throw "No network provided";
    }

    // constants
    this.radiusMin = constants.packages.radiusMin;
    this.radiusMax = constants.packages.radiusMax;
    this.imagelist = imagelist;
    this.network = network;

    // initialize variables
    this.id =        undefined;
    this.from =      undefined;
    this.to =        undefined;
    this.title =     undefined;
    this.style =     constants.packages.style;
    this.radius =    constants.packages.radius;
    this.color =     constants.packages.color;
    this.image =     constants.packages.image;
    this.value =     undefined;
    this.progress =  0.0;
    this.timestamp = undefined;
    this.duration = constants.packages.duration;
    this.autoProgress = true;
    this.radiusFixed = false;

    // set properties
    this.setProperties(properties, constants);
};

links.Network.Package.DEFAULT_DURATION = 1.0; // seconds

/**
 * Set or overwrite properties for the package
 * @param {Object} properties an object with properties
 * @param {Object} constants  and object with default, global properties
 */
links.Network.Package.prototype.setProperties = function(properties, constants) {
    if (!properties) {
        return;
    }

    // note that the provided properties can also be null, when they come from the Google DataTable
    if (properties.from != undefined) {this.from = this.network._getNode(properties.from);}
    if (properties.to != undefined) {this.to = this.network._getNode(properties.to);}

    if (!this.from) {
        throw "Node with id " + properties.from + " not found";
    }
    if (!this.to) {
        throw "Node with id " + properties.to + " not found";
    }

    if (properties.id != undefined) {this.id = properties.id;}
    if (properties.title != undefined) {this.title = properties.title;}
    if (properties.style != undefined) {this.style = properties.style;}
    if (properties.radius != undefined) {this.radius = properties.radius;}
    if (properties.value != undefined) {this.value = properties.value;}
    if (properties.image != undefined) {this.image = properties.image;}
    if (properties.color != undefined) {this.color = properties.color;}
    if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
    if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
    if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
    if (properties.progress != undefined) {this.progress = properties.progress;}
    if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
    if (properties.duration != undefined) {this.duration = properties.duration;}

    this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
    this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false;

    if (this.style == 'image') {
        this.radiusMin = constants.packages.widthMin;
        this.radiusMax = constants.packages.widthMax;
    }

    // handle progress
    if (this.progress < 0.0) {this.progress = 0.0;}
    if (this.progress > 1.0) {this.progress = 1.0;}

    // handle image
    if (this.image != undefined) {
        if (this.imagelist) {
            this.imageObj = this.imagelist.load(this.image);
        }
        else {
            throw "No imagelist provided";
        }
    }

    // choose draw method depending on the style
    switch (this.style) {
        // TODO: add more styles
        case 'dot':         this.draw = this._drawDot; break;
        case 'square':      this.draw = this._drawSquare; break;
        case 'triangle':    this.draw = this._drawTriangle; break;
        case 'triangleDown':this.draw = this._drawTriangleDown; break;
        case 'star':        this.draw = this._drawStar; break;
        case 'image':       this.draw = this._drawImage; break;
        default:            this.draw = this._drawDot; break;
    }
};

/**
 * Set a new value for the progress of the package
 * @param {number} progress    A value between 0 and 1
 */
links.Network.Package.prototype.setProgress = function (progress) {
    this.progress = progress;
    this.autoProgress = false;
};

/**
 * Check if a package is finished, if it has reached its destination.
 * If so, the package can be removed.
 * Only packages with automatically animated progress can be finished
 * @return {boolean}    true if finished, else false.
 */
links.Network.Package.prototype.isFinished = function () {
    return (this.autoProgress == true && this.progress >= 1.0);
};

/**
 * Check if this package is moving.
 * A packages moves when it has automatic progress and not yet reached its
 * destination.
 * @return {boolean}    true if moving, else false.
 */
links.Network.Package.prototype.isMoving = function () {
    return (this.autoProgress || this.isFinished());
};


/**
 * Perform one discrete step for the package. Only applicable when the
 * package has no manually set, fixed progress.
 * @param {number} interval    Time interval in seconds
 */
links.Network.Package.prototype.discreteStep = function(interval) {
    if (this.autoProgress == true) {
        this.progress += (parseFloat(interval) / this.duration);

        if (this.progress > 1.0)
            this.progress = 1.0;
    }
};


/**
 * Draw this package in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Package.prototype.draw = function(ctx) {
    throw "Draw method not initialized for package";
};


/**
 * Check if this object is overlapping with the provided object
 * @param {Object} obj   an object with parameters left, top, right, bottom
 * @return {boolean}     True if location is located on node
 */
links.Network.Package.prototype.isOverlappingWith = function(obj) {
    // radius minimum 10px else it is too hard to get your mouse at the exact right position
    var radius = Math.max(this.radius, 10);
    var pos = this._getPosition();

    return (pos.x - radius < obj.right &&
        pos.x + radius > obj.left &&
        pos.y - radius < obj.bottom &&
        pos.y + radius > obj.top);
};

/**
 * Calculate the current position of the package
 * @return {Object} position    The object has parameters x and y.
 */
links.Network.Package.prototype._getPosition = function() {
    return {
        "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x,
        "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y
    };
};


/**
 * get the title of this package.
 * @return {string} title    The title of the package, or undefined when no
 *                           title has been set.
 */
links.Network.Package.prototype.getTitle = function() {
    return this.title;
};

/**
 * Retrieve the value of the package. Can be undefined
 * @return {Number} value
 */
links.Network.Package.prototype.getValue = function() {
    return this.value;
};

/**
 * Calculate the distance from the packages location to the given location (x,y)
 * @param {Number} x
 * @param {Number} y
 * @return {Number} value
 */
links.Network.Package.prototype.getDistance = function(x, y) {
    var pos = this._getPosition(),
        dx = pos.x - x,
        dy = pos.y - y;
    return Math.sqrt(dx * dx + dy * dy);
};

/**
 * Adjust the value range of the package. The package will adjust it's radius
 * based on its value.
 * @param {Number} min
 * @param {Number} max
 */
links.Network.Package.prototype.setValueRange = function(min, max) {
    if (!this.radiusFixed && this.value !== undefined) {
        var diff = (max - min);
        if (diff) {
            var factor = (this.radiusMax - this.radiusMin) / diff;
            this.radius = (this.value - min) * factor + this.radiusMin;
        }
        else {
            this.radius = this.radiusMin;
        }
    }
};



/**
 * Redraw a package as a dot
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
/* TODO: cleanup
 links.Network.Package.prototype._drawDot = function(ctx) {
 // set style
 ctx.fillStyle = this.color;
 // draw dot
 var pos = this._getPosition();
 ctx.circle(pos.x, pos.y, this.radius);
 ctx.fill();
 }
 */

links.Network.Package.prototype._drawDot = function (ctx) {
    this._drawShape(ctx, 'circle');
};

links.Network.Package.prototype._drawTriangle = function (ctx) {
    this._drawShape(ctx, 'triangle');
};

links.Network.Package.prototype._drawTriangleDown = function (ctx) {
    this._drawShape(ctx, 'triangleDown');
};

links.Network.Package.prototype._drawSquare = function (ctx) {
    this._drawShape(ctx, 'square');
};

links.Network.Package.prototype._drawStar = function (ctx) {
    this._drawShape(ctx, 'star');
};

links.Network.Package.prototype._drawShape = function (ctx, shape) {
    // set style
    ctx.fillStyle = this.color;

    // draw shape
    var pos = this._getPosition();
    ctx[shape](pos.x, pos.y, this.radius);
    ctx.fill();
};

/**
 * Redraw a package as an image
 * Draw this link in the given canvas
 * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
 * @param {CanvasRenderingContext2D}   ctx
 */
links.Network.Package.prototype._drawImage = function (ctx) {
    if (this.imageObj) {
        var width, height;
        if (this.value) {
            var scale = this.imageObj.height / this.imageObj.width;
            width = this.radius || this.imageObj.width;
            height = this.radius * scale || this.imageObj.height;
        }
        else {
            width = this.imageObj.width;
            height = this.imageObj.height;
        }
        var pos = this._getPosition();

        ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height);
    }
    else {
        console.log("image still loading...");
    }
};



/**--------------------------------------------------------------------------**/


/**
 * @class Groups
 * This class can store groups and properties specific for groups.
 */
links.Network.Groups = function () {
    this.clear();
    this.defaultIndex = 0;
};


/**
 * default constants for group colors
 */
links.Network.Groups.DEFAULT = [
    {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue
    {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow
    {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red
    {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green
    {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta
    {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple
    {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange
    {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue
    {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink
    {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"}  // mint
];


/**
 * Clear all groups
 */
links.Network.Groups.prototype.clear = function () {
    this.groups = {};
    this.groups.length = function()
    {
        var i = 0;
        for ( var p in this ) {
            if (this.hasOwnProperty(p)) {
                i++;
            }
        }
        return i;
    }
};


/**
 * get group properties of a groupname. If groupname is not found, a new group
 * is added.
 * @param {*} groupname        Can be a number, string, Date, etc.
 * @return {Object} group      The created group, containing all group properties
 */
links.Network.Groups.prototype.get = function (groupname) {
    var group = this.groups[groupname];

    if (group == undefined) {
        // create new group
        var index = this.defaultIndex % links.Network.Groups.DEFAULT.length;
        this.defaultIndex++;
        group = {};
        group.borderColor     = links.Network.Groups.DEFAULT[index].borderColor;
        group.backgroundColor = links.Network.Groups.DEFAULT[index].backgroundColor;
        group.highlightColor  = links.Network.Groups.DEFAULT[index].highlightColor;
        this.groups[groupname] = group;
    }

    return group;
};

/**
 * Add a custom group style
 * @param {String} groupname
 * @param {Object} style       An object containing borderColor,
 *                             backgroundColor, etc.
 * @return {Object} group      The created group object
 */
links.Network.Groups.prototype.add = function (groupname, style) {
    this.groups[groupname] = style;
    return style;
};

/**
 * Check if given object is a Javascript Array
 * @param {*} obj
 * @return {Boolean} isArray    true if the given object is an array
 */
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.Network.isArray = function (obj) {
    if (obj instanceof Array) {
        return true;
    }
    return (Object.prototype.toString.call(obj) === '[object Array]');
};



/**--------------------------------------------------------------------------**/


/**
 * @class Slider
 *
 * An html slider control with start/stop/prev/next buttons
 * @param {Element} container  The element where the slider will be created
 */
links.Network.Slider = function(container) {
    if (container === undefined) throw "Error: No container element defined";

    this.container = container;

    this.frame = document.createElement("DIV");
    //this.frame.style.backgroundColor = "#E5E5E5";
    this.frame.style.width = "100%";
    this.frame.style.position = "relative";

    this.title = document.createElement("DIV");
    this.title.style.margin = "2px";
    this.title.style.marginBottom = "5px";
    this.title.innerHTML = "";
    this.container.appendChild(this.title);

    this.frame.prev = document.createElement("INPUT");
    this.frame.prev.type = "BUTTON";
    this.frame.prev.value = "Prev";
    this.frame.appendChild(this.frame.prev);

    this.frame.play = document.createElement("INPUT");
    this.frame.play.type = "BUTTON";
    this.frame.play.value = "Play";
    this.frame.appendChild(this.frame.play);

    this.frame.next = document.createElement("INPUT");
    this.frame.next.type = "BUTTON";
    this.frame.next.value = "Next";
    this.frame.appendChild(this.frame.next);

    this.frame.bar = document.createElement("INPUT");
    this.frame.bar.type = "BUTTON";
    this.frame.bar.style.position = "absolute";
    this.frame.bar.style.border = "1px solid red";
    this.frame.bar.style.width = "100px";
    this.frame.bar.style.height = "6px";
    this.frame.bar.style.borderRadius = "2px";
    this.frame.bar.style.MozBorderRadius = "2px";
    this.frame.bar.style.border = "1px solid #7F7F7F";
    this.frame.bar.style.backgroundColor = "#E5E5E5";
    this.frame.appendChild(this.frame.bar);

    this.frame.slide = document.createElement("INPUT");
    this.frame.slide.type = "BUTTON";
    this.frame.slide.style.margin = "0px";
    this.frame.slide.value = " ";
    this.frame.slide.style.position = "relative";
    this.frame.slide.style.left = "-100px";
    this.frame.appendChild(this.frame.slide);

    // create events
    var me = this;
    this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
    this.frame.prev.onclick = function (event) {me.prev(event);};
    this.frame.play.onclick = function (event) {me.togglePlay(event);};
    this.frame.next.onclick = function (event) {me.next(event);};

    this.container.appendChild(this.frame);

    this.onChangeCallback = undefined;

    this.playTimeout = undefined;
    this.framerate = 20; // frames per second
    this.duration = 10; // seconds
    this.doLoop = true;

    this.start = 0;
    this.end = 0;
    this.value = 0;
    this.step = 0;
    this.rangeIsDate = false;

    this.redraw();
};

/**
 * Retrieve the step size, depending on the range, framerate, and duration
 */
links.Network.Slider.prototype._updateStep = function() {
    var range = (this.end - this.start);
    var frameCount = this.duration * this.framerate;

    this.step = range / frameCount;
};

/**
 * Select the previous index
 */
links.Network.Slider.prototype.prev = function() {
    this._setValue(this.value - this.step);
};

/**
 * Select the next index
 */
links.Network.Slider.prototype.next = function() {
    this._setValue(this.value + this.step);
};

/**
 * Select the next index
 */
links.Network.Slider.prototype.playNext = function() {
    var start = new Date();

    if (!this.leftButtonDown) {
        if (this.value + this.step < this.end) {
            this._setValue(this.value + this.step);
        }
        else {
            if (this.doLoop) {
                this._setValue(this.start);
            }
            else {
                this._setValue(this.end);
                this.stop();
                return;
            }
        }
    }

    var end = new Date();
    var diff = (end - start);

    // calculate how much time it to to set the index and to execute the callback
    // function.
    var interval = Math.max(1000 / this.framerate - diff, 0);

    var me = this;
    this.playTimeout = setTimeout(function() {me.playNext();}, interval);
};

/**
 * Toggle start or stop playing
 */
links.Network.Slider.prototype.togglePlay = function() {
    if (this.playTimeout === undefined) {
        this.play();
    } else {
        this.stop();
    }
};

/**
 * Start playing
 */
links.Network.Slider.prototype.play = function() {
    this.frame.play.value = "Stop";

    this.playNext();
};

/**
 * Stop playing
 */
links.Network.Slider.prototype.stop = function() {
    this.frame.play.value = "Play";

    clearInterval(this.playTimeout);
    this.playTimeout = undefined;
};

/**
 * Set a callback function which will be triggered when the value of the
 * slider bar has changed.
 */
links.Network.Slider.prototype.setOnChangeCallback = function(callback) {
    this.onChangeCallback = callback;
};

/**
 * Set the interval for playing the list
 * @param {number} framerate    Framerate in frames per second
 */
links.Network.Slider.prototype.setFramerate = function(framerate) {
    this.framerate = framerate;
    this._updateStep();
};

/**
 * Retrieve the current framerate
 * @return {number} framerate in frames per second
 */
links.Network.Slider.prototype.getFramerate = function() {
    return this.framerate;
};

/**
 * Set the duration for playing
 * @param {number} duration    Duration in seconds
 */
links.Network.Slider.prototype.setDuration = function(duration) {
    this.duration = duration;
    this._updateStep();
};

/**
 * Set the time acceleration for playing the history. Only applicable when
 * the values are of type Date.
 * @param {number} acceleration    Acceleration, for example 10 means play
 *                                 ten times as fast as real time. A value
 *                                 of 1 will play the history in real time.
 */
links.Network.Slider.prototype.setAcceleration = function(acceleration) {
    var durationRealtime = (this.end - this.start) / 1000; // in seconds

    this.duration = durationRealtime / acceleration;
    this._updateStep();
};


/**
 * Set looping on or off
 * @param {boolean} doLoop    If true, the slider will jump to the start when
 *                            the end is passed, and will jump to the end
 *                            when the start is passed.
 */
links.Network.Slider.prototype.setLoop = function(doLoop) {
    this.doLoop = doLoop;
};

/**
 * Retrieve the current value of loop
 * @return {boolean} doLoop    If true, the slider will jump to the start when
 *                             the end is passed, and will jump to the end
 *                             when the start is passed.
 */
links.Network.Slider.prototype.getLoop = function() {
    return this.doLoop;
};


/**
 * Execute the onchange callback function
 */
links.Network.Slider.prototype.onChange = function() {
    if (this.onChangeCallback !== undefined) {
        this.onChangeCallback();
    }
};

/**
 * redraw the slider on the correct place
 */
links.Network.Slider.prototype.redraw = function() {
    // resize the bar
    var barTop = (this.frame.clientHeight/2 -
        this.frame.bar.offsetHeight/2);
    var barWidth = (this.frame.clientWidth -
        this.frame.prev.clientWidth -
        this.frame.play.clientWidth -
        this.frame.next.clientWidth - 30);
    this.frame.bar.style.top = barTop + "px";
    this.frame.bar.style.width = barWidth + "px";

    // position the slider button
    this.frame.slide.title = this.getValue();
    this.frame.slide.style.left = this._valueToLeft(this.value) + "px";

    // set the title
    this.title.innerHTML = this.getValue();
};


/**
 * Set the range for the slider
 * @param {Date | Number} start  Start of the range
 * @param {Date | Number} end    End of the range
 */
links.Network.Slider.prototype.setRange = function(start, end) {
    if (start === undefined || start === null || start === NaN) {
        this.start = 0;
        this.rangeIsDate = false;
    }
    else if (start instanceof Date) {
        this.start = start.getTime();
        this.rangeIsDate = true;
    }
    else {
        this.start = start;
        this.rangeIsDate = false;
    }

    if (end === undefined || end === null || end === NaN) {
        if (this.start instanceof Date) {
            this.end = new Date(this.start);
        }
        else {
            this.end = this.start;
        }
    }
    else if (end instanceof Date) {
        this.end = end.getTime();
    }
    else {
        this.end = end;
    }

    this.value = this.start;

    this._updateStep();
    this.redraw();
};



/**
 * Set a value for the slider. The value must be between start and end
 * When the range are Dates, the value will be translated to a date
 * @param {Number} value
 */
links.Network.Slider.prototype._setValue = function(value) {
    this.value = this._limitValue(value);
    this.redraw();

    this.onChange();
};

/**
 * retrieve the current value in the correct type, Number or Date
 * @return {Date | Number} value
 */
links.Network.Slider.prototype.getValue = function() {
    if (this.rangeIsDate) {
        return new Date(this.value);
    }
    else {
        return this.value;
    }
};


links.Network.Slider.prototype.offset = 3;

links.Network.Slider.prototype._leftToValue = function (left) {
    var width = parseFloat(this.frame.bar.style.width) -
        this.frame.slide.clientWidth - 10;
    var x = left - this.offset;

    var range = this.end - this.start;
    var value = this._limitValue(x / width * range + this.start);

    return value;
};

links.Network.Slider.prototype._valueToLeft = function (value) {
    var width = parseFloat(this.frame.bar.style.width) -
        this.frame.slide.clientWidth - 10;

    var x;
    if (this.end > this.start) {
        x = (value - this.start) / (this.end - this.start) * width;
    }
    else {
        x = 0;
    }
    var left = x + this.offset;

    return left;
};

links.Network.Slider.prototype._limitValue = function(value) {
    if (value < this.start) {
        value = this.start
    }
    if (value > this.end) {
        value = this.end;
    }

    return value;
};

links.Network.Slider.prototype._onMouseDown = function(event) {
    // only react on left mouse button down
    this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
    if (!this.leftButtonDown) return;

    this.startClientX = event.clientX;
    this.startSlideX = parseFloat(this.frame.slide.style.left);

    this.frame.style.cursor = 'move';

    // add event listeners to handle moving the contents
    // we store the function onmousemove and onmouseup in the graph, so we can
    // remove the eventlisteners lateron in the function mouseUp()
    var me = this;
    this.onmousemove = function (event) {me._onMouseMove(event);};
    this.onmouseup   = function (event) {me._onMouseUp(event);};
    links.Network.addEventListener(document, "mousemove", this.onmousemove);
    links.Network.addEventListener(document, "mouseup",   this.onmouseup);
    links.Network.preventDefault(event);
};


links.Network.Slider.prototype._onMouseMove = function (event) {
    var diff = event.clientX - this.startClientX;
    var x = this.startSlideX + diff;

    var value = this._leftToValue(x);
    this._setValue(value);

    links.Network.preventDefault(event);
};


links.Network.Slider.prototype._onMouseUp = function (event) {
    this.frame.style.cursor = 'auto';

    this.leftButtonDown = false;

    // remove event listeners
    links.Network.removeEventListener(document, "mousemove", this.onmousemove);
    links.Network.removeEventListener(document, "mouseup", this.onmouseup);

    links.Network.preventDefault(event);
};



/**--------------------------------------------------------------------------**/


/**
 * Popup is a class to create a popup window with some text
 * @param {Element}  container     The container object.
 * @param {Number} x
 * @param {Number} y
 * @param {String} text
 */
links.Network.Popup = function (container, x, y, text) {
    if (container) {
        this.container = container;
    }
    else {
        this.container = document.body;
    }
    this.x = 0;
    this.y = 0;
    this.padding = 5;

    if (x !== undefined && y !== undefined ) {
        this.setPosition(x, y);
    }
    if (text !== undefined) {
        this.setText(text);
    }

    // create the frame
    this.frame = document.createElement("div");
    var style = this.frame.style;
    style.position = "absolute";
    style.visibility = "hidden";
    style.border = "1px solid #666";
    style.color = "black";
    style.padding = this.padding + "px";
    style.backgroundColor = "#FFFFC6";
    style.borderRadius = "3px";
    style.MozBorderRadius = "3px";
    style.WebkitBorderRadius = "3px";
    style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
    style.whiteSpace = "nowrap";
    this.container.appendChild(this.frame);
};

/**
 * @param {number} x   Horizontal position of the popup window
 * @param {number} y   Vertical position of the popup window
 */
links.Network.Popup.prototype.setPosition = function(x, y) {
    this.x = parseInt(x);
    this.y = parseInt(y);
};

/**
 * Set the text for the popup window. This can be HTML code
 * @param {string} text
 */
links.Network.Popup.prototype.setText = function(text) {
    this.frame.innerHTML = text;
};

/**
 * Show the popup window
 * @param {boolean} show    Optional. Show or hide the window
 */
links.Network.Popup.prototype.show = function (show) {
    if (show === undefined) {
        show = true;
    }

    if (show) {
        var height = this.frame.clientHeight;
        var width =  this.frame.clientWidth;
        var maxHeight = this.frame.parentNode.clientHeight;
        var maxWidth = this.frame.parentNode.clientWidth;

        var top = (this.y - height);
        if (top + height + this.padding > maxHeight) {
            top = maxHeight - height - this.padding;
        }
        if (top < this.padding) {
            top = this.padding;
        }

        var left = this.x;
        if (left + width + this.padding > maxWidth) {
            left = maxWidth - width - this.padding;
        }
        if (left < this.padding) {
            left = this.padding;
        }

        this.frame.style.left = left + "px";
        this.frame.style.top = top + "px";
        this.frame.style.visibility = "visible";
    }
    else {
        this.hide();
    }
};

/**
 * Hide the popup window
 */
links.Network.Popup.prototype.hide = function () {
    this.frame.style.visibility = "hidden";
};



/** ------------------------------------------------------------------------ **/


/**
 * Event listener (singleton)
 */
links.events = links.events || {
    'listeners': [],

    /**
     * Find a single listener by its object
     * @param {Object} object
     * @return {Number} index  -1 when not found
     */
    'indexOf': function (object) {
        var listeners = this.listeners;
        for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
            var listener = listeners[i];
            if (listener && listener.object == object) {
                return i;
            }
        }
        return -1;
    },

    /**
     * Add an event listener
     * @param {Object} object
     * @param {String} event       The name of an event, for example 'select'
     * @param {function} callback  The callback method, called when the
     *                             event takes place
     */
    'addListener': function (object, event, callback) {
        var index = this.indexOf(object);
        var listener = this.listeners[index];
        if (!listener) {
            listener = {
                'object': object,
                'events': {}
            };
            this.listeners.push(listener);
        }

        var callbacks = listener.events[event];
        if (!callbacks) {
            callbacks = [];
            listener.events[event] = callbacks;
        }

        // add the callback if it does not yet exist
        if (callbacks.indexOf(callback) == -1) {
            callbacks.push(callback);
        }
    },

    /**
     * Remove an event listener
     * @param {Object} object
     * @param {String} event       The name of an event, for example 'select'
     * @param {function} callback  The registered callback method
     */
    'removeListener': function (object, event, callback) {
        var index = this.indexOf(object);
        var listener = this.listeners[index];
        if (listener) {
            var callbacks = listener.events[event];
            if (callbacks) {
                var callbackIndex = callbacks.indexOf(callback);
                if (callbackIndex != -1) {
                    callbacks.splice(callbackIndex, 1);
                }

                // remove the array when empty
                if (callbacks.length == 0) {
                    delete listener.events[event];
                }
            }

            // count the number of registered events. remove listener when empty
            var count = 0;
            var events = listener.events;
            for (var ev in events) {
                if (events.hasOwnProperty(ev)) {
                    count++;
                }
            }
            if (count == 0) {
                delete this.listeners[index];
            }
        }
    },

    /**
     * Remove all registered event listeners
     */
    'removeAllListeners': function () {
        this.listeners = [];
    },

    /**
     * Trigger an event. All registered event handlers will be called
     * @param {Object} object
     * @param {String} event
     * @param {Object} properties (optional)
     */
    'trigger': function (object, event, properties) {
        var index = this.indexOf(object);
        var listener = this.listeners[index];
        if (listener) {
            var callbacks = listener.events[event];
            if (callbacks) {
                for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
                    callbacks[i](properties);
                }
            }
        }
    }
};


/**--------------------------------------------------------------------------**/


/**
 * Draw a circle shape
 */
CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
    this.beginPath();
    this.arc(x, y, r, 0, 2*Math.PI, false);
};

/**
 * Draw a square shape
 * @param {Number} x horizontal center
 * @param {Number} y vertical center
 * @param {Number} r   size, width and height of the square
 */
CanvasRenderingContext2D.prototype.square = function(x, y, r) {
    this.beginPath();
    this.rect(x - r, y - r, r * 2, r * 2);
};

/**
 * Draw a triangle shape
 * @param {Number} x horizontal center
 * @param {Number} y vertical center
 * @param {Number} r   radius, half the length of the sides of the triangle
 */
CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
    // http://en.wikipedia.org/wiki/Equilateral_triangle
    this.beginPath();

    var s = r * 2;
    var s2 = s / 2;
    var ir = Math.sqrt(3) / 6 * s;      // radius of inner circle
    var h = Math.sqrt(s * s - s2 * s2); // height

    this.moveTo(x, y - (h - ir));
    this.lineTo(x + s2, y + ir);
    this.lineTo(x - s2, y + ir);
    this.lineTo(x, y - (h - ir));
    this.closePath();
};

/**
 * Draw a triangle shape in downward orientation
 * @param {Number} x horizontal center
 * @param {Number} y vertical center
 * @param {Number} r radius
 */
CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
    // http://en.wikipedia.org/wiki/Equilateral_triangle
    this.beginPath();

    var s = r * 2;
    var s2 = s / 2;
    var ir = Math.sqrt(3) / 6 * s;      // radius of inner circle
    var h = Math.sqrt(s * s - s2 * s2); // height

    this.moveTo(x, y + (h - ir));
    this.lineTo(x + s2, y - ir);
    this.lineTo(x - s2, y - ir);
    this.lineTo(x, y + (h - ir));
    this.closePath();
};

/**
 * Draw a star shape, a star with 5 points
 * @param {Number} x horizontal center
 * @param {Number} y vertical center
 * @param {Number} r   radius, half the length of the sides of the triangle
 */
CanvasRenderingContext2D.prototype.star = function(x, y, r) {
    // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
    this.beginPath();

    for (var n = 0; n < 10; n++) {
        var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
        this.lineTo(
            x + radius * Math.sin(n * 2 * Math.PI / 10),
            y - radius * Math.cos(n * 2 * Math.PI / 10)
        );
    }

    this.closePath();
};

/**
 * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
 */
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
    var r2d = Math.PI/180;
    if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
    if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
    this.beginPath();
    this.moveTo(x+r,y);
    this.lineTo(x+w-r,y);
    this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
    this.lineTo(x+w,y+h-r);
    this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
    this.lineTo(x+r,y+h);
    this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
    this.lineTo(x,y+r);
    this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
};

/**
 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
 */
CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
    var kappa = .5522848,
        ox = (w / 2) * kappa, // control point offset horizontal
        oy = (h / 2) * kappa, // control point offset vertical
        xe = x + w,           // x-end
        ye = y + h,           // y-end
        xm = x + w / 2,       // x-middle
        ym = y + h / 2;       // y-middle

    this.beginPath();
    this.moveTo(x, ym);
    this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
    this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
};



/**
 * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
 */
CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
    var f = 1/3;
    var wEllipse = w;
    var hEllipse = h * f;

    var kappa = .5522848,
        ox = (wEllipse / 2) * kappa, // control point offset horizontal
        oy = (hEllipse / 2) * kappa, // control point offset vertical
        xe = x + wEllipse,           // x-end
        ye = y + hEllipse,           // y-end
        xm = x + wEllipse / 2,       // x-middle
        ym = y + hEllipse / 2,       // y-middle
        ymb = y + (h - hEllipse/2),  // y-midlle, bottom ellipse 
        yeb = y + h;                 // y-end, bottom ellipse

    this.beginPath();
    this.moveTo(xe, ym);

    this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);

    this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
    this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);

    this.lineTo(xe, ymb);

    this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
    this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);

    this.lineTo(x, ym);
};


/**
 * Draw an arrow point (no line)
 */
CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
    // tail
    var xt = x - length * Math.cos(angle);
    var yt = y - length * Math.sin(angle);

    // inner tail
    // TODO: allow to customize different shapes
    var xi = x - length * 0.9 * Math.cos(angle);
    var yi = y - length * 0.9 * Math.sin(angle);

    // left
    var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
    var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);

    // right
    var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
    var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);

    this.beginPath();
    this.moveTo(x, y);
    this.lineTo(xl, yl);
    this.lineTo(xi, yi);
    this.lineTo(xr, yr);
    this.closePath();
};


// TODO: add diamond shape


/*----------------------------------------------------------------------------*/

// utility methods
links.Network.util = {};

/**
 * Parse a text source containing data in DOT language into a JSON object.
 * The object contains two lists: one with nodes and one with edges.
 * @param {String} data     Text containing a graph in DOT-notation
 * @return {Object} json    An object containing two parameters:
 *                          {Object[]} nodes
 *                          {Object[]} edges
 */
links.Network.util.parseDOT = function (data) {
    /**
     * Test whether given character is a whitespace character
     * @param {String} c
     * @return {Boolean} isWhitespace
     */
    function isWhitespace(c) {
        return c == ' ' || c == '\t' || c == '\n' || c == '\r';
    }

    /**
     * Test whether given character is a delimeter
     * @param {String} c
     * @return {Boolean} isDelimeter
     */
    function isDelimeter(c) {
        return '[]{}();,=->'.indexOf(c) != -1;
    }

    var i = -1;  // current index in the data
    var c = '';  // current character in the data

    /**
     * Read the next character from the data
     */
    function next() {
        i++;
        c = data[i];
    }

    /**
     * Preview the next character in the data
     * @returns {String} nextChar
     */
    function previewNext () {
        return data[i + 1];
    }

    /**
     * Preview the next character in the data
     * @returns {String} nextChar
     */
    function previewPrevious () {
        return data[i + 1];
    }

    /**
     * Get a text description of the the current index in the data
     * @return {String} desc
     */
    function pos() {
        return '(char ' + i + ')';
    }

    /**
     * Skip whitespace and comments
     */
    function parseWhitespace() {
        // skip whitespace
        while (c && isWhitespace(c)) {
            next();
        }

        // test for comment
        var cNext = data[i + 1];
        var cPrev = data[i - 1];
        var c2 = c + cNext;
        if (c2 == '/*') {
            // block comment. skip until the block is closed
            while (c && !(c == '*' && data[i + 1] == '/')) {
                next();
            }
            next();
            next();

            parseWhitespace();
        }
        else if (c2 == '//' || (c == '#' && cPrev == '\n')) {
            // line comment. skip until the next return
            while (c && c != '\n') {
                next();
            }
            next();
            parseWhitespace();
        }
    }

    /**
     * Parse a string
     * The string may be enclosed by double quotes
     * @return {String | undefined} value
     */
    function parseString() {
        parseWhitespace();

        var name = '';
        if (c == '"') {
            next();
            while (c && c != '"') {
                name += c;
                next();
            }
            next(); // skip the closing quote
        }
        else {
            while (c && !isWhitespace(c) && !isDelimeter(c)) {
                name += c;
                next();
            }

            // cast string to number or boolean
            var number = Number(name);
            if (!isNaN(number)) {
                name = number;
            }
            else if (name == 'true') {
                name = true;
            }
            else if (name == 'false') {
                name = false;
            }
            else if (name == 'null') {
                name = null;
            }
        }

        return name;
    }

    /**
     * Parse a value, can be a string, number, or boolean.
     * The value may be enclosed by double quotes
     * @return {String | Number | Boolean | undefined} value
     */
    function parseValue() {
        parseWhitespace();

        if (c == '"') {
            return parseString();
        }
        else {
            var value = parseString();
            if (value != undefined) {
                // cast string to number or boolean
                var number = Number(value);
                if (!isNaN(number)) {
                    value = number;
                }
                else if (value == 'true') {
                    value = true;
                }
                else if (value == 'false') {
                    value = false;
                }
                else if (value == 'null') {
                    value = null;
                }
            }
            return value;
        }
    }

    /**
     * Parse a set with attributes,
     * for example [label="1.000", style=solid]
     * @return {Object | undefined} attr
     */
    function parseAttributes() {
        parseWhitespace();

        if (c == '[') {
            next();
            var attr = {};
            while (c && c != ']') {
                parseWhitespace();

                var name = parseString();
                if (!name) {
                    throw new SyntaxError('Attribute name expected ' + pos());
                }

                parseWhitespace();
                if (c != '=') {
                    throw new SyntaxError('Equal sign = expected ' + pos());
                }
                next();

                var value = parseValue();
                if (!value) {
                    throw new SyntaxError('Attribute value expected ' + pos());
                }
                attr[name] = value;

                parseWhitespace();

                if (c ==',') {
                    next();
                }
            }
            next();

            return attr;
        }
        else {
            return undefined;
        }
    }

    /**
     * Parse a directed or undirected arrow '->' or '--'
     * @return {String | undefined} arrow
     */
    function parseArrow() {
        parseWhitespace();

        if (c == '-') {
            next();
            if (c == '>' || c == '-') {
                var arrow = '-' + c;
                next();
                return arrow;
            }
            else {
                throw new SyntaxError('Arrow "->" or "--" expected ' + pos());
            }
        }

        return undefined;
    }

    /**
     * Parse a line separator ';'
     * @return {String | undefined} separator
     */
    function parseSeparator() {
        parseWhitespace();

        if (c == ';') {
            next();
            return ';';
        }

        return undefined;
    }

    /**
     * Merge all properties of object b into object b
     * @param {Object} a
     * @param {Object} b
     */
    function merge (a, b) {
        if (a && b) {
            for (var name in b) {
                if (b.hasOwnProperty(name)) {
                    a[name] = b[name];
                }
            }
        }
    }

    var nodeMap = {};
    var edgeList = [];

    /**
     * Register a node with attributes
     * @param {String} id
     * @param {Object} [attr]
     */
    function addNode(id, attr) {
        var node = {
            id: String(id),
            attr: attr || {}
        };
        if (!nodeMap[id]) {
            nodeMap[id] = node;
        }
        else {
            merge(nodeMap[id].attr, node.attr);
        }
    }

    /**
     * Register an edge
     * @param {String} from
     * @param {String} to
     * @param {String} type    A string "->" or "--"
     * @param {Object} [attr]
     */
    function addEdge(from, to, type, attr) {
        edgeList.push({
            from: String(from),
            to: String(to),
            type: type,
            attr: attr || {}
        });
    }

    // find the opening curly bracket
    next();
    while (c && c != '{') {
        next();
    }
    if (c != '{') {
        throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos())
    }
    next();

    // parse all data until a closing curly bracket is encountered
    while (c && c != '}') {
        // parse node id and optional node attributes
        var id = parseString();
        if (id == undefined) {
            throw new SyntaxError('String with id expected ' + pos());
        }
        var attr = parseAttributes();
        addNode(id, attr);

        // TODO: parse global attributes "graph", "node", "edge"

        // parse arrow
        var type = parseArrow();
        while (type) {
            // parse node id
            var prevId = id;
            id = parseString();
            if (id == undefined) {
                throw new SyntaxError('String with id expected ' + pos());
            }
            addNode(id);

            // parse edge attributes and register edge
            attr = parseAttributes();
            addEdge(prevId, id, type, attr);

            // parse next arrow (optional)
            type = parseArrow();
        }

        // parse separator (optional)
        parseSeparator();

        parseWhitespace();
    }
    if (c != '}') {
        throw new SyntaxError('Invalid data. Curly bracket } expected');
    }

    // crop data between the curly brackets
    var start = data.indexOf('{');
    var end = data.indexOf('}', start);
    var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined;

    if (!text) {
        throw new Error('Invalid data. no curly brackets containing data found');
    }

    // return the results
    var nodeList = [];
    for (id in nodeMap) {
        if (nodeMap.hasOwnProperty(id)) {
            nodeList.push(nodeMap[id]);
        }
    }
    return {
        nodes: nodeList,
        edges: edgeList
    }
};

/**
 * Convert a string containing a graph in DOT language into a map containing
 * with nodes and edges in the format of Network.
 * @param {String} data         Text containing a graph in DOT-notation
 * @return {Object} networkData
 */
links.Network.util.DOTToNetwork = function (data) {
    // parse the DOT file
    var dotData = links.Network.util.parseDOT(data);
    var networkData = {
        nodes: [],
        edges: [],
        options: {
            nodes: {},
            links: {}
        }
    };

    /**
     * Merge the properties of object b into object a, and adjust properties
     * not supported by Network (for example replace "shape" with "style"
     * @param {Object} a
     * @param {Object} b
     * @param {Array} [ignore]   Optional array with property names to be ignored
     */
    function merge (a, b, ignore) {
        for (var prop in b) {
            if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) {
                a[prop] = b[prop];
            }
        }

        // Convert aliases to configuration settings supported by Network
        if (a.label) {
            a.text = a.label;
            delete a.label;
        }
        if (a.shape) {
            a.style = a.shape;
            delete a.shape;
        }
    }

    dotData.nodes.forEach(function (node) {
        if (node.id.toLowerCase() == 'graph') {
            merge(networkData.options, node.attr);
        }
        else if (node.id.toLowerCase() == 'node') {
            merge(networkData.options.nodes, node.attr);
        }
        else if (node.id.toLowerCase() == 'edge') {
            merge(networkData.options.links, node.attr);
        }
        else {
            var networkNode = {};
            networkNode.id = node.id;
            networkNode.text = node.id;
            merge(networkNode, node.attr);
            networkData.nodes.push(networkNode);
        }
    });

    dotData.edges.forEach(function (edge) {
        var networkEdge = {};
        networkEdge.from = edge.from;
        networkEdge.to = edge.to;
        networkEdge.text = edge.id;
        networkEdge.style = (edge.type == '->') ? 'arrow-end' : 'line';
        merge(networkEdge, edge.attr);
        networkData.edges.push(networkEdge);
    });

    return networkData;
};