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