1 /** 2 * @file timeline.js 3 * 4 * @brief 5 * The Timeline is an interactive visualization chart to visualize events in 6 * time, having a start and end date. 7 * You can freely move and zoom in the timeline by dragging 8 * and scrolling in the Timeline. Items are optionally dragable. The time 9 * scale on the axis is adjusted automatically, and supports scales ranging 10 * from milliseconds to years. 11 * 12 * Timeline is part of the CHAP Links library. 13 * 14 * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and 15 * Internet Explorer 6 to 9. 16 * 17 * @license 18 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 19 * use this file except in compliance with the License. You may obtain a copy 20 * of the License at 21 * 22 * http://www.apache.org/licenses/LICENSE-2.0 23 * 24 * Unless required by applicable law or agreed to in writing, software 25 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 26 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 27 * License for the specific language governing permissions and limitations under 28 * the License. 29 * 30 * Copyright (c) 2011 Almende B.V. 31 * 32 * @author Jos de Jong, <jos@almende.org> 33 * @date 2011-11-08 34 */ 35 36 37 /* 38 * TODO 39 * 40 * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible 41 */ 42 43 /** 44 * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, 45 * "links" 46 */ 47 var links; 48 if (links === undefined) { 49 links = {}; 50 } 51 52 /** 53 * Declare google variable, loaded and used in case of Google DataTable 54 */ 55 var google; 56 57 /** 58 * @class Timeline 59 * The timeline is a visualization chart to visualize events in time. 60 * 61 * The timeline is developed in javascript as a Google Visualization Chart. 62 * 63 * @param {dom_element} container The DOM element in which the Timeline will 64 * be created. Normally a div element. 65 */ 66 links.Timeline = function(container) { 67 // create variables and set default values 68 this.dom = {}; 69 this.conversion = {}; 70 this.eventParams = {}; // stores parameters for mouse events 71 this.groups = []; 72 this.groupIndexes = {}; 73 this.items = []; 74 this.selection = undefined; // stores index and item which is currently selected 75 76 this.listeners = {}; // event listener callbacks 77 78 // Initialize sizes. 79 // Needed for IE (which gives an error when you try to set an undefined 80 // value in a style) 81 this.size = { 82 'actualHeight': 0, 83 'axis': { 84 'characterMajorHeight': 0, 85 'characterMajorWidth': 0, 86 'characterMinorHeight': 0, 87 'characterMinorWidth': 0, 88 'height': 0, 89 'labelMajorTop': 0, 90 'labelMinorTop': 0, 91 'line': 0, 92 'lineMajorWidth': 0, 93 'lineMinorHeight': 0, 94 'lineMinorTop': 0, 95 'lineMinorWidth': 0, 96 'top': 0 97 }, 98 'contentHeight': 0, 99 'contentLeft': 0, 100 'contentWidth': 0, 101 'dataChanged': false, 102 'frameHeight': 0, 103 'frameWidth': 0, 104 'groupsLeft': 0, 105 'groupsWidth': 0, 106 'items': { 107 'top': 0 108 } 109 }; 110 111 this.dom.container = container; 112 113 this.options = { 114 'width': "100%", 115 'height': "auto", 116 'minHeight': 0, // minimal height in pixels 117 'autoHeight': true, 118 119 'eventMargin': 10, // minimal margin between events 120 'eventMarginAxis': 20, // minimal margin beteen events and the axis 121 'dragAreaWidth': 10, // pixels 122 123 'moveable': true, 124 'zoomable': true, 125 'selectable': true, 126 'editable': false, 127 'snapEvents': true, 128 129 'showCurrentTime': true, // show a red bar displaying the current time 130 'showCustomTime': false, // show a blue, draggable bar displaying a custom time 131 'showMajorLabels': true, 132 'showNavigation': false, 133 'showButtonAdd': true, 134 'groupsOnRight': false, 135 'axisOnTop': false, 136 'stackEvents': true, 137 'animate': true, 138 'animateZoom': true, 139 'style': 'box' 140 }; 141 142 this.clientTimeOffset = 0; // difference between client time and the time 143 // set via Timeline.setCurrentTime() 144 145 var dom = this.dom; 146 147 // remove all elements from the container element. 148 while (dom.container.hasChildNodes()) { 149 dom.container.removeChild(dom.container.firstChild); 150 } 151 152 // create a step for drawing the axis 153 this.step = new links.Timeline.StepDate(); 154 155 // initialize data 156 this.data = []; 157 158 // date interval must be initialized 159 this.setVisibleChartRange(undefined, undefined, false); 160 161 // create all DOM elements 162 this.redrawFrame(); 163 164 // Internet Explorer does not support Array.indexof, 165 // so we define it here in that case 166 // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ 167 if(!Array.prototype.indexOf) { 168 Array.prototype.indexOf = function(obj){ 169 for(var i = 0; i < this.length; i++){ 170 if(this[i] == obj){ 171 return i; 172 } 173 } 174 return -1; 175 } 176 } 177 178 // fire the ready event 179 this.trigger('ready'); 180 } 181 182 183 /** 184 * Main drawing logic. This is the function that needs to be called 185 * in the html page, to draw the timeline. 186 * 187 * A data table with the events must be provided, and an options table. 188 * 189 * @param {DataTable} data The data containing the events for the timeline. 190 * Object DataTable is defined in 191 * google.visualization.DataTable 192 * @param {name/value map} options A name/value map containing settings for the 193 * timeline. Optional. 194 */ 195 links.Timeline.prototype.draw = function(data, options) { 196 if (options) { 197 // retrieve parameter values 198 for (var i in options) { 199 if (options.hasOwnProperty(i)) { 200 this.options[i] = options[i]; 201 } 202 } 203 } 204 this.options.autoHeight = (this.options.height === "auto"); 205 206 // read the data 207 this.setData(data); 208 209 // set timer range. this will also redraw the timeline 210 if (options && options.start && options.end) { 211 this.setVisibleChartRange(options.start, options.end); 212 } 213 else { 214 this.setVisibleChartRangeAuto(); 215 } 216 } 217 218 /** 219 * Set data for the timeline 220 * @param {DataTable or JSON array} data 221 */ 222 links.Timeline.prototype.setData = function(data) { 223 // unselect any previously selected item 224 this.unselectItem(); 225 226 this.items = []; 227 this.data = data; 228 var items = this.items; 229 var options = this.options; 230 231 // create groups from the data 232 this.setGroups(data); 233 234 if (google && google.visualization && google.visualization.DataTable && 235 data instanceof google.visualization.DataTable) { 236 // read DataTable 237 var hasGroups = (data.getNumberOfColumns() > 3); 238 for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { 239 items.push(this.createItem({ 240 'start': data.getValue(row, 0), 241 'end': data.getValue(row, 1), 242 'content': data.getValue(row, 2), 243 'group': (hasGroups ? data.getValue(row, 3) : undefined) 244 })); 245 } 246 } 247 else if (data instanceof Array) { 248 // read JSON array 249 for (var row = 0, rows = data.length; row < rows; row++) { 250 var itemData = data[row] 251 var item = this.createItem(itemData); 252 items.push(item); 253 } 254 } 255 else { 256 throw "Unknown data type. DataTable or Array expected."; 257 } 258 259 // set a flag to force the recalcSize method to recalculate the 260 // heights and widths of the events 261 this.size.dataChanged = true; 262 this.redrawFrame(); // create the items for the new data 263 this.recalcSize(); // position the items 264 this.stackEvents(false); 265 this.redrawFrame(); // redraw the items on the final positions 266 this.size.dataChanged = false; 267 } 268 269 /** 270 * Set the groups available in the given dataset 271 * @param {DataTable or JSON array} data 272 */ 273 links.Timeline.prototype.setGroups = function (data) { 274 this.deleteGroups(); 275 var groups = this.groups; 276 var groupIndexes = this.groupIndexes; 277 278 if (google && google.visualization && google.visualization.DataTable && 279 data instanceof google.visualization.DataTable) { 280 // get groups from DataTable 281 var hasGroups = (data.getNumberOfColumns() > 3); 282 if (hasGroups) { 283 var groupNames = data.getDistinctValues(3); 284 for (var i = 0, iMax = groupNames.length; i < iMax; i++) { 285 this.addGroup(groupNames[i]); 286 } 287 } 288 } 289 else if (data instanceof Array){ 290 // get groups from JSON Array 291 for (var i = 0, iMax = data.length; i < iMax; i++) { 292 var row = data[i], 293 group = row.group; 294 if (group) { 295 this.addGroup(group); 296 } 297 } 298 } 299 else { 300 throw 'Unknown data type. DataTable or Array expected.'; 301 } 302 } 303 304 305 /** 306 * Return the original data table. 307 * @param {Google DataTable or Array} data 308 */ 309 links.Timeline.prototype.getData = function () { 310 return this.data; 311 } 312 313 314 /** 315 * Update the original data with changed start, end or group. 316 * 317 * @param {Number} index 318 * @param {Object} values An object containing some of the following parameters: 319 * {Date} start, 320 * {Date} end, 321 * {String} content, 322 * {String} group 323 */ 324 links.Timeline.prototype.updateData = function (index, values) { 325 var data = this.data; 326 327 if (google && google.visualization && google.visualization.DataTable && 328 data instanceof google.visualization.DataTable) { 329 // update the original google DataTable 330 var missingRows = (index + 1) - data.getNumberOfRows(); 331 if (missingRows > 0) { 332 data.addRows(missingRows); 333 } 334 335 if (values.start) { 336 data.setValue(index, 0, values.start); 337 } 338 if (values.end) { 339 data.setValue(index, 1, values.end); 340 } 341 if (values.content) { 342 data.setValue(index, 2, values.content); 343 } 344 if (values.group && data.getNumberOfColumns() > 3) { 345 // TODO: append a column when needed? 346 data.setValue(index, 3, values.group); 347 } 348 } 349 else if (data instanceof Array) { 350 // update the original JSON table 351 var row = data[index]; 352 if (row == undefined) { 353 row = {}; 354 data[index] = row; 355 } 356 357 if (values.start) { 358 row.start = values.start; 359 } 360 if (values.end) { 361 row.end = values.end; 362 } 363 if (values.content) { 364 row.content = values.content; 365 } 366 if (values.group) { 367 row.group = values.group; 368 } 369 } 370 else { 371 throw "Cannot update data, unknown type of data"; 372 } 373 } 374 375 /** 376 * Find the item index from a given HTML element 377 * If no item index is found, undefined is returned 378 * @param {HTML DOM element} element 379 * @return {Number} index 380 */ 381 links.Timeline.prototype.getItemIndex = function(element) { 382 var e = element, 383 dom = this.dom, 384 items = this.items, 385 index = undefined; 386 387 // try to find the frame where the items are located in 388 while (e.parentNode && e.parentNode !== dom.items.frame) { 389 e = e.parentNode; 390 } 391 392 if (e.parentNode === dom.items.frame) { 393 // yes! we have found the parent element of all items 394 // retrieve its id from the array with items 395 for (var i = 0, iMax = items.length; i < iMax; i++) { 396 if (items[i].dom === e) { 397 index = i; 398 break; 399 } 400 } 401 } 402 403 return index; 404 } 405 406 /** 407 * Set a new size for the timeline 408 * @param {string} width Width in pixels or percentage (for example "800px" 409 * or "50%") 410 * @param {string} height Height in pixels or percentage (for example "400px" 411 * or "30%") 412 */ 413 links.Timeline.prototype.setSize = function(width, height) { 414 if (width) { 415 this.options.width = width; 416 this.dom.frame.style.width = width; 417 } 418 if (height) { 419 this.options.height = height; 420 this.options.autoHeight = (this.options.height === "auto"); 421 if (height !== "auto" ) { 422 this.dom.frame.style.height = height; 423 } 424 } 425 426 this.recalcSize(); 427 this.stackEvents(false); 428 this.redrawFrame(); 429 } 430 431 432 /** 433 * Set a new value for the visible range int the timeline. 434 * Set start to null to include everything from the earliest date to end. 435 * Set end to null to include everything from start to the last date. 436 * Example usage: 437 * myTimeline.setVisibleChartRange(new Date("2010-08-22"), 438 * new Date("2010-09-13")); 439 * @param {Date} start The start date for the timeline. optional 440 * @param {Date} end The end date for the timeline. optional 441 * @param {boolean} redraw Optional. If true (default) the Timeline is 442 * directly redrawn 443 */ 444 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) { 445 if (start != null) { 446 this.start = new Date(start); 447 } else { 448 // default of 3 days ago 449 this.start = new Date(); 450 this.start.setDate(this.start.getDate() - 3); 451 } 452 453 if (end != null) { 454 this.end = new Date(end); 455 } else { 456 // default of 4 days ahead 457 this.end = new Date(); 458 this.end.setDate(this.end.getDate() + 4); 459 } 460 461 // prevent start Date <= end Date 462 if (this.end.valueOf() <= this.start.valueOf()) { 463 this.end = new Date(this.start); 464 this.end.setDate(this.end.getDate() + 7); 465 } 466 467 if (redraw == undefined || redraw == true) { 468 this.recalcSize(); 469 this.stackEvents(false); 470 this.redrawFrame(); 471 } 472 else { 473 this.recalcConversion(); 474 } 475 } 476 477 478 /** 479 * Change the visible chart range such that all items become visible 480 */ 481 links.Timeline.prototype.setVisibleChartRangeAuto = function() { 482 var items = this.items; 483 startMin = undefined, // long value of a data 484 endMax = undefined; // long value of a data 485 486 // find earliest start date from the data 487 for (var i = 0, iMax = items.length; i < iMax; i++) { 488 var item = items[i], 489 start = item.start ? item.start.valueOf() : undefined, 490 end = item.end ? item.end.valueOf() : start; 491 492 if (startMin !== undefined && start !== undefined) { 493 startMin = Math.min(startMin, start); 494 } 495 else { 496 startMin = start; 497 } 498 if (endMax !== undefined && end !== undefined) { 499 endMax = Math.max(endMax, end); 500 } 501 else { 502 endMax = end; 503 } 504 } 505 506 if (startMin !== undefined && endMax !== undefined) { 507 // zoom out 5% such that you have a little white space on the left and right 508 var center = (endMax + startMin) / 2, 509 diff = (endMax - startMin); 510 startMin = startMin - diff * 0.05; 511 endMax = endMax + diff * 0.05; 512 513 // adjust the start and end date 514 this.setVisibleChartRange(new Date(startMin), new Date(endMax)); 515 } 516 else { 517 this.setVisibleChartRange(undefined, undefined); 518 } 519 } 520 521 /** 522 * Adjust the visible range such that the current time is located in the center 523 * of the timeline 524 */ 525 links.Timeline.prototype.setVisibleChartRangeNow = function() { 526 var now = new Date(); 527 528 var diff = (this.end.getTime() - this.start.getTime()); 529 530 var startNew = new Date(now.getTime() - diff/2); 531 var endNew = new Date(startNew.getTime() + diff); 532 this.setVisibleChartRange(startNew, endNew); 533 } 534 535 536 /** 537 * Retrieve the current visible range in the timeline. 538 * @return {Object} An object with start and end properties 539 */ 540 links.Timeline.prototype.getVisibleChartRange = function() { 541 var range = { 542 'start': new Date(this.start), 543 'end': new Date(this.end) 544 }; 545 return range; 546 } 547 548 549 /** 550 * Redraw the timeline. This needs to be executed after the start and/or 551 * end time are changed, or when data is added or removed dynamically. 552 */ 553 links.Timeline.prototype.redrawFrame = function() { 554 var dom = this.dom, 555 options = this.options, 556 size = this.size; 557 558 if (!dom.frame) { 559 // the surrounding main frame 560 dom.frame = document.createElement("DIV"); 561 dom.frame.className = "timeline-frame"; 562 dom.frame.style.position = "relative"; 563 dom.frame.style.overflow = "hidden"; 564 dom.frame.style.width = options.width || "100%"; 565 dom.frame.style.height = options.height || "100%"; 566 dom.container.appendChild(dom.frame); 567 } 568 569 if (options.autoHeight) { 570 dom.frame.style.height = size.frameHeight + "px"; 571 } 572 else { 573 dom.frame.style.height = options.height; 574 } 575 576 this.redrawContent(); 577 this.redrawGroups(); 578 this.redrawCurrentTime(); 579 this.redrawCustomTime(); 580 this.redrawNavigation(); 581 } 582 583 584 /** 585 * Redraw the content of the timeline: the axis and the items 586 */ 587 links.Timeline.prototype.redrawContent = function() { 588 var dom = this.dom, 589 size = this.size; 590 591 if (!dom.content) { 592 // create content box where the axis and canvas will 593 dom.content = document.createElement("DIV"); 594 //this.frame.className = "timeline-frame"; 595 dom.content.style.position = "relative"; 596 dom.content.style.overflow = "hidden"; 597 dom.frame.appendChild(dom.content); 598 599 var timelines = document.createElement("DIV"); 600 timelines.style.position = "absolute"; 601 timelines.style.left = "0px"; 602 timelines.style.top = "0px"; 603 timelines.style.height = "100%"; 604 timelines.style.width = "0px"; 605 dom.content.appendChild(timelines); 606 dom.contentTimelines = timelines; 607 608 var params = this.eventParams, 609 me = this; 610 if (!params.onMouseDown) { 611 params.onMouseDown = function (event) {me.onMouseDown(event);}; 612 links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown); 613 } 614 if (!params.onTouchStart) { 615 params.onTouchStart = function (event) {me.onTouchStart(event);}; 616 links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart); 617 } 618 if (!params.onMouseWheel) { 619 params.onMouseWheel = function (event) {me.onMouseWheel(event);}; 620 links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel); 621 } 622 if (!params.onDblClick) { 623 params.onDblClick = function (event) {me.onDblClick(event);}; 624 links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick); 625 } 626 } 627 dom.content.style.left = size.contentLeft + "px"; 628 dom.content.style.top = "0px"; 629 dom.content.style.width = size.contentWidth + "px"; 630 dom.content.style.height = size.frameHeight + "px"; 631 632 this.redrawAxis(); 633 this.redrawItems(); 634 this.redrawDeleteButton(); 635 this.redrawDragAreas(); 636 } 637 638 /** 639 * Redraw the timeline axis with minor and major labels 640 */ 641 links.Timeline.prototype.redrawAxis = function() { 642 var dom = this.dom, 643 options = this.options, 644 size = this.size, 645 step = this.step; 646 647 var axis = dom.axis; 648 if (!axis) { 649 axis = {}; 650 dom.axis = axis; 651 } 652 if (size.axis.properties === undefined) { 653 size.axis.properties = {}; 654 } 655 if (axis.minorTexts === undefined) { 656 axis.minorTexts = []; 657 } 658 if (axis.minorLines === undefined) { 659 axis.minorLines = []; 660 } 661 if (axis.majorTexts === undefined) { 662 axis.majorTexts = []; 663 } 664 if (axis.majorLines === undefined) { 665 axis.majorLines = []; 666 } 667 668 if (!axis.frame) { 669 axis.frame = document.createElement("DIV"); 670 axis.frame.style.position = "absolute"; 671 axis.frame.style.left = "0px"; 672 axis.frame.style.top = "0px"; 673 dom.content.appendChild(axis.frame); 674 } 675 676 // take axis offline 677 dom.content.removeChild(axis.frame); 678 679 axis.frame.style.width = (size.contentWidth) + "px"; 680 axis.frame.style.height = (size.axis.height) + "px"; 681 682 // the drawn axis is more wide than the actual visual part, such that 683 // the axis can be dragged without having to redraw it each time again. 684 var start = this.screenToTime(0); 685 var end = this.screenToTime(size.contentWidth); 686 var width = size.contentWidth; 687 688 // calculate minimum step (in milliseconds) based on character size 689 this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() - 690 this.screenToTime(0).valueOf(); 691 692 step.setRange(start, end, this.minimumStep); 693 694 this.redrawAxisCharacters(); 695 696 this.redrawAxisStartOverwriting(); 697 698 step.start(); 699 var xFirstMajorLabel = undefined; 700 while (!step.end()) { 701 var cur = step.getCurrent(), 702 x = this.timeToScreen(cur), 703 isMajor = step.isMajor(); 704 705 this.redrawAxisMinorText(x, step.getLabelMinor()); 706 707 if (isMajor && options.showMajorLabels) { 708 if (x > 0) { 709 if (xFirstMajorLabel === undefined) { 710 xFirstMajorLabel = x; 711 } 712 this.redrawAxisMajorText(x, step.getLabelMajor()); 713 } 714 this.redrawAxisMajorLine(x); 715 } 716 else { 717 this.redrawAxisMinorLine(x); 718 } 719 720 step.next(); 721 } 722 723 // create a major label on the left when needed 724 if (options.showMajorLabels) { 725 var leftTime = this.screenToTime(0), 726 leftText = this.step.getLabelMajor(leftTime), 727 width = leftText.length * size.axis.characterMajorWidth + 10;// estimation 728 729 if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) { 730 this.redrawAxisMajorText(0, leftText, leftTime); 731 } 732 } 733 734 this.redrawAxisHorizontal(); 735 736 // cleanup left over labels 737 this.redrawAxisEndOverwriting(); 738 739 // put axis online 740 dom.content.insertBefore(axis.frame, dom.content.firstChild); 741 742 } 743 744 /** 745 * Create characters used to determine the size of text on the axis 746 */ 747 links.Timeline.prototype.redrawAxisCharacters = function () { 748 // calculate the width and height of a single character 749 // this is used to calculate the step size, and also the positioning of the 750 // axis 751 var dom = this.dom, 752 axis = dom.axis; 753 754 if (!axis.characterMinor) { 755 var text = document.createTextNode("0"); 756 var characterMinor = document.createElement("DIV"); 757 characterMinor.className = "timeline-axis-text timeline-axis-text-minor"; 758 characterMinor.appendChild(text); 759 characterMinor.style.position = "absolute"; 760 characterMinor.style.visibility = "hidden"; 761 characterMinor.style.paddingLeft = "0px"; 762 characterMinor.style.paddingRight = "0px"; 763 axis.frame.appendChild(characterMinor); 764 765 axis.characterMinor = characterMinor; 766 } 767 768 if (!axis.characterMajor) { 769 var text = document.createTextNode("0"); 770 var characterMajor = document.createElement("DIV"); 771 characterMajor.className = "timeline-axis-text timeline-axis-text-major"; 772 characterMajor.appendChild(text); 773 characterMajor.style.position = "absolute"; 774 characterMajor.style.visibility = "hidden"; 775 characterMajor.style.paddingLeft = "0px"; 776 characterMajor.style.paddingRight = "0px"; 777 axis.frame.appendChild(characterMajor); 778 779 axis.characterMajor = characterMajor; 780 } 781 } 782 783 /** 784 * Initialize redraw of the axis. All existing labels and lines will be 785 * overwritten and reused. 786 */ 787 links.Timeline.prototype.redrawAxisStartOverwriting = function () { 788 var properties = this.size.axis.properties; 789 790 properties.minorTextNum = 0; 791 properties.minorLineNum = 0; 792 properties.majorTextNum = 0; 793 properties.majorLineNum = 0; 794 } 795 796 /** 797 * End of overwriting HTML DOM elements of the axis. 798 * remaining elements will be removed 799 */ 800 links.Timeline.prototype.redrawAxisEndOverwriting = function () { 801 var dom = this.dom, 802 props = this.size.axis.properties, 803 frame = this.dom.axis.frame; 804 805 // remove leftovers 806 var minorTexts = dom.axis.minorTexts, 807 num = props.minorTextNum; 808 while (minorTexts.length > num) { 809 var minorText = minorTexts[num]; 810 frame.removeChild(minorText); 811 minorTexts.splice(num, 1); 812 } 813 814 var minorLines = dom.axis.minorLines, 815 num = props.minorLineNum; 816 while (minorLines.length > num) { 817 var minorLine = minorLines[num]; 818 frame.removeChild(minorLine); 819 minorLines.splice(num, 1); 820 } 821 822 var majorTexts = dom.axis.majorTexts, 823 num = props.majorTextNum; 824 while (majorTexts.length > num) { 825 var majorText = majorTexts[num]; 826 frame.removeChild(majorText); 827 majorTexts.splice(num, 1); 828 } 829 830 var majorLines = dom.axis.majorLines, 831 num = props.majorLineNum; 832 while (majorLines.length > num) { 833 var majorLine = majorLines[num]; 834 frame.removeChild(majorLine); 835 majorLines.splice(num, 1); 836 } 837 } 838 839 /** 840 * Redraw the horizontal line and background of the axis 841 */ 842 links.Timeline.prototype.redrawAxisHorizontal = function() { 843 var axis = this.dom.axis, 844 size = this.size; 845 846 if (!axis.backgroundLine) { 847 // create the axis line background (for a background color or so) 848 var backgroundLine = document.createElement("DIV"); 849 backgroundLine.className = "timeline-axis"; 850 backgroundLine.style.position = "absolute"; 851 backgroundLine.style.left = "0px"; 852 backgroundLine.style.width = "100%"; 853 backgroundLine.style.border = "none"; 854 axis.frame.insertBefore(backgroundLine, axis.frame.firstChild); 855 856 axis.backgroundLine = backgroundLine; 857 } 858 axis.backgroundLine.style.top = size.axis.top + "px"; 859 axis.backgroundLine.style.height = size.axis.height + "px"; 860 861 if (axis.line) { 862 // put this line at the end of all childs 863 var line = axis.frame.removeChild(axis.line); 864 axis.frame.appendChild(line); 865 } 866 else { 867 // make the axis line 868 var line = document.createElement("DIV"); 869 line.className = "timeline-axis"; 870 line.style.position = "absolute"; 871 line.style.left = "0px"; 872 line.style.width = "100%"; 873 line.style.height = "0px"; 874 axis.frame.appendChild(line); 875 876 axis.line = line; 877 } 878 axis.line.style.top = size.axis.line + "px"; 879 880 } 881 882 /** 883 * Create a minor label for the axis at position x 884 * @param {Number} x 885 * @param {String} text 886 */ 887 links.Timeline.prototype.redrawAxisMinorText = function (x, text) { 888 var size = this.size, 889 dom = this.dom, 890 props = size.axis.properties, 891 frame = dom.axis.frame, 892 minorTexts = dom.axis.minorTexts, 893 index = props.minorTextNum, 894 label; 895 896 if (index < minorTexts.length) { 897 label = minorTexts[index] 898 } 899 else { 900 // create new label 901 var content = document.createTextNode(""), 902 label = document.createElement("DIV"); 903 label.appendChild(content); 904 label.className = "timeline-axis-text timeline-axis-text-minor"; 905 label.style.position = "absolute"; 906 907 frame.appendChild(label); 908 909 minorTexts.push(label); 910 } 911 912 label.childNodes[0].nodeValue = text; 913 label.style.left = x + "px"; 914 label.style.top = size.axis.labelMinorTop + "px"; 915 //label.title = title; // TODO: this is a heavy operation 916 917 props.minorTextNum++; 918 } 919 920 /** 921 * Create a minor line for the axis at position x 922 * @param {Number} x 923 */ 924 links.Timeline.prototype.redrawAxisMinorLine = function (x) { 925 var axis = this.size.axis, 926 dom = this.dom, 927 props = axis.properties, 928 frame = dom.axis.frame, 929 minorLines = dom.axis.minorLines, 930 index = props.minorLineNum, 931 line; 932 933 if (index < minorLines.length) { 934 line = minorLines[index]; 935 } 936 else { 937 // create vertical line 938 line = document.createElement("DIV"); 939 line.className = "timeline-axis-grid timeline-axis-grid-minor"; 940 line.style.position = "absolute"; 941 line.style.width = "0px"; 942 943 frame.appendChild(line); 944 minorLines.push(line); 945 } 946 947 line.style.top = axis.lineMinorTop + "px"; 948 line.style.height = axis.lineMinorHeight + "px"; 949 line.style.left = (x - axis.lineMinorWidth/2) + "px"; 950 951 props.minorLineNum++; 952 } 953 954 /** 955 * Create a Major label for the axis at position x 956 * @param {Number} x 957 * @param {String} text 958 */ 959 links.Timeline.prototype.redrawAxisMajorText = function (x, text) { 960 var size = this.size, 961 props = size.axis.properties, 962 frame = this.dom.axis.frame, 963 majorTexts = this.dom.axis.majorTexts, 964 index = props.majorTextNum, 965 label; 966 967 if (index < majorTexts.length) { 968 label = majorTexts[index]; 969 } 970 else { 971 // create label 972 var content = document.createTextNode(text); 973 label = document.createElement("DIV"); 974 label.className = "timeline-axis-text timeline-axis-text-major"; 975 label.appendChild(content); 976 label.style.position = "absolute"; 977 label.style.top = "0px"; 978 979 frame.appendChild(label); 980 majorTexts.push(label); 981 } 982 983 label.childNodes[0].nodeValue = text; 984 label.style.top = size.axis.labelMajorTop + "px"; 985 label.style.left = x + "px"; 986 //label.title = title; // TODO: this is a heavy operation 987 988 props.majorTextNum ++; 989 } 990 991 /** 992 * Create a Major line for the axis at position x 993 * @param {Number} x 994 */ 995 links.Timeline.prototype.redrawAxisMajorLine = function (x) { 996 var size = this.size, 997 props = size.axis.properties, 998 axis = this.size.axis, 999 frame = this.dom.axis.frame, 1000 majorLines = this.dom.axis.majorLines, 1001 index = props.majorLineNum, 1002 line; 1003 1004 if (index < majorLines.length) { 1005 var line = majorLines[index]; 1006 } 1007 else { 1008 // create vertical line 1009 line = document.createElement("DIV"); 1010 line.className = "timeline-axis-grid timeline-axis-grid-major"; 1011 line.style.position = "absolute"; 1012 line.style.top = "0px"; 1013 line.style.width = "0px"; 1014 1015 frame.appendChild(line); 1016 majorLines.push(line); 1017 } 1018 1019 line.style.left = (x - axis.lineMajorWidth/2) + "px"; 1020 line.style.height = size.frameHeight + "px"; 1021 1022 props.majorLineNum ++; 1023 } 1024 1025 /** 1026 * Redraw all items 1027 */ 1028 links.Timeline.prototype.redrawItems = function() { 1029 var dom = this.dom, 1030 options = this.options, 1031 size = this.size, 1032 contentWidth = size.contentWidth, 1033 items = this.items; 1034 1035 if (!dom.items) { 1036 dom.items = {}; 1037 } 1038 1039 // draw the frame containing the items 1040 var frame = dom.items.frame; 1041 if (!frame) { 1042 frame = document.createElement("DIV"); 1043 frame.style.position = "relative"; 1044 frame.style.left = "0px"; 1045 frame.style.width = "0px"; 1046 dom.content.appendChild(frame); 1047 dom.items.frame = frame; 1048 } 1049 frame.style.top = size.items.top + "px"; 1050 frame.style.height = (size.frameHeight - size.axis.height) + "px"; 1051 1052 // initialize arrarys for storing the items 1053 var ranges = dom.items.ranges; 1054 if (!ranges) { 1055 ranges = []; 1056 dom.items.ranges = ranges; 1057 } 1058 var boxes = dom.items.boxes; 1059 if (!boxes) { 1060 boxes = []; 1061 dom.items.boxes = boxes; 1062 } 1063 var dots = dom.items.dots; 1064 if (!dots) { 1065 dots = []; 1066 dom.items.dots = dots; 1067 } 1068 1069 // Take frame offline 1070 dom.content.removeChild(frame); 1071 1072 if (size.dataChanged) { 1073 // create the items 1074 var rangesCreated = ranges.length, 1075 boxesCreated = boxes.length, 1076 dotsCreated = dots.length, 1077 rangesUsed = 0, 1078 boxesUsed = 0, 1079 dotsUsed = 0, 1080 itemsLength = items.length; 1081 1082 for (var i = 0, iMax = items.length; i < iMax; i++) { 1083 var item = items[i]; 1084 switch (item.type) { 1085 case 'range': 1086 if (rangesUsed < rangesCreated) { 1087 // reuse existing range 1088 var domItem = ranges[rangesUsed]; 1089 domItem.firstChild.innerHTML = item.content; 1090 domItem.style.display = ''; 1091 item.dom = domItem; 1092 rangesUsed++; 1093 } 1094 else { 1095 // create a new range 1096 var domItem = this.createEventRange(item.content); 1097 ranges[rangesUsed] = domItem; 1098 frame.appendChild(domItem); 1099 item.dom = domItem; 1100 rangesUsed++; 1101 rangesCreated++; 1102 } 1103 break; 1104 1105 case 'box': 1106 if (boxesUsed < boxesCreated) { 1107 // reuse existing box 1108 var domItem = boxes[boxesUsed]; 1109 domItem.firstChild.innerHTML = item.content; 1110 domItem.style.display = ''; 1111 item.dom = domItem; 1112 boxesUsed++; 1113 } 1114 else { 1115 // create a new box 1116 var domItem = this.createEventBox(item.content); 1117 boxes[boxesUsed] = domItem; 1118 frame.appendChild(domItem); 1119 frame.insertBefore(domItem.line, frame.firstChild); 1120 // Note: line must be added in front of the items, 1121 // such that it stays below all items 1122 frame.appendChild(domItem.dot); 1123 item.dom = domItem; 1124 boxesUsed++; 1125 boxesCreated++; 1126 } 1127 break; 1128 1129 case 'dot': 1130 if (dotsUsed < dotsCreated) { 1131 // reuse existing box 1132 var domItem = dots[dotsUsed]; 1133 domItem.firstChild.innerHTML = item.content; 1134 domItem.style.display = ''; 1135 item.dom = domItem; 1136 dotsUsed++; 1137 } 1138 else { 1139 // create a new box 1140 var domItem = this.createEventDot(item.content); 1141 dots[dotsUsed] = domItem; 1142 frame.appendChild(domItem); 1143 item.dom = domItem; 1144 dotsUsed++; 1145 dotsCreated++; 1146 } 1147 break; 1148 1149 default: 1150 // do nothing 1151 break; 1152 } 1153 } 1154 1155 // remove redundant items when needed 1156 for (var i = rangesUsed; i < rangesCreated; i++) { 1157 frame.removeChild(ranges[i]); 1158 } 1159 ranges.splice(rangesUsed, rangesCreated - rangesUsed); 1160 for (var i = boxesUsed; i < boxesCreated; i++) { 1161 var box = boxes[i]; 1162 frame.removeChild(box.line); 1163 frame.removeChild(box.dot); 1164 frame.removeChild(box); 1165 } 1166 boxes.splice(boxesUsed, boxesCreated - boxesUsed); 1167 for (var i = dotsUsed; i < dotsCreated; i++) { 1168 frame.removeChild(dots[i]); 1169 } 1170 dots.splice(dotsUsed, dotsCreated - dotsUsed); 1171 } 1172 1173 // reposition all items 1174 for (var i = 0, iMax = items.length; i < iMax; i++) { 1175 var item = items[i], 1176 domItem = item.dom; 1177 1178 switch (item.type) { 1179 case 'range': 1180 var left = this.timeToScreen(item.start), 1181 right = this.timeToScreen(item.end); 1182 1183 // limit the width of the item, as browsers cannot draw very wide divs 1184 if (left < -contentWidth) { 1185 left = -contentWidth; 1186 } 1187 if (right > 2 * contentWidth) { 1188 right = 2 * contentWidth; 1189 } 1190 1191 var visible = right > -contentWidth && left < 2 * contentWidth; 1192 if (visible || size.dataChanged) { 1193 // when data is changed, all items must be kept visible, as their heights must be measured 1194 if (item.hidden) { 1195 item.hidden = false; 1196 domItem.style.display = ''; 1197 } 1198 domItem.style.top = item.top + "px"; 1199 domItem.style.left = left + "px"; 1200 //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth 1201 domItem.style.width = Math.max(right - left, 1) + "px"; 1202 } 1203 else { 1204 // hide when outside of the current window 1205 if (!item.hidden) { 1206 domItem.style.display = 'none'; 1207 item.hidden = true; 1208 } 1209 } 1210 1211 break; 1212 1213 case 'box': 1214 var left = this.timeToScreen(item.start); 1215 1216 var axisOnTop = options.axisOnTop, 1217 axisHeight = size.axis.height, 1218 axisTop = size.axis.top; 1219 var visible = ((left + item.width/2 > -contentWidth) && 1220 (left - item.width/2 < 2 * contentWidth)); 1221 if (visible || size.dataChanged) { 1222 // when data is changed, all items must be kept visible, as their heights must be measured 1223 if (item.hidden) { 1224 item.hidden = false; 1225 domItem.style.display = ''; 1226 domItem.line.style.display = ''; 1227 domItem.dot.style.display = ''; 1228 } 1229 domItem.style.top = item.top + "px"; 1230 domItem.style.left = (left - item.width/2) + "px"; 1231 1232 var line = domItem.line; 1233 line.style.left = (left - item.lineWidth/2) + "px"; 1234 if (axisOnTop) { 1235 //line.style.top = axisHeight + "px"; // TODO: cleanup 1236 //line.style.height = (item.top - axisHeight) + "px"; 1237 line.style.top = "0px"; 1238 line.style.height = item.top + "px"; 1239 } 1240 else { 1241 line.style.top = (item.top + item.height) + "px"; 1242 line.style.height = (axisTop - item.top - item.height) + "px"; 1243 } 1244 1245 var dot = domItem.dot; 1246 dot.style.left = (left - item.dotWidth/2) + "px"; 1247 dot.style.top = (axisTop - item.dotHeight/2) + "px"; 1248 } 1249 else { 1250 // hide when outside of the current window 1251 if (!item.hidden) { 1252 domItem.style.display = 'none'; 1253 domItem.line.style.display = 'none'; 1254 domItem.dot.style.display = 'none'; 1255 item.hidden = true; 1256 } 1257 } 1258 break; 1259 1260 case 'dot': 1261 var left = this.timeToScreen(item.start); 1262 1263 var axisOnTop = options.axisOnTop, 1264 axisHeight = size.axis.height, 1265 axisTop = size.axis.top; 1266 var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth); 1267 if (visible || size.dataChanged) { 1268 // when data is changed, all items must be kept visible, as their heights must be measured 1269 if (item.hidden) { 1270 item.hidden = false; 1271 domItem.style.display = ''; 1272 } 1273 domItem.style.top = item.top + "px"; 1274 domItem.style.left = (left - item.dotWidth / 2) + "px"; 1275 1276 domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px"; 1277 domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px"; 1278 } 1279 else { 1280 // hide when outside of the current window 1281 if (!item.hidden) { 1282 domItem.style.display = 'none'; 1283 item.hidden = true; 1284 } 1285 } 1286 break; 1287 1288 default: 1289 // do nothing 1290 break; 1291 } 1292 } 1293 1294 // move selected item to the end, to ensure that it is always on top 1295 if (this.selection) { 1296 var item = this.selection.item; 1297 frame.removeChild(item); 1298 frame.appendChild(item); 1299 } 1300 1301 // put frame online again 1302 dom.content.appendChild(frame); 1303 } 1304 1305 1306 /** 1307 * Create an event in the timeline, with (optional) formatting: inside a box 1308 * with rounded corners, and a vertical line+dot to the axis. 1309 * @param {string} content The content for the event. This can be plain text 1310 * or HTML code. 1311 */ 1312 links.Timeline.prototype.createEventBox = function(content) { 1313 // background box 1314 var divBox = document.createElement("DIV"); 1315 divBox.style.position = "absolute"; 1316 divBox.style.left = "0px"; 1317 divBox.style.top = "0px"; 1318 divBox.className = "timeline-event timeline-event-box"; 1319 1320 // contents box (inside the background box). used for making margins 1321 var divContent = document.createElement("DIV"); 1322 divContent.className = "timeline-event-content"; 1323 divContent.innerHTML = content; 1324 divBox.appendChild(divContent); 1325 1326 // line to axis 1327 var divLine = document.createElement("DIV"); 1328 divLine.style.position = "absolute"; 1329 divLine.style.width = "0px"; 1330 divLine.className = "timeline-event timeline-event-line"; 1331 // important: the vertical line is added at the front of the list of elements, 1332 // so it will be drawn behind all boxes and ranges 1333 divBox.line = divLine; 1334 1335 // dot on axis 1336 var divDot = document.createElement("DIV"); 1337 divDot.style.position = "absolute"; 1338 divDot.style.width = "0px"; 1339 divDot.style.height = "0px"; 1340 divDot.className = "timeline-event timeline-event-dot"; 1341 divBox.dot = divDot; 1342 1343 return divBox; 1344 } 1345 1346 1347 /** 1348 * Create an event in the timeline: a dot, followed by the content. 1349 * @param {string} content The content for the event. This can be plain text 1350 * or HTML code. 1351 */ 1352 links.Timeline.prototype.createEventDot = function(content) { 1353 // background box 1354 var divBox = document.createElement("DIV"); 1355 divBox.style.position = "absolute"; 1356 1357 // contents box, right from the dot 1358 var divContent = document.createElement("DIV"); 1359 divContent.className = "timeline-event-content"; 1360 divContent.innerHTML = content; 1361 divBox.appendChild(divContent); 1362 1363 // dot at start 1364 var divDot = document.createElement("DIV"); 1365 divDot.style.position = "absolute"; 1366 divDot.className = "timeline-event timeline-event-dot"; 1367 divDot.style.width = "0px"; 1368 divDot.style.height = "0px"; 1369 divBox.appendChild(divDot); 1370 1371 divBox.content = divContent; 1372 divBox.dot = divDot; 1373 1374 return divBox; 1375 } 1376 1377 1378 /** 1379 * Create an event range as a beam in the timeline. 1380 * @param {string} content The content for the event. This can be plain text 1381 * or HTML code. 1382 */ 1383 links.Timeline.prototype.createEventRange = function(content) { 1384 // background box 1385 var divBox = document.createElement("DIV"); 1386 divBox.style.position = "absolute"; 1387 divBox.className = "timeline-event timeline-event-range"; 1388 1389 // contents box 1390 var divContent = document.createElement("DIV"); 1391 divContent.className = "timeline-event-content"; 1392 divContent.innerHTML = content; 1393 divBox.appendChild(divContent); 1394 1395 return divBox; 1396 } 1397 1398 /** 1399 * Redraw the group labels 1400 */ 1401 links.Timeline.prototype.redrawGroups = function() { 1402 var dom = this.dom, 1403 options = this.options, 1404 size = this.size, 1405 groups = this.groups; 1406 1407 if (dom.groups === undefined) { 1408 dom.groups = {}; 1409 } 1410 1411 var labels = dom.groups.labels; 1412 if (!labels) { 1413 labels = []; 1414 dom.groups.labels = labels; 1415 } 1416 var labelLines = dom.groups.labelLines; 1417 if (!labelLines) { 1418 labelLines = []; 1419 dom.groups.labelLines = labelLines; 1420 } 1421 var itemLines = dom.groups.itemLines; 1422 if (!itemLines) { 1423 itemLines = []; 1424 dom.groups.itemLines = itemLines; 1425 } 1426 1427 // create the frame for holding the groups 1428 var frame = dom.groups.frame; 1429 if (!frame) { 1430 var frame = document.createElement("DIV"); 1431 frame.className = "timeline-groups-axis"; 1432 frame.style.position = "absolute"; 1433 frame.style.overflow = "hidden"; 1434 frame.style.top = "0px"; 1435 frame.style.height = "100%"; 1436 1437 dom.frame.appendChild(frame); 1438 dom.groups.frame = frame; 1439 } 1440 1441 frame.style.left = size.groupsLeft + "px"; 1442 frame.style.width = (options.groupsWidth !== undefined) ? 1443 options.groupsWidth : 1444 size.groupsWidth + "px"; 1445 1446 // hide groups axis when there are no groups 1447 if (groups.length == 0) { 1448 frame.style.display = 'none'; 1449 } 1450 else { 1451 frame.style.display = ''; 1452 } 1453 1454 if (size.dataChanged) { 1455 // create the items 1456 var current = labels.length, 1457 needed = groups.length; 1458 1459 // overwrite existing items 1460 for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) { 1461 var group = groups[i]; 1462 var label = labels[i]; 1463 label.innerHTML = group.content; 1464 label.style.display = ''; 1465 } 1466 1467 // append new items when needed 1468 for (var i = current; i < needed; i++) { 1469 var group = groups[i]; 1470 1471 // create text label 1472 var label = document.createElement("DIV"); 1473 label.className = "timeline-groups-text"; 1474 label.style.position = "absolute"; 1475 if (options.groupsWidth === undefined) { 1476 label.style.whiteSpace = "nowrap"; 1477 } 1478 label.innerHTML = group.content; 1479 frame.appendChild(label); 1480 labels[i] = label; 1481 1482 // create the grid line between the group labels 1483 var labelLine = document.createElement("DIV"); 1484 labelLine.className = "timeline-axis-grid timeline-axis-grid-minor"; 1485 labelLine.style.position = "absolute"; 1486 labelLine.style.left = "0px"; 1487 labelLine.style.width = "100%"; 1488 labelLine.style.height = "0px"; 1489 labelLine.style.borderTopStyle = "solid"; 1490 frame.appendChild(labelLine); 1491 labelLines[i] = labelLine; 1492 1493 // create the grid line between the items 1494 var itemLine = document.createElement("DIV"); 1495 itemLine.className = "timeline-axis-grid timeline-axis-grid-minor"; 1496 itemLine.style.position = "absolute"; 1497 itemLine.style.left = "0px"; 1498 itemLine.style.width = "100%"; 1499 itemLine.style.height = "0px"; 1500 itemLine.style.borderTopStyle = "solid"; 1501 dom.content.insertBefore(itemLine, dom.content.firstChild); 1502 itemLines[i] = itemLine; 1503 } 1504 1505 // remove redundant items from the DOM when needed 1506 for (var i = needed; i < current; i++) { 1507 var label = labels[i], 1508 labelLine = labelLines[i], 1509 itemLine = itemLines[i]; 1510 1511 frame.removeChild(label); 1512 frame.removeChild(labelLine); 1513 dom.content.removeChild(itemLine); 1514 } 1515 labels.splice(needed, current - needed); 1516 labelLines.splice(needed, current - needed); 1517 itemLines.splice(needed, current - needed); 1518 1519 frame.style.borderStyle = options.groupsOnRight ? 1520 "none none none solid" : 1521 "none solid none none"; 1522 } 1523 1524 // position the groups 1525 for (var i = 0, iMax = groups.length; i < iMax; i++) { 1526 var group = groups[i], 1527 label = labels[i], 1528 labelLine = labelLines[i], 1529 itemLine = itemLines[i]; 1530 1531 label.style.top = group.labelTop + "px"; 1532 labelLine.style.top = group.lineTop + "px"; 1533 itemLine.style.top = group.lineTop + "px"; 1534 itemLine.style.width = size.contentWidth + "px"; 1535 } 1536 1537 if (!dom.groups.background) { 1538 // create the axis grid line background 1539 var background = document.createElement("DIV"); 1540 background.className = "timeline-axis"; 1541 background.style.position = "absolute"; 1542 background.style.left = "0px"; 1543 background.style.width = "100%"; 1544 background.style.border = "none"; 1545 1546 frame.appendChild(background); 1547 dom.groups.background = background; 1548 } 1549 dom.groups.background.style.top = size.axis.top + 'px'; 1550 dom.groups.background.style.height = size.axis.height + 'px'; 1551 1552 if (!dom.groups.line) { 1553 // create the axis grid line 1554 var line = document.createElement("DIV"); 1555 line.className = "timeline-axis"; 1556 line.style.position = "absolute"; 1557 line.style.left = "0px"; 1558 line.style.width = "100%"; 1559 line.style.height = "0px"; 1560 1561 frame.appendChild(line); 1562 dom.groups.line = line; 1563 } 1564 dom.groups.line.style.top = size.axis.line + 'px'; 1565 } 1566 1567 1568 /** 1569 * Redraw the current time bar 1570 */ 1571 links.Timeline.prototype.redrawCurrentTime = function() { 1572 var options = this.options, 1573 dom = this.dom, 1574 size = this.size; 1575 1576 if (!options.showCurrentTime) { 1577 if (dom.currentTime) { 1578 dom.contentTimelines.removeChild(dom.currentTime); 1579 delete dom.currentTime; 1580 } 1581 1582 return; 1583 } 1584 1585 if (!dom.currentTime) { 1586 // create the current time bar 1587 var currentTime = document.createElement("DIV"); 1588 currentTime.className = "timeline-currenttime"; 1589 currentTime.style.position = "absolute"; 1590 currentTime.style.top = "0px"; 1591 currentTime.style.height = "100%"; 1592 1593 dom.contentTimelines.appendChild(currentTime); 1594 dom.currentTime = currentTime; 1595 } 1596 1597 var now = new Date(); 1598 var nowOffset = new Date(now.getTime() + this.clientTimeOffset); 1599 var x = this.timeToScreen(nowOffset); 1600 1601 var visible = (x > -size.contentWidth && x < 2 * size.contentWidth); 1602 dom.currentTime.style.display = visible ? '' : 'none'; 1603 dom.currentTime.style.left = x + "px"; 1604 dom.currentTime.title = "Current time: " + nowOffset; 1605 1606 // start a timer to adjust for the new time 1607 if (this.currentTimeTimer != undefined) { 1608 clearTimeout(this.currentTimeTimer); 1609 delete this.currentTimeTimer; 1610 } 1611 var timeline = this; 1612 var onTimeout = function() { 1613 timeline.redrawCurrentTime(); 1614 } 1615 // the time equal to the width of one pixel, divided by 2 for more smoothness 1616 var interval = 1 / this.conversion.factor / 2; 1617 if (interval < 30) interval = 30; 1618 this.currentTimeTimer = setTimeout(onTimeout, interval); 1619 } 1620 1621 /** 1622 * Redraw the custom time bar 1623 */ 1624 links.Timeline.prototype.redrawCustomTime = function() { 1625 var options = this.options, 1626 dom = this.dom, 1627 size = this.size; 1628 1629 if (!options.showCustomTime) { 1630 if (dom.customTime) { 1631 dom.contentTimelines.removeChild(dom.customTime); 1632 delete dom.customTime; 1633 } 1634 1635 return; 1636 } 1637 1638 if (!dom.customTime) { 1639 var customTime = document.createElement("DIV"); 1640 customTime.className = "timeline-customtime"; 1641 customTime.style.position = "absolute"; 1642 customTime.style.top = "0px"; 1643 customTime.style.height = "100%"; 1644 1645 var drag = document.createElement("DIV"); 1646 drag.style.position = "relative"; 1647 drag.style.top = "0px"; 1648 drag.style.left = "-10px"; 1649 drag.style.height = "100%"; 1650 drag.style.width = "20px"; 1651 customTime.appendChild(drag); 1652 1653 dom.contentTimelines.appendChild(customTime); 1654 dom.customTime = customTime; 1655 1656 // initialize parameter 1657 this.customTime = new Date(); 1658 } 1659 1660 var x = this.timeToScreen(this.customTime), 1661 visible = (x > -size.contentWidth && x < 2 * size.contentWidth); 1662 dom.customTime.style.display = visible ? '' : 'none'; 1663 dom.customTime.style.left = x + "px"; 1664 dom.customTime.title = "Time: " + this.customTime; 1665 } PORT 1667 1668 /** 1669 * Redraw the delete button, on the top right of the currently selected item 1670 * if there is no item selected, the button is hidden. 1671 */ 1672 links.Timeline.prototype.redrawDeleteButton = function () { 1673 var timeline = this, 1674 options = this.options, 1675 dom = this.dom, 1676 size = this.size, 1677 frame = dom.items.frame; 1678 1679 if (!options.editable) { 1680 return; 1681 } 1682 1683 var deleteButton = dom.items.deleteButton; 1684 if (!deleteButton) { 1685 // create a delete button 1686 deleteButton = document.createElement("DIV"); 1687 deleteButton.className = "timeline-navigation-delete"; 1688 deleteButton.style.position = "absolute"; 1689 1690 frame.appendChild(deleteButton); 1691 dom.items.deleteButton = deleteButton; 1692 } 1693 1694 if (this.selection) { 1695 var index = this.selection.index, 1696 item = this.items[index], 1697 domItem = this.selection.item, 1698 right, 1699 top = item.top; 1700 1701 switch (item.type) { 1702 case 'range': 1703 right = this.timeToScreen(item.end); 1704 break; 1705 1706 case 'box': 1707 //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth 1708 right = this.timeToScreen(item.start) + item.width / 2; 1709 break; 1710 1711 case 'dot': 1712 right = this.timeToScreen(item.start) + item.width; 1713 break; 1714 } 1715 1716 // limit the position 1717 if (right < -size.contentWidth) { 1718 right = -size.contentWidth; 1719 } 1720 if (right > 2 * size.contentWidth) { 1721 right = 2 * size.contentWidth; 1722 } 1723 1724 deleteButton.style.left = right + 'px'; 1725 deleteButton.style.top = top + 'px'; 1726 deleteButton.style.display = ''; 1727 frame.removeChild(deleteButton); 1728 frame.appendChild(deleteButton); 1729 } 1730 else { 1731 deleteButton.style.display = 'none'; 1732 } 1733 } 1734 1735 1736 /** 1737 * Redraw the drag areas. When an item (ranges only) is selected, 1738 * it gets a drag area on the left and right side, to change its width 1739 */ 1740 links.Timeline.prototype.redrawDragAreas = function () { 1741 var timeline = this, 1742 options = this.options, 1743 dom = this.dom, 1744 size = this.size, 1745 frame = this.dom.items.frame; 1746 1747 if (!options.editable) { 1748 return; 1749 } 1750 1751 // create left drag area 1752 var dragLeft = dom.items.dragLeft; 1753 if (!dragLeft) { 1754 dragLeft = document.createElement("DIV"); 1755 dragLeft.style.width = options.dragAreaWidth + "px"; 1756 dragLeft.style.position = "absolute"; 1757 dragLeft.style.cursor = "w-resize"; 1758 1759 frame.appendChild(dragLeft); 1760 dom.items.dragLeft = dragLeft; 1761 } 1762 1763 // create right drag area 1764 var dragRight = dom.items.dragRight; 1765 if (!dragRight) { 1766 dragRight = document.createElement("DIV"); 1767 dragRight.style.width = options.dragAreaWidth + "px"; 1768 dragRight.style.position = "absolute"; 1769 dragRight.style.cursor = "e-resize"; 1770 1771 frame.appendChild(dragRight); 1772 dom.items.dragRight = dragRight; 1773 } 1774 1775 // reposition left and right drag area 1776 if (this.selection) { 1777 var index = this.selection.index, 1778 item = this.items[index]; 1779 1780 if (item.type == 'range') { 1781 var domItem = item.dom, 1782 left = this.timeToScreen(item.start), 1783 right = this.timeToScreen(item.end), 1784 top = item.top, 1785 height = item.height; 1786 1787 dragLeft.style.left = left + 'px'; 1788 dragLeft.style.top = top + 'px'; 1789 dragLeft.style.height = height + 'px'; 1790 dragLeft.style.display = ''; 1791 frame.removeChild(dragLeft); 1792 frame.appendChild(dragLeft); 1793 1794 dragRight.style.left = (right - options.dragAreaWidth) + 'px'; 1795 dragRight.style.top = top + 'px'; 1796 dragRight.style.height = height + 'px'; 1797 dragRight.style.display = ''; 1798 frame.removeChild(dragRight); 1799 frame.appendChild(dragRight); 1800 } 1801 } 1802 else { 1803 dragLeft.style.display = 'none'; 1804 dragRight.style.display = 'none'; 1805 } 1806 } 1807 1808 1809 1810 /** 1811 * Create the navigation buttons for zooming and moving 1812 */ 1813 links.Timeline.prototype.redrawNavigation = function () { 1814 var timeline = this, 1815 options = this.options, 1816 dom = this.dom, 1817 frame = dom.frame, 1818 navBar = dom.navBar; 1819 1820 if (!navBar) { 1821 if (options.editable || options.showNavigation) { 1822 // create a navigation bar containing the navigation buttons 1823 navBar = document.createElement("DIV"); 1824 navBar.style.position = "absolute"; 1825 navBar.className = "timeline-navigation"; 1826 if (options.groupsOnRight) { 1827 navBar.style.left = '10px'; 1828 } 1829 else { 1830 navBar.style.right = '10px'; 1831 } 1832 if (options.axisOnTop) { 1833 navBar.style.bottom = '10px'; 1834 } 1835 else { 1836 navBar.style.top = '10px'; 1837 } 1838 dom.navBar = navBar; 1839 frame.appendChild(navBar); 1840 } 1841 1842 if (options.editable && options.showButtonAdd) { 1843 // create a new in button 1844 navBar.addButton = document.createElement("DIV"); 1845 navBar.addButton.className = "timeline-navigation-new"; 1846 1847 navBar.addButton.title = "Create new event"; 1848 var onAdd = function(event) { 1849 links.Timeline.prevent ult(event); 1850 links.Timeline.stopPropagation(event); 1851 1852 // create a new event at the center of the frame 1853 var w = timeline.size.contentWidth; 1854 var x = w / 2; 1855 var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width 1856 var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width 1857 if (options.snapEvents) { 1858 timeline.step.snap(xstart); 1859 timeline.step.snap(xend); 1860 } 1861 1862 var content = "New"; 1863 var group = timeline.groups.length ? timeline.groups[0].content : undefined; 1864 1865 timeline.addItem({ 1866 'start': xstart, 1867 'end': xend, 1868 'content': content, 1869 'group': group 1870 }); 1871 var index = (timeline.items.length - 1); 1872 timeline.selectItem(index); 1873 1874 timeline.applyAdd = true; 1875 1876 // fire an add event. 1877 // Note that the change can be canceled from within an event listener if 1878 // this listener calls the method cancelAdd(). 1879 timeline.trigger('add'); 1880 1881 if (!timeline.applyAdd) { 1882 // undo an add 1883 timeline.deleteItem(index); 1884 } 1885 timeline.redrawDeleteButton(); 1886 timeline.redrawDragAreas(); 1887 }; 1888 links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd); 1889 navBar.appendChild(navBar.addButton); 1890 } 1891 1892 if (options.editable && options.showButtonAdd && options.showNavigation) { 1893 // create a separator line 1894 navBar.addButton.style.borderRightWidth = "1px"; 1895 navBar.addButton.style.borderRightStyle = "solid"; 1896 } 1897 1898 if (options.showNavigation) { 1899 // create a zoom in button 1900 navBar.zoomInButton = document.createElement("DIV"); 1901 navBar.zoomInButton.className = "timeline-navigation-zoom-in"; 1902 navBar.zoomInButton.title = "Zoom in"; 1903 var onZoomIn = function(event) { 1904 links.Timeline.prevent ult(event); 1905 links.Timeline.stopPropagation(event); 1906 timeline.zoom(0.4); 1907 timeline.trigger("rangechange"); 1908 timeline.trigger("rangechanged"); 1909 }; 1910 links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn); 1911 navBar.appendChild(navBar.zoomInButton); 1912 1913 // create a zoom out button 1914 navBar.zoomOutButton = document.createElement("DIV"); 1915 navBar.zoomOutButton.className = "timeline-navigation-zoom-out"; 1916 navBar.zoomOutButton.title = "Zoom out"; 1917 var onZoomOut = function(event) { 1918 links.Timeline.prevent ult(event); 1919 links.Timeline.stopPropagation(event); 1920 timeline.zoom(-0.4); 1921 timeline.trigger("rangechange"); 1922 timeline.trigger("rangechanged"); 1923 }; 1924 links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut); 1925 navBar.appendChild(navBar.zoomOutButton); 1926 1927 // create a move left button 1928 navBar.moveLeftButton = document.createElement("DIV"); 1929 navBar.moveLeftButton.className = "timeline-navigation-move-left"; 1930 navBar.moveLeftButton.title = "Move left"; 1931 var onMoveLeft = function(event) { 1932 links.Timeline.prevent ult(event); 1933 links.Timeline.stopPropagation(event); 1934 timeline.move(-0.2); 1935 timeline.trigger("rangechange"); 1936 timeline.trigger("rangechanged"); 1937 }; 1938 links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft); 1939 navBar.appendChild(navBar.moveLeftButton); 1940 1941 // create a move right button 1942 navBar.moveRightButton = document.createElement("DIV"); 1943 navBar.moveRightButton.className = "timeline-navigation-move-right"; 1944 navBar.moveRightButton.title = "Move right"; 1945 var onMoveRight = function(event) { 1946 links.Timeline.prevent ult(event); 1947 links.Timeline.stopPropagation(event); 1948 timeline.move(0.2); 1949 timeline.trigger("rangechange"); 1950 timeline.trigger("rangechanged"); 1951 }; 1952 links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight); 1953 navBar.appendChild(navBar.moveRightButton); 1954 } 1955 } 1956 } 1957 1958 1959 /** 1960 * Set current time. This function can be used to set the time in the client 1961 * timeline equal with the time on a server. 1962 * @param {Date} time 1963 */ 1964 links.Timeline.prototype.setCurrentTime = function(time) { 1965 var now = new Date(); 1966 this.clientTimeOffset = time.getTime() - now.getTime(); 1967 1968 this.redrawCurrentTime(); 1969 } 1970 1971 /** 1972 * Get current time. The time can have an offset from the real time, when 1973 * the current time has been changed via the method setCurrentTime. 1974 * @return {Date} time 1975 */ 1976 links.Timeline.prototype.getCurrentTime = function() { 1977 var now = new Date(); 1978 return new Date(now.getTime() + this.clientTimeOffset); 1979 } 1980 1981 1982 /** 1983 * Set custom time. 1984 * The custom time bar can be used to display events in past or future. 1985 * @param {Date} time 1986 */ 1987 links.Timeline.prototype.setCustomTime = function(time) { 1988 this.customTime = new Date(time); 1989 this.redrawCustomTime(); 1990 } 1991 1992 /** 1993 * Retrieve the current custom time. 1994 * @return {Date} customTime 1995 */ 1996 links.Timeline.prototype.getCustomTime = function() { 1997 return new Date(this.customTime); 1998 } 1999 2000 /** 2001 * Set a custom scale. Autoscaling will be disabled. 2002 * For example setScale(SCALE.MINUTES, 5) will result 2003 * in minor steps of 5 minutes, and major steps of an hour. 2004 * 2005 * @param {Step.SCALE} newScale A scale. Choose from SCALE.MILLISECOND, 2006 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 2007 * SCALE.DAY, SCALE.MONTH, SCALE.YEAR. 2008 * @param {int} newStep A step size, by default 1. Choose for 2009 * example 1, 2, 5, or 10. 2010 */ 2011 links.Timeline.prototype.setScale = function(scale, step) { 2012 this.step.setScale(scale, step); 2013 this.redrawFrame(); 2014 } 2015 2016 /** 2017 * Enable or disable autoscaling 2018 * @param {boolean} enable If true or not defined, autoscaling is enabled. 2019 * If false, autoscaling is disabled. 2020 */ 2021 links.Timeline.prototype.setAutoScale = function(enable) { 2022 this.step.setAutoScale(enable); 2023 this.redrawFrame(); 2024 } 2025 2026 /** 2027 * Redraw the timeline 2028 * Reloads the (linked) data table and redraws the timeline when resized. 2029 * See also the method checkResize 2030 */ 2031 links.Timeline.prototype.redraw = function() { 2032 this.setData(this.data); 2033 } 2034 2035 2036 /** 2037 * Check if the timeline is resized, and if so, redraw the timeline. 2038 * Useful when the webpage is resized. 2039 */ 2040 links.Timeline.prototype.checkResize = function() { 2041 var resized = this.recalcSize(); 2042 if (resized) { 2043 this.redrawFrame(); 2044 } 2045 } 2046 2047 2048 /** 2049 * Recalculate the sizes of all frames, groups, items, axis 2050 * After recalcSize() is executed, the Timeline should be redrawn normally 2051 * 2052 * @return {boolean} resized Returns true when the timeline has been resized 2053 */ 2054 links.Timeline.prototype.recalcSize = function() { 2055 var resized = false; 2056 2057 var size = this.size, 2058 options = this.options, 2059 axisOnTop = options.axisOnTop, 2060 dom = this.dom, 2061 axis = dom.axis, 2062 groups = this.groups, 2063 labels = dom.groups.labels, 2064 items = this.items 2065 2066 groupsWidth = size.groupsWidth, 2067 characterMinorWidth = axis.characterMinor ? axis.characterMinor.clientWidth : 0, 2068 characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0, 2069 characterMajorWidth = axis.characterMajor ? axis.characterMajor.clientWidth : 0, 2070 characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0, 2071 axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0), 2072 actualHeight = size.actualHeight || axisHeight; 2073 2074 // check sizes of the groups (width and height) when the data is changed 2075 if (size.dataChanged) { 2076 groupsWidth = 0; 2077 2078 // loop through all groups to get the maximum width and the heights 2079 for (var i = 0, iMax = labels.length; i < iMax; i++) { 2080 var group = groups[i]; 2081 group.width = labels[i].clientWidth; 2082 group.height = labels[i].clientHeight; 2083 group.labelHeight = group.height; 2084 2085 groupsWidth = Math.max(groupsWidth, group.width); 2086 } 2087 2088 // loop through the width and height of all items 2089 for (var i = 0, iMax = items.length; i < iMax; i++) { 2090 var item = items[i], 2091 domItem = item.dom, 2092 group = item.group; 2093 2094 item.width = domItem ? domItem.clientWidth : 0; 2095 item.height = domItem ? domItem.clientHeight : 0; 2096 //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth 2097 2098 switch (item.type) { 2099 case 'range': 2100 break; 2101 2102 case 'box': 2103 item.dotHeight = domItem.dot.offsetHeight; 2104 item.dotWidth = domItem.dot.offsetWidth; 2105 item.lineWidth = domItem.line.offsetWidth; 2106 break; 2107 2108 case 'dot': 2109 item.dotHeight = domItem.dot.offsetHeight; 2110 item.dotWidth = domItem.dot.offsetWidth; 2111 item.contentHeight = domItem.content.offsetHeight; 2112 break; 2113 } 2114 2115 if (group) { 2116 group.height = group.height ? Math.max(group.height, item.height) : item.height; 2117 } 2118 } 2119 2120 // calculate the actual height of the timeline (needed for auto sizing 2121 // the timeline) 2122 actualHeight = axisHeight + 2 * options.eventMarginAxis; 2123 for (var i = 0, iMax = groups.length; i < iMax; i++) { 2124 actualHeight += groups[i].height + options.eventMargin; 2125 } 2126 } 2127 2128 // calculate actual height of the timeline when there are no groups 2129 // but stacked items 2130 if (groups.length == 0 && options.autoHeight) { 2131 var min = 0, 2132 max = 0; 2133 2134 if (this.animation && this.animation.finalItems) { 2135 // adjust the offset of all finalItems when the actualHeight has been changed 2136 var finalItems = this.animation.finalItems, 2137 finalItem = finalItems[0]; 2138 if (finalItem && finalItem.top) { 2139 min = finalItem.top, 2140 max = finalItem.top + finalItem.height; 2141 } 2142 for (var i = 1, iMax = finalItems.length; i < iMax; i++) { 2143 finalItem = finalItems[i]; 2144 min = Math.min(min, finalItem.top); 2145 max = Math.max(max, finalItem.top + finalItem.height); 2146 } 2147 } 2148 else { 2149 var item = items[0]; 2150 if (item && item.top) { 2151 min = item.top, 2152 max = item.top + item.height; 2153 } 2154 for (var i = 1, iMax = items.length; i < iMax; i++) { 2155 var item = items[i]; 2156 if (item.top) { 2157 min = Math.min(min, item.top); 2158 max = Math.max(max, (item.top + item.height)); 2159 } 2160 } 2161 } 2162 2163 actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight; 2164 2165 if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) { 2166 // adjust the offset of all items when the actualHeight has been changed 2167 var diff = actualHeight - size.actualHeight; 2168 if (this.animation && this.animation.finalItems) { 2169 var finalItems = this.animation.finalItems; 2170 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 2171 finalItems[i].top += diff; 2172 finalItems[i].item.top += diff; // TODO 2173 } 2174 } 2175 else { 2176 for (var i = 0, iMax = items.length; i < iMax; i++) { 2177 items[i].top += diff; 2178 } 2179 } 2180 } 2181 } 2182 2183 // now the heights of the elements are known, we can calculate the the 2184 // width and height of frame and axis and content 2185 // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead 2186 var frameWidth = dom.frame ? dom.frame.offsetWidth : 0, 2187 frameHeight = Math.max(options.autoHeight ? 2188 actualHeight : (dom.frame ? dom.frame.clientHeight : 0), 2189 options.minHeight), 2190 axisTop = axisOnTop ? 0 : frameHeight - axisHeight, 2191 axisLine = axisOnTop ? axisHeight : axisTop, 2192 itemsTop = axisOnTop ? axisHeight : 0, 2193 contentHeight = Math.max(frameHeight - axisHeight, 0); 2194 2195 if (options.groupsWidth !== undefined) { 2196 groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0; 2197 } 2198 var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0; 2199 2200 if (size.dataChanged) { 2201 // calculate top positions of the group labels and lines 2202 var eventMargin = options.eventMargin, 2203 top = axisOnTop ? 2204 options.eventMarginAxis + eventMargin/2 : 2205 contentHeight - options.eventMarginAxis + eventMargin/2; 2206 2207 for (var i = 0, iMax = groups.length; i < iMax; i++) { 2208 var group = groups[i]; 2209 if (axisOnTop) { 2210 group.top = top; 2211 group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2; 2212 group.lineTop = top + axisHeight + group.height + eventMargin/2; 2213 top += group.height + eventMargin; 2214 } 2215 else { 2216 top -= group.height + eventMargin; 2217 group.top = top; 2218 group.labelTop = top + (group.height - group.labelHeight) / 2; 2219 group.lineTop = top - eventMargin/2; 2220 } 2221 } 2222 2223 // calculate top position of the items 2224 for (var i = 0, iMax = items.length; i < iMax; i++) { 2225 var item = items[i], 2226 group = item.group; 2227 2228 if (group) { 2229 item.top = group.top; 2230 } 2231 } 2232 2233 resized = true; 2234 } 2235 2236 resized = resized || (size.groupsWidth !== groupsWidth); 2237 resized = resized || (size.groupsLeft !== groupsLeft); 2238 resized = resized || (size.actualHeight !== actualHeight); 2239 size.groupsWidth = groupsWidth; 2240 size.groupsLeft = groupsLeft; 2241 size.actualHeight = actualHeight; 2242 2243 resized = resized || (size.frameWidth !== frameWidth); 2244 resized = resized || (size.frameHeight !== frameHeight); 2245 size.frameWidth = frameWidth; 2246 size.frameHeight = frameHeight; 2247 2248 resized = resized || (size.groupsWidth !== groupsWidth); 2249 size.groupsWidth = groupsWidth; 2250 size.contentLeft = options.groupsOnRight ? 0 : groupsWidth; 2251 size.contentWidth = Math.max(frameWidth - groupsWidth, 0); 2252 size.contentHeight = contentHeight; 2253 2254 resized = resized || (size.axis.top !== axisTop); 2255 resized = resized || (size.axis.line !== axisLine); 2256 resized = resized || (size.axis.height !== axisHeight); 2257 resized = resized || (size.items.top !== itemsTop); 2258 size.axis.top = axisTop; 2259 size.axis.line = axisLine; 2260 size.axis.height = axisHeight; 2261 size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight; 2262 size.axis.labelMinorTop = options.axisOnTop ? 2263 (options.showMajorLabels ? characterMajorHeight : 0) : 2264 axisLine; 2265 size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0; 2266 size.axis.lineMinorHeight = options.showMajorLabels ? 2267 frameHeight - characterMajorHeight: 2268 frameHeight; 2269 size.axis.lineMinorWidth = dom.axis.minorLines.length ? 2270 dom.axis.minorLines[0].offsetWidth : 1; 2271 size.axis.lineMajorWidth = dom.axis.majorLines.length ? 2272 dom.axis.majorLines[0].offsetWidth : 1; 2273 2274 size.items.top = itemsTop; 2275 2276 resized = resized || (size.axis.characterMinorWidth !== characterMinorWidth); 2277 resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight); 2278 resized = resized || (size.axis.characterMajorWidth !== characterMajorWidth); 2279 resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight); 2280 size.axis.characterMinorWidth = characterMinorWidth; 2281 size.axis.characterMinorHeight = characterMinorHeight; 2282 size.axis.characterMajorWidth = characterMajorWidth; 2283 size.axis.characterMajorHeight = characterMajorHeight; 2284 2285 // conversion factors can be changed when width of the Timeline is changed, 2286 // and when start or end are changed 2287 this.recalcConversion(); 2288 2289 return resized; 2290 } 2291 2292 2293 2294 /** 2295 * Calculate the factor and offset to convert a position on screen to the 2296 * corresponding date and vice versa. 2297 * After the method calcConversionFactor is executed once, the methods screenToTime and 2298 * timeToScreen can be used. 2299 */ 2300 links.Timeline.prototype.recalcConversion = function() { 2301 this.conversion.offset = parseFloat(this.start.valueOf()); 2302 this.conversion.factor = parseFloat(this.size.contentWidth) / 2303 parseFloat(this.end.valueOf() - this.start.valueOf()); 2304 } 2305 2306 2307 /** 2308 * Convert a position on screen (pixels) to a datetime 2309 * Before this method can be used, the method calcConversionFactor must be 2310 * executed once. 2311 * @param {int} x Position on the screen in pixels 2312 * @return {Date} time The datetime the corresponds with given position x 2313 */ 2314 links.Timeline.prototype.screenToTime = function(x) { 2315 var conversion = this.conversion, 2316 time = new Date(parseFloat(x) / conversion.factor + conversion.offset); 2317 return time; 2318 } 2319 2320 /** 2321 * Convert a datetime (Date object) into a position on the screen 2322 * Before this method can be used, the method calcConversionFactor must be 2323 * executed once. 2324 * @param {Date} time A date 2325 * @return {int} x The position on the screen in pixels which corresponds 2326 * with the given date. 2327 */ 2328 links.Timeline.prototype.timeToScreen = function(time) { 2329 var conversion = this.conversion; 2330 var x = (time.valueOf() - conversion.offset) * conversion.factor; 2331 return x; 2332 } 2333 2334 2335 2336 /** 2337 * Event handler for touchstart event on mobile devices 2338 */ 2339 links.Timeline.prototype.onTouchStart = function(event) { 2340 var params = this.eventParams, 2341 dom = this.dom, 2342 me = this; 2343 2344 if (params.touchDown) { 2345 // if already moving, return 2346 return; 2347 } 2348 2349 params.touchDown = true; 2350 params.zoomed = false; 2351 2352 this.onMouseDown(event); 2353 2354 if (!params.onTouchMove) { 2355 params.onTouchMove = function (event) {me.onTouchMove(event);}; 2356 links.Timeline.addEventListener(document, "touchmove", params.onTouchMove); 2357 } 2358 if (!params.onTouchEnd) { 2359 params.onTouchEnd = function (event) {me.onTouchEnd(event);}; 2360 links.Timeline.addEventListener(document, "touchend", params.onTouchEnd); 2361 } 2362 }; 2363 2364 /** 2365 * Event handler for touchmove event on mobile devices 2366 */ 2367 links.Timeline.prototype.onTouchMove = function(event) { 2368 var params = this.eventParams; 2369 2370 if (event.scale && event.scale !== 1) { 2371 params.zoomed = true; 2372 } 2373 2374 if (!params.zoomed) { 2375 // move 2376 this.onMouseMove(event); 2377 } 2378 else { 2379 if (this.options.zoomable) { 2380 // pinch 2381 // TODO: pinch only supported on iPhone/iPad. Create something manually for Android? 2382 params.zoomed = true; 2383 2384 var scale = event.scale, 2385 oldWidth = (params.end.valueOf() - params.start.valueOf()), 2386 newWidth = oldWidth / scale, 2387 diff = newWidth - oldWidth, 2388 start = new Date(parseInt(params.start.valueOf() - diff/2)), 2389 end = new Date(parseInt(params.end.valueOf() + diff/2)); 2390 2391 // TODO: determine zoom-around-date from touch positions? 2392 2393 this.setVisibleChartRange(start, end); 2394 timeline.trigger("rangechange"); 2395 } 2396 } 2397 }; 2398 2399 /** 2400 * Event handler for touchend event on mobile devices 2401 */ 2402 links.Timeline.prototype.onTouchEnd = function(event) { 2403 var params = this.eventParams; 2404 params.touchDown = false; 2405 2406 /* TODO: cleanup 2407 document.getElementById("info").innerHTML = "touchEnd"; 2408 */ 2409 2410 if (params.zoomed) { 2411 timeline.trigger("rangechanged"); 2412 } 2413 2414 if (params.onTouchMove) { 2415 links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove); 2416 delete params.onTouchMove; 2417 2418 } 2419 if (params.onTouchEnd) { 2420 links.Timeline.removeEventListener(document, "touchend", params.onTouchEnd); 2421 delete params.onTouchEnd; 2422 } 2423 2424 this.onMouseUp(event); 2425 }; 2426 2427 2428 /** 2429 * Start a moving operation inside the provided parent element 2430 * @param {event} event The event that occurred (required for 2431 * retrieving the mouse position) 2432 */ 2433 links.Timeline.prototype.onMouseDown = function(event) { 2434 event = event || window.event; 2435 2436 var params = this.eventParams, 2437 options = this.options, 2438 dom = this.dom; 2439 2440 // only react on left mouse button down 2441 var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); 2442 if (!leftButtonDown && !params.touchDown) { 2443 return; 2444 } 2445 2446 // check if frame is not resized (causing a mismatch with the end Date) 2447 this.recalcSize(); 2448 2449 // get mouse position 2450 if (!params.touchDown) { 2451 params.mouseX = event.clientX; 2452 params.mouseY = event.clientY; 2453 } 2454 else { 2455 params.mouseX = event.targetTouches[0].clientX; 2456 params.mouseY = event.targetTouches[0].clientY; 2457 } 2458 if (params.mouseX === undefined) {params.mouseX = 0;} 2459 if (params.mouseY === undefined) {params.mouseY = 0;} 2460 params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content); 2461 params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content); 2462 2463 params.moved = false; 2464 params.start = new Date(this.start); 2465 params.end = new Date(this.end); 2466 2467 params.target = links.Timeline.getTarget(event); 2468 params.itemDragLeft = (params.target === this.dom.items.dragLeft); 2469 params.itemDragRight = (params.target === this.dom.items.dragRight); 2470 2471 if (params.itemDragLeft || params.itemDragRight) { 2472 params.itemIndex = this.selection ? this.selection.index : undefined; 2473 } 2474 else { 2475 params.itemIndex = this.getItemIndex(params.target); 2476 } 2477 2478 params.customTime = (params.target === dom.customTime || 2479 params.target.parentNode === dom.customTime) ? 2480 this.customTime : 2481 undefined; 2482 2483 params.addItem = (options.editable && event.ctrlKey); 2484 if (params.addItem) { 2485 // create a new event at the current mouse position 2486 var x = params.mouseX - params.frameLeft; 2487 var y = params.mouseY - params.frameTop; 2488 2489 var xstart = this.screenToTime(x); 2490 if (options.snapEvents) { 2491 this.step.snap(xstart); 2492 } 2493 var xend = new Date(xstart); 2494 var content = "New"; 2495 var group = this.getGroupFromHeight(y); 2496 this.addItem({ 2497 'start': xstart, 2498 'end': xend, 2499 'content': content, 2500 'group': group 2501 }); 2502 params.itemIndex = (this.items.length - 1); 2503 this.selectItem(params.itemIndex); 2504 params.itemDragRight = true; 2505 } 2506 2507 params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined; 2508 if (params.editItem) { 2509 var item = this.items[params.itemIndex]; 2510 params.itemStart = item.start; 2511 params.itemEnd = item.end; 2512 params.itemType = item.type; 2513 if (params.itemType == 'range') { 2514 params.itemLeft = this.timeToScreen(item.start); 2515 params.itemRight = this.timeToScreen(item.end); 2516 } 2517 else { 2518 params.itemLeft = this.timeToScreen(item.start); 2519 } 2520 } 2521 else { 2522 this.dom.frame.style.cursor = 'move'; 2523 } 2524 if (!params.touchDown) { 2525 // add event listeners to handle moving the contents 2526 // we store the function onmousemove and onmouseup in the timeline, so we can 2527 // remove the eventlisteners lateron in the function mouseUp() 2528 var me = this; 2529 if (!params.onMouseMove) { 2530 params.onMouseMove = function (event) {me.onMouseMove(event);}; 2531 links.Timeline.addEventListener(document, "mousemove", params.onMouseMove); 2532 } 2533 if (!params.onMouseUp) { 2534 params.onMouseUp = function (event) {me.onMouseUp(event);}; 2535 links.Timeline.addEventListener(document, "mouseup", params.onMouseUp); 2536 } 2537 } 2538 2539 links.Timeline.prevent ult(event); 2540 } 2541 2542 2543 /** 2544 * Perform moving operating. 2545 * This function activated from within the funcion links.Timeline.onMouseDown(). 2546 * @param {event} event Well, eehh, the event 2547 */ 2548 links.Timeline.prototype.onMouseMove = function (event) { 2549 event = event || window.event; 2550 2551 var params = this.eventParams, 2552 size = this.size, 2553 dom = this.dom, 2554 options = this.options; 2555 2556 // calculate change in mouse position 2557 if (!params.touchDown) { 2558 var mouseX = event.clientX; 2559 var mouseY = event.clientY; 2560 } 2561 else { 2562 var mouseX = event.targetTouches[0].clientX; 2563 var mouseY = event.targetTouches[0].clientY; 2564 } 2565 if (mouseX === undefined) {mouseX = 0;} 2566 if (mouseY === undefined) {mouseY = 0;} 2567 var diffX = parseFloat(mouseX) - params.mouseX; 2568 var diffY = parseFloat(mouseY) - params.mouseY; 2569 2570 params.moved = true; 2571 2572 if (params.customTime) { 2573 var x = this.timeToScreen(params.customTime); 2574 var xnew = x + diffX; 2575 this.customTime = this.screenToTime(xnew); 2576 this.redrawCustomTime(); 2577 2578 // fire a timechange event 2579 this.trigger('timechange'); 2580 } 2581 else if (params.editItem) { 2582 var item = this.items[params.itemIndex], 2583 domItem = item.dom, 2584 left, 2585 right; 2586 2587 if (params.itemDragLeft) { 2588 // move the start of the item 2589 left = params.itemLeft + diffX; 2590 right = params.itemRight; 2591 2592 item.start = this.screenToTime(left); 2593 if (options.snapEvents) { 2594 this.step.snap(item.start); 2595 left = this.timeToScreen(item.start); 2596 } 2597 2598 if (left > right) { 2599 left = right; 2600 item.start = this.screenToTime(left); 2601 } 2602 } 2603 else if (params.itemDragRight) { 2604 // move the end of the item 2605 left = params.itemLeft; 2606 right = params.itemRight + diffX; 2607 2608 item.end = this.screenToTime(right); 2609 if (options.snapEvents) { 2610 this.step.snap(item.end); 2611 right = this.timeToScreen(item.end); 2612 } 2613 2614 if (right < left) { 2615 right = left; 2616 item.end = this.screenToTime(right); 2617 } 2618 } 2619 else { 2620 // move the item 2621 left = params.itemLeft + diffX; 2622 item.start = this.screenToTime(left); 2623 if (options.snapEvents) { 2624 this.step.snap(item.start); 2625 left = this.timeToScreen(item.start); 2626 } 2627 2628 if (item.end) { 2629 right = left + (params.itemRight - params.itemLeft); 2630 item.end = this.screenToTime(right); 2631 } 2632 } 2633 2634 switch(item.type) { 2635 case 'range': 2636 domItem.style.left = left + "px"; 2637 //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO 2638 domItem.style.width = Math.max(right - left, 1) + "px"; 2639 break; 2640 2641 case 'box': 2642 domItem.style.left = (left - item.width / 2) + "px"; 2643 domItem.line.style.left = (left - item.lineWidth / 2) + "px"; 2644 domItem.dot.style.left = (left - item.dotWidth / 2) + "px"; 2645 break; 2646 2647 case 'dot': 2648 domItem.style.left = (left - item.dotWidth / 2) + "px"; 2649 break; 2650 } 2651 2652 if (this.groups.length == 0) { 2653 // TODO: does not work well in FF, forces redraw with every mouse move it seems 2654 this.stackEvents(options.animate); 2655 if (!options.animate) { 2656 this.redrawFrame(); 2657 } 2658 // Note: when animate==true, no redraw is needed here, its done by stackEvents animation 2659 } 2660 this.redrawDeleteButton(); 2661 this.redrawDragAreas(); 2662 } 2663 else if (options.moveable) { 2664 var interval = (params.end.valueOf() - params.start.valueOf()); 2665 var diffMillisecs = parseFloat(-diffX) / size.contentWidth * interval; 2666 this.start = new Date(params.start.valueOf() + Math.round(diffMillisecs)); 2667 this.end = new Date(params.end.valueOf() + Math.round(diffMillisecs)); 2668 2669 this.recalcConversion(); 2670 2671 // move the items by changing the left position of their frame. 2672 // this is much faster than repositioning all elements individually via the 2673 // redrawFrame() function (which is done once at mouseup) 2674 // note that we round diffX to prevent wrong positioning on millisecond scale 2675 var diffXRounded = -Math.round(diffMillisecs) / interval * size.contentWidth; 2676 dom.items.frame.style.left = diffXRounded + "px"; 2677 2678 this.redrawCurrentTime(); 2679 this.redrawCustomTime(); 2680 this.redrawAxis(); 2681 2682 // fire a rangechange event 2683 this.trigger('rangechange'); 2684 } 2685 2686 links.Timeline.prevent ult(event); 2687 } 2688 2689 2690 /** 2691 * Stop moving operating. 2692 * This function activated from within the funcion links.Timeline.onMouseDown(). 2693 * @param {event} event The event 2694 */ 2695 links.Timeline.prototype.onMouseUp = function (event) { 2696 var params = this.eventParams, 2697 options = this.options; 2698 2699 event = event || window.event; 2700 2701 this.dom.frame.style.cursor = 'auto'; 2702 2703 // remove event listeners here, important for Safari 2704 if (params.onMouseMove) { 2705 links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove); 2706 delete params.onMouseMove; 2707 } 2708 if (params.onMouseUp) { 2709 links.Timeline.removeEventListener(document, "mouseup", params.onMouseUp); 2710 delete params.onMouseUp; 2711 } 2712 links.Timeline.prevent ult(event); 2713 2714 if (params.customTime) { 2715 // fire a timechanged event 2716 this.trigger('timechanged'); 2717 } 2718 else if (params.editItem) { 2719 var item = this.items[params.itemIndex]; 2720 2721 if (params.moved || params.addItem) { 2722 this.applyChange = true; 2723 this.applyAdd = true; 2724 2725 this.updateData(params.itemIndex, { 2726 'start': item.start, 2727 'end': item.end 2728 }); 2729 2730 // fire an add or change event. 2731 // Note that the change can be canceled from within an event listener if 2732 // this listener calls the method cancelChange(). 2733 this.trigger(params.addItem ? 'add' : 'change'); 2734 2735 if (params.addItem) { 2736 if (this.applyAdd) { 2737 this.updateData(params.itemIndex, { 2738 'start': item.start, 2739 'end': item.end, 2740 'content': item.content, 2741 'group': item.group ? item.group.content : undefined 2742 }); 2743 } 2744 else { 2745 // undo an add 2746 this.deleteItem(params.itemIndex); 2747 } 2748 } 2749 else { 2750 if (this.applyChange) { 2751 this.updateData(params.itemIndex, { 2752 'start': item.start, 2753 'end': item.end 2754 }); 2755 } 2756 else { 2757 // undo a change 2758 delete this.applyChange; 2759 delete this.applyAdd; 2760 2761 var item = this.items[params.itemIndex], 2762 domItem = item.dom; 2763 2764 item.start = params.itemStart; 2765 item.end = params.itemEnd; 2766 domItem.style.left = params.itemLeft + "px"; 2767 domItem.style.width = (params.itemRight - params.itemLeft) + "px"; 2768 } 2769 } 2770 2771 this.recalcSize(); 2772 this.stackEvents(options.animate); 2773 if (!options.animate) { 2774 this.redrawFrame(); 2775 } 2776 this.redrawDeleteButton(); 2777 this.redrawDragAreas(); 2778 } 2779 } 2780 else { 2781 if (!params.moved && !params.zoomed) { 2782 // mouse did not move -> user has selected an item 2783 2784 // delete item 2785 if (options.editable) { 2786 if (params.target === this.dom.items.deleteButton) { 2787 if (this.selection) { 2788 this.confirmDeleteItem(this.selection.index); 2789 } 2790 } 2791 } 2792 2793 // select/unselect item 2794 if (options.selectable) { 2795 if (params.itemIndex !== undefined) { 2796 if (!this.isSelected(params.itemIndex)) { 2797 this.selectItem(params.itemIndex); 2798 this.trigger('select'); 2799 } 2800 } 2801 else { 2802 this.unselectItem(); 2803 } 2804 } 2805 2806 this.redrawFrame(); 2807 } 2808 else { 2809 // timeline is moved 2810 this.dom.items.frame.style.left = "0px"; 2811 this.redrawFrame(); 2812 2813 if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) { 2814 // fire a rangechanged event 2815 this.trigger('rangechanged'); 2816 } 2817 } 2818 } 2819 } 2820 2821 /** 2822 * Double click event occurred for an item 2823 * @param {event} event 2824 */ 2825 links.Timeline.prototype.onDblClick = function (event) { 2826 var params = this.eventParams, 2827 options = this.options, 2828 dom = this.dom, 2829 size = this.size; 2830 event = event || window.event; 2831 2832 if (!options.editable) { 2833 return; 2834 } 2835 2836 if (params.itemIndex !== undefined) { 2837 // fire the edit event 2838 this.trigger('edit'); 2839 } 2840 else { 2841 // create a new item 2842 var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content); 2843 var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content); 2844 2845 // create a new event at the current mouse position 2846 var xstart = this.screenToTime(x); 2847 var xend = this.screenToTime(x + size.frameWidth / 10); // add 10% of timeline width 2848 if (options.snapEvents) { 2849 this.step.snap(xstart); 2850 this.step.snap(xend); 2851 } 2852 2853 var content = "New"; 2854 var group = this.getGroupFromHeight(y); // (group may be undefined) 2855 this.addItem({ 2856 'start': xstart, 2857 'end': xend, 2858 'content': content, 2859 'group': group 2860 }); 2861 params.itemIndex = (this.items.length - 1); 2862 this.selectItem(params.itemIndex); 2863 2864 this.applyAdd = true; 2865 2866 // fire an add event. 2867 // Note that the change can be canceled from within an event listener if 2868 // this listener calls the method cancelAdd(). 2869 this.trigger('add'); 2870 2871 if (!this.applyAdd) { 2872 // undo an add 2873 this.deleteItem(params.itemIndex); 2874 } 2875 2876 this.redrawDeleteButton(); 2877 this.redrawDragAreas(); 2878 } 2879 2880 links.Timeline.prevent ult(event); 2881 } 2882 2883 2884 /** 2885 * Event handler for mouse wheel event, used to zoom the timeline 2886 * Code from http://adomas.org/javascript-mouse-wheel/ 2887 * @param {event} event The event 2888 */ 2889 links.Timeline.prototype.onMouseWheel = function(event) { 2890 if (!this.options.zoomable) 2891 return; 2892 2893 if (!event) { /* For IE. */ 2894 event = window.event; 2895 } 2896 2897 // retrieve delta 2898 var delta = 0; 2899 if (event.wheelDelta) { /* IE/Opera. */ 2900 delta = event.wheelDelta/120; 2901 } else if (event.detail) { /* Mozilla case. */ 2902 // In Mozilla, sign of delta is different than in IE. 2903 // Also, delta is multiple of 3. 2904 delta = -event.detail/3; 2905 } 2906 2907 // If delta is nonzero, handle it. 2908 // Basically, delta is now positive if wheel was scrolled up, 2909 // and negative, if wheel was scrolled down. 2910 if (delta) { 2911 // TODO: on FireFox, the window is not redrawn within repeated scroll-events 2912 // -> use a delayed redraw? Make a zoom queue? 2913 2914 var timeline = this; 2915 var z = function () { 2916 // check if frame is not resized (causing a mismatch with the end date) 2917 timeline.recalcSize(); 2918 2919 // perform the zoom action. Delta is normally 1 or -1 2920 var zoomFactor = delta / 5.0; 2921 var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content); 2922 var zoomAroundDate = 2923 (event.clientX != undefined && frameLeft != undefined) ? 2924 timeline.screenToTime(event.clientX - frameLeft) : 2925 undefined; 2926 2927 timeline.zoom(zoomFactor, zoomAroundDate); 2928 2929 // fire a rangechange and a rangechanged event 2930 timeline.trigger("rangechange"); 2931 timeline.trigger("rangechanged"); 2932 2933 timeline.zooming = false; 2934 }; 2935 2936 z(); 2937 /* 2938 if (!timeline.zooming) { 2939 timeline.zooming = true; 2940 setTimeout(z, 30); 2941 } 2942 */ 2943 } 2944 2945 // Prevent default actions caused by mouse wheel. 2946 // That might be ugly, but we handle scrolls somehow 2947 // anyway, so don't bother here... 2948 links.Timeline.prevent ult(event); 2949 } 2950 2951 2952 /** 2953 * Zoom the timeline the given zoomfactor in or out. Start and end date will 2954 * be adjusted, and the timeline will be redrawn. You can optionally give a 2955 * date around which to zoom. 2956 * For example, try zoomfactor = 0.1 or -0.1 2957 * @param {float} zoomFactor Zooming amount. Positive value will zoom in, 2958 * negative value will zoom out 2959 * @param {Date} zoomAroundDate Date around which will be zoomed. Optional 2960 */ 2961 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) { 2962 // if zoomAroundDate is not provided, take it half between start Date and end Date 2963 if (zoomAroundDate == undefined) { 2964 zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2); 2965 } 2966 2967 // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will 2968 // result in a start>=end ) 2969 if (zoomFactor >= 1) { 2970 zoomFactor = 0.9; 2971 } 2972 if (zoomFactor <= -1) { 2973 zoomFactor = -0.9; 2974 } 2975 2976 // adjust a negative factor such that zooming in with 0.1 equals zooming 2977 // out with a factor -0.1 2978 if (zoomFactor < 0) { 2979 zoomFactor = zoomFactor / (1 + zoomFactor); 2980 } 2981 2982 // zoom start Date and end Date relative to the zoomAroundDate 2983 var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf()); 2984 var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf()); 2985 2986 // calculate new dates 2987 var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor); 2988 var newEnd = new Date(this.end.valueOf() - endDiff * zoomFactor); 2989 2990 // prevent scale of less than 10 milliseconds 2991 // TODO: IE has problems with milliseconds 2992 if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10) { 2993 return; 2994 } 2995 2996 // prevent scale of mroe than than 10 thousand years 2997 if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000) { 2998 return; 2999 } 3000 3001 // apply new dates 3002 this.start = newStart; 3003 this.end = newEnd; 3004 3005 this.recalcSize(); 3006 var animate = this.options.animate ? this.options.animateZoom : false; 3007 this.stackEvents(animate); 3008 if (!animate || this.groups.length > 0) { 3009 this.redrawFrame(); 3010 } 3011 } 3012 3013 3014 /** 3015 * Move the timeline the given movefactor to the left or right. Start and end 3016 * date will be adjusted, and the timeline will be redrawn. 3017 * For example, try moveFactor = 0.1 or -0.1 3018 * @param {float} moveFactor Moving amount. Positive value will move right, 3019 * negative value will move left 3020 */ 3021 links.Timeline.prototype.move = function(moveFactor) { 3022 // zoom start Date and end Date relative to the zoomAroundDate 3023 var diff = parseFloat(this.end.valueOf() - this.start.valueOf()); 3024 3025 // apply new dates 3026 this.start = new Date(this.start.valueOf() + diff * moveFactor); 3027 this.end = new Date(this.end.valueOf() + diff * moveFactor); 3028 3029 this.recalcConversion(); 3030 this.redrawFrame(); 3031 } 3032 3033 /** 3034 * Delete an item after a confirmation. 3035 * The deletion can be cancelled by executing .cancelDelete() during the 3036 * triggered event 'delete'. 3037 * @param {int} index Index of the item to be deleted 3038 */ 3039 links.Timeline.prototype.confirmDeleteItem = function(index) { 3040 this.applyDelete = true; 3041 3042 // select the event to be deleted 3043 if (!this.isSelected(index)) { 3044 this.selectItem(index); 3045 } 3046 3047 // fire a delete event trigger. 3048 // Note that the delete event can be canceled from within an event listener if 3049 // this listener calls the method cancelChange(). 3050 this.trigger('delete'); 3051 3052 if (this.applyDelete) { 3053 this.deleteItem(index); 3054 } 3055 3056 delete this.applyDelete; 3057 } 3058 3059 /** 3060 * Delete an item 3061 * @param {int} index Index of the item to be deleted 3062 */ 3063 links.Timeline.prototype.deleteItem = function(index) { 3064 if (index >= this.items.length) { 3065 throw "Cannot delete row, index out of range"; 3066 } 3067 3068 this.unselectItem(); 3069 3070 // actually delete the item 3071 this.items.splice(index, 1); 3072 3073 // delete the row in the original data table 3074 if (this.data) { 3075 if (google && google.visualization && google.visualization.DataTable && 3076 this.data instanceof google.visualization.DataTable) { 3077 this.data.removeRow(index); 3078 } 3079 else if (this.data instanceof Array) { 3080 this.data.splice(index, 1); 3081 } 3082 else { 3083 throw "Cannot delete row from data, unknown data type"; 3084 } 3085 } 3086 3087 this.size.dataChanged = true; 3088 this.redrawFrame(); 3089 this.recalcSize(); 3090 this.stackEvents(this.options.animate); 3091 if (!this.options.animate) { 3092 this.redrawFrame(); 3093 } 3094 this.size.dataChanged = false; 3095 } 3096 3097 3098 /** 3099 * Delete all items 3100 */ 3101 links.Timeline.prototype.deleteAllItems = function() { 3102 this.unselectItem(); 3103 3104 // delete the loaded data 3105 this.items = []; 3106 3107 // delete the groups 3108 this.deleteGroups(); 3109 3110 // empty original data table 3111 if (this.data) { 3112 if (google && google.visualization && google.visualization.DataTable && 3113 this.data instanceof google.visualization.DataTable) { 3114 this.data.removeRows(0, this.data.getNumberOfRows()); 3115 } 3116 else if (this.data instanceof Array) { 3117 this.data.splice(0, this.data.length); 3118 } 3119 else { 3120 throw "Cannot delete row from data, unknown data type"; 3121 } 3122 } 3123 3124 this.size.dataChanged = true; 3125 this.redrawFrame(); 3126 this.recalcSize(); 3127 this.stackEvents(this.options.animate); 3128 if (!this.options.animate) { 3129 this.redrawFrame(); 3130 } 3131 this.size.dataChanged = false; 3132 3133 } 3134 3135 3136 /** 3137 * Find the group from a given height in the timeline 3138 * @param {Number} height Height in the timeline 3139 * @return {String} group The group name, or undefined if out of range 3140 */ 3141 links.Timeline.prototype.getGroupFromHeight = function(height) { 3142 var groups = this.groups, 3143 options = this.options, 3144 size = this.size, 3145 y = height - (options.axisOnTop ? size.axis.height : 0); 3146 3147 if (groups) { 3148 var group; 3149 for (var i = 0, iMax = groups.length; i < iMax; i++) { 3150 group = groups[i]; 3151 if (y > group.top && y < group.top + group.height) { 3152 return group.content; 3153 } 3154 } 3155 3156 return group ? group.content : undefined; // return the last group 3157 } 3158 3159 return undefined; 3160 } 3161 3162 /** 3163 * Retrieve the properties of an item. 3164 * @param {Number} index 3165 * @return {Object} properties Object containing item properties:<br> 3166 * {Date} start (required), 3167 * {Date} end (optional), 3168 * {String} content (required), 3169 * {String} group (optional) 3170 */ 3171 links.Timeline.prototype.getItem = function (index) { 3172 if (index >= this.items.length) { 3173 throw "Cannot get item, index out of range"; 3174 } 3175 3176 var item = this.items[index]; 3177 3178 var properties = {}; 3179 properties.start = new Date(item.start); 3180 if (item.end) { 3181 properties.end = new Date(item.end); 3182 } 3183 properties.content = item.content; 3184 if (item.group) { 3185 properties.group = item.group.content; 3186 } 3187 3188 return properties; 3189 } 3190 3191 /** 3192 * Add a new item. 3193 * @param {Object} itemData Object containing item properties:<br> 3194 * {Date} start (required), 3195 * {Date} end (optional), 3196 * {String} content (required), 3197 * {String} group (optional) 3198 */ 3199 links.Timeline.prototype.addItem = function (itemData) { 3200 var items = [ 3201 itemData 3202 ]; 3203 3204 this.addItems(items); 3205 } 3206 3207 /** 3208 * Add new items. 3209 * @param {Array} items An array containing Objects. 3210 * The objects must have the following parameters: 3211 * {Date} start, 3212 * {Date} end, 3213 * {String} content with text or HTML code, 3214 * {String} group 3215 */ 3216 links.Timeline.prototype.addItems = function (items) { 3217 var newItems = items, 3218 curItems = this.items, 3219 groups = this.groups, 3220 groupIndexes = this.groupIndexes; 3221 3222 // append the items 3223 for (var i = 0, iMax = newItems.length; i < iMax; i++) { 3224 var itemData = items[i]; 3225 3226 this.addGroup(itemData.group); 3227 3228 curItems.push(this.createItem(itemData)); 3229 3230 var index = curItems.length - 1; 3231 this.updateData(index, itemData); 3232 } 3233 3234 // redraw timeline 3235 this.size.dataChanged = true; 3236 this.redrawFrame(); 3237 this.recalcSize(); 3238 this.stackEvents(false); 3239 this.redrawFrame(); 3240 this.size.dataChanged = false; 3241 } 3242 3243 /** 3244 * Create an item object, containing all needed parameters 3245 * @param {Object} itemData Object containing parameters start, end 3246 * content, group. 3247 * @return {Object} item 3248 */ 3249 links.Timeline.prototype.createItem = function(itemData) { 3250 var item = { 3251 'start': itemData.start, 3252 'end': itemData.end, 3253 'content': itemData.content, 3254 'type': itemData.end ? 'range' : this.options.style, 3255 'group': this.findGroup(itemData.group), 3256 'top': 0, 3257 'left': 0, 3258 'width': 0, 3259 'height': 0, 3260 'lineWidth' : 0, 3261 'dotWidth': 0, 3262 'dotHeight': 0 3263 }; 3264 return item; 3265 } 3266 3267 /** 3268 * Edit an item 3269 * @param {Number} index 3270 * @param {Object} itemData Object containing item properties:<br> 3271 * {Date} start (required), 3272 * {Date} end (optional), 3273 * {String} content (required), 3274 * {String} group (optional) 3275 */ 3276 links.Timeline.prototype.changeItem = function (index, itemData) { 3277 if (index >= this.items.length) { 3278 throw "Cannot change item, index out of range"; 3279 } 3280 3281 var style = this.options.style; 3282 var item = this.items[index]; 3283 3284 // edit the item 3285 if (itemData.start) { 3286 item.start = itemData.start; 3287 } 3288 if (itemData.end) { 3289 item.end = itemData.end; 3290 } 3291 if (itemData.content) { 3292 item.content = itemData.content; 3293 } 3294 if (itemData.group) { 3295 item.group = this.addGroup(itemData.group); 3296 } 3297 3298 // update the original data table 3299 this.updateData(index, itemData); 3300 3301 // redraw timeline 3302 this.size.dataChanged = true; 3303 this.redrawFrame(); 3304 this.recalcSize(); 3305 this.stackEvents(false); 3306 this.redrawFrame(); 3307 this.size.dataChanged = false; 3308 } 3309 3310 3311 /** 3312 * Find a group by its name. 3313 * @param {String} group 3314 * @return {Object} a group object or undefined when group is not found 3315 */ 3316 links.Timeline.prototype.findGroup = function (group) { 3317 var index = this.groupIndexes[group]; 3318 return (index != undefined) ? this.groups[index] : undefined; 3319 } 3320 3321 /** 3322 * Delete all groups 3323 */ 3324 links.Timeline.prototype.deleteGroups = function () { 3325 this.groups = []; 3326 this.groupIndexes = {}; 3327 } 3328 3329 3330 /** 3331 * Add a group. When the group already exists, no new group is created 3332 * but the existing group is returned. 3333 * @param {String} groupName the name of the group 3334 * @return {Object} groupObject 3335 */ 3336 links.Timeline.prototype.addGroup = function (groupName) { 3337 var groups = this.groups, 3338 groupIndexes = this.groupIndexes; 3339 3340 var groupObj = groupIndexes[groupName]; 3341 if (groupObj === undefined && groupName !== undefined) { 3342 var groupObj = { 3343 'content': groupName, 3344 'labelTop': 0, 3345 'lineTop': 0 3346 // note: this object will lateron get addition information, 3347 // such as height and width of the group 3348 }; 3349 groups.push(groupObj); 3350 3351 // sort the groups 3352 if (this.options.axisOnTop) { 3353 groups.sort(function (a, b) { 3354 return a.content > b.content; 3355 }); 3356 } 3357 else { 3358 groups.sort(function (a, b) { 3359 return a.content < b.content; 3360 }); 3361 } 3362 3363 // rebuilt the groupIndexes 3364 for (var i = 0, iMax = groups.length; i < iMax; i++) { 3365 groupIndexes[groups[i].content] = i; 3366 } 3367 } 3368 3369 return groupObj; 3370 } 3371 3372 /** 3373 * Cancel a change item 3374 * This method can be called insed an event listener which catches the "change" 3375 * event. The changed event position will be undone. 3376 */ 3377 links.Timeline.prototype.cancelChange = function () { 3378 this.applyChange = false; 3379 } 3380 3381 /** 3382 * Cancel deletion of an item 3383 * This method can be called insed an event listener which catches the "delete" 3384 * event. Deletion of the event will be undone. 3385 */ 3386 links.Timeline.prototype.cancelDelete = function () { 3387 this.applyDelete = false; 3388 } 3389 3390 3391 /** 3392 * Cancel creation of a new item 3393 * This method can be called insed an event listener which catches the "new" 3394 * event. Creation of the new the event will be undone. 3395 */ 3396 links.Timeline.prototype.cancelAdd = function () { 3397 this.applyAdd = false; 3398 } 3399 3400 3401 /** 3402 * Select an event. The visible chart range will be moved such that the selected 3403 * event is placed in the middle. 3404 * For example selection = [{row: 5}]; 3405 * @param {array} sel An array with a column row, containing the row number 3406 * (the id) of the event to be selected. 3407 * @return {boolean} true if selection is succesfully set, else false. 3408 */ 3409 links.Timeline.prototype.setSelection = function(selection) { 3410 if (selection != undefined && selection.length > 0) { 3411 if (selection[0].row != undefined) { 3412 var index = selection[0].row; 3413 if (this.items[index]) { 3414 var item = this.items[index]; 3415 this.selectItem(index); 3416 3417 // move the visible chart range to the selected event. 3418 var start = item.start; 3419 var end = item.end; 3420 if (end != undefined) { 3421 var middle = new Date((end.valueOf() + start.valueOf()) / 2); 3422 } else { 3423 var middle = new Date(start); 3424 } 3425 var diff = (this.end.valueOf() - this.start.valueOf()), 3426 newStart = new Date(middle.valueOf() - diff/2), 3427 newEnd = new Date(middle.valueOf() + diff/2); 3428 3429 this.setVisibleChartRange(newStart, newEnd); 3430 3431 return true; 3432 } 3433 } 3434 } 3435 return false; 3436 } 3437 3438 /** 3439 * Retrieve the currently selected event 3440 * @return {array} sel An array with a column row, containing the row number 3441 * of the selected event. If there is no selection, an 3442 * empty array is returned. 3443 */ 3444 links.Timeline.prototype.getSelection = function() { 3445 var sel = []; 3446 if (this.selection) { 3447 sel.push({"row": this.selection.index}); 3448 } 3449 return sel; 3450 } 3451 3452 3453 /** 3454 * Select an item by its index 3455 * @param {Number} index 3456 */ 3457 links.Timeline.prototype.selectItem = function(index) { 3458 this.unselectItem(); 3459 3460 this.selection = undefined; 3461 3462 if (this.items[index] !== undefined) { 3463 var item = this.items[index], 3464 domItem = item.dom; 3465 3466 this.selection = { 3467 'index': index, 3468 'item': domItem 3469 }; 3470 3471 if (this.options.editable) { 3472 domItem.style.cursor = 'move'; 3473 } 3474 switch (item.type) { 3475 case 'range': 3476 domItem.className = "timeline-event timeline-event-selected timeline-event-range"; 3477 break; 3478 case 'box': 3479 domItem.className = "timeline-event timeline-event-selected timeline-event-box"; 3480 domItem.line.className = "timeline-event timeline-event-selected timeline-event-line"; 3481 domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot"; 3482 break; 3483 case 'dot': 3484 domItem.className = "timeline-event timeline-event-selected"; 3485 domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot"; 3486 break; 3487 } 3488 } 3489 } 3490 3491 /** 3492 * Check if an item is currently selected 3493 * @param {Number} index 3494 * @return {boolean} true if row is selected, else false 3495 */ 3496 links.Timeline.prototype.isSelected = function (index) { 3497 return (this.selection && this.selection.index === index); 3498 } 3499 3500 /** 3501 * Unselect the currently selected event (if any) 3502 */ 3503 links.Timeline.prototype.unselectItem = function() { 3504 if (this.selection) { 3505 var item = this.items[this.selection.index]; 3506 3507 if (item && item.dom) { 3508 var domItem = item.dom; 3509 domItem.style.cursor = ''; 3510 switch (item.type) { 3511 case 'range': 3512 domItem.className = "timeline-event timeline-event-range"; 3513 break; 3514 case 'box': 3515 domItem.className = "timeline-event timeline-event-box"; 3516 domItem.line.className = "timeline-event timeline-event-line"; 3517 domItem.dot.className = "timeline-event timeline-event-dot"; 3518 break; 3519 case 'dot': 3520 domItem.className = ""; 3521 domItem.dot.className = "timeline-event timeline-event-dot"; 3522 break; 3523 } 3524 } 3525 } 3526 3527 this.selection = undefined; 3528 } 3529 3530 3531 /** 3532 * Stack the items such that they don't overlap. The items will have a minimal 3533 * distance equal to options.eventMargin. 3534 * @param {boolean} animate if animate is true, the items are moved to 3535 * their new position animated 3536 */ 3537 links.Timeline.prototype.stackEvents = function(animate) { 3538 if (this.options.stackEvents == false || this.groups.length > 0) { 3539 // under this conditions we refuse to stack the events 3540 return; 3541 } 3542 3543 if (animate == undefined) { 3544 animate = false; 3545 } 3546 3547 var sortedItems = this.stackOrder(this.items); 3548 var finalItems = this.stackCalculateFinal(sortedItems, animate); 3549 3550 if (animate) { 3551 // move animated to the final positions 3552 var animation = this.animation; 3553 if (!animation) { 3554 animation = {}; 3555 this.animation = animation; 3556 } 3557 animation.finalItems = finalItems; 3558 3559 var timeline = this; 3560 var step = function () { 3561 var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems); 3562 timeline.recalcSize(); 3563 timeline.redrawFrame(); 3564 3565 if (!arrived) { 3566 animation.timer = setTimeout(step, 30); 3567 } 3568 else { 3569 delete animation.finalItems; 3570 delete animation.timer; 3571 } 3572 } 3573 3574 if (!animation.timer) { 3575 animation.timer = setTimeout(step, 30); 3576 } 3577 } 3578 else { 3579 this.stackMoveToFinal(sortedItems, finalItems); 3580 this.recalcSize(); 3581 //this.redraw(); // TODO: cleanup 3582 } 3583 } 3584 3585 3586 /** 3587 * Order the items in the array this.items. The order is determined via: 3588 * - Ranges go before boxes and dots. 3589 * - The item with the left most location goes first 3590 * @param {Array} items Array with items 3591 * @return {Array} sortedItems Array with sorted items 3592 */ 3593 links.Timeline.prototype.stackOrder = function(items) { 3594 // TODO: store the sorted items, to have less work later on 3595 var sortedItems = items.concat([]); 3596 3597 var f = function (a, b) { 3598 if (a.type == 'range' && b.type != 'range') { 3599 return -1; 3600 } 3601 3602 if (a.type != 'range' && b.type == 'range') { 3603 return 1; 3604 } 3605 3606 return (a.left - b.left); 3607 }; 3608 3609 sortedItems.sort(f); 3610 3611 return sortedItems; 3612 } 3613 3614 /** 3615 * Adjust vertical positions of the events such that they don't overlap each 3616 * other. 3617 */ 3618 links.Timeline.prototype.stackCalculateFinal = function(items) { 3619 var size = this.size, 3620 axisTop = size.axis.top, 3621 options = this.options, 3622 axisOnTop = options.axisOnTop, 3623 eventMargin = options.eventMargin, 3624 eventMarginAxis = options.eventMarginAxis, 3625 finalItems = []; 3626 3627 // initialize final positions 3628 for (var i = 0, iMax = items.length; i < iMax; i++) { 3629 var item = items[i], 3630 top, 3631 left, 3632 right, 3633 bottom, 3634 height = item.height, 3635 width = item.width; 3636 3637 if (axisOnTop) { 3638 top = axisTop + eventMarginAxis + eventMargin / 2; 3639 } 3640 else { 3641 top = axisTop - height - eventMarginAxis - eventMargin / 2; 3642 } 3643 bottom = top + height; 3644 3645 switch (item.type) { 3646 case 'range': 3647 case 'dot': 3648 left = this.timeToScreen(item.start); 3649 right = item.end ? this.timeToScreen(item.end) : left + width; 3650 break; 3651 3652 case 'box': 3653 left = this.timeToScreen(item.start) - width / 2; 3654 right = left + width; 3655 break; 3656 } 3657 3658 finalItems[i] = { 3659 'left': left, 3660 'top': top, 3661 'right': right, 3662 'bottom': bottom, 3663 'height': height, 3664 'item': item 3665 }; 3666 } 3667 3668 // calculate new, non-overlapping positions 3669 //var items = sortedItems; 3670 for (var i = 0, iMax = finalItems.length; i < iMax; i++) { 3671 var finalItem = finalItems[i]; 3672 var collidingElement = null; 3673 do { 3674 // TODO: optimize checking for overlap. when there is a gap without items, 3675 // you only need to check for items from the next item on, not from zero 3676 collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1); 3677 if (collidingItem != null) { 3678 // There is a collision. Reposition the event above the colliding element 3679 if (axisOnTop) { 3680 finalItem.top = collidingItem.top + collidingItem.height + eventMargin; 3681 } 3682 else { 3683 finalItem.top = collidingItem.top - finalItem.height - eventMargin; 3684 } 3685 finalItem.bottom = finalItem.top + finalItem.height; 3686 } 3687 } while (collidingItem); 3688 } 3689 3690 return finalItems; 3691 } 3692 3693 3694 /** 3695 * Move the events one step in the direction of their final positions 3696 * @param {Array} currentItems Array with the real items and their current 3697 * positions 3698 * @param {Array} finalItems Array with objects containing the final 3699 * positions of the items 3700 * @return {boolean} arrived True if all items have reached their final 3701 * location, else false 3702 */ 3703 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) { 3704 // TODO: check this method 3705 var arrived = true; 3706 3707 // apply new positions animated 3708 for (i = 0, iMax = currentItems.length; i < iMax; i++) { 3709 var finalItem = finalItems[i], 3710 item = finalItem.item; 3711 3712 var topNow = parseInt(item.top); 3713 var topFinal = parseInt(finalItem.top); 3714 var diff = (topFinal - topNow); 3715 if (diff) { 3716 var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1); 3717 if (Math.abs(diff) > 4) step = diff / 4; 3718 var topNew = parseInt(topNow + step); 3719 3720 if (topNew != topFinal) { 3721 arrived = false; 3722 } 3723 3724 item.top = topNew; 3725 item.bottom = item.top + item.height; 3726 } 3727 else { 3728 item.top = finalItem.top; 3729 item.bottom = finalItem.bottom; 3730 } 3731 3732 item.left = finalItem.left; 3733 item.right = finalItem.right; 3734 } 3735 3736 return arrived; 3737 } 3738 3739 3740 3741 /** 3742 * Move the events from their current position to the final position 3743 * @param {Array} currentItems Array with the real items and their current 3744 * positions 3745 * @param {Array} finalItems Array with objects containing the final 3746 * positions of the items 3747 */ 3748 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) { 3749 // Put the events directly at there final position 3750 for (i = 0, iMax = currentItems.length; i < iMax; i++) { 3751 var current = currentItems[i], 3752 finalItem = finalItems[i]; 3753 3754 current.left = finalItem.left; 3755 current.top = finalItem.top; 3756 current.right = finalItem.right; 3757 current.bottom = finalItem.bottom; 3758 } 3759 } 3760 3761 3762 3763 /** 3764 * Check if the destiny position of given item overlaps with any 3765 * of the other items from index itemStart to itemEnd. 3766 * @param {Array} items Array with items 3767 * @param {int} itemIndex Number of the item to be checked for overlap 3768 * @param {int} itemStart First item to be checked. 3769 * @param {int} itemEnd Last item to be checked. 3770 * @return {Object} colliding item, or undefined when no collisions 3771 */ 3772 links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex, 3773 itemStart, itemEnd) { 3774 eventMargin = this.options.eventMargin, 3775 collision = this.collision; 3776 3777 var item1 = items[itemIndex]; 3778 for (var i = itemStart; i <= itemEnd; i++) { 3779 var item2 = items[i]; 3780 if (collision(item1, item2, eventMargin)) { 3781 if (i != itemIndex) { 3782 return item2; 3783 } 3784 } 3785 } 3786 } 3787 3788 /** 3789 * Test if the two provided items collide 3790 * The items must have parameters left, right, top, and bottom. 3791 * @param {htmlelement} item1 The first item 3792 * @param {htmlelement} item2 The second item 3793 * @param {int} margin A minimum required margin. Optional. 3794 * If margin is provided, the two items will be 3795 * marked colliding when they overlap or 3796 * when the margin between the two is smaller than 3797 * the requested margin. 3798 * @return {boolean} true if item1 and item2 collide, else false 3799 */ 3800 links.Timeline.prototype.collision = function(item1, item2, margin) { 3801 // set margin if not specified 3802 if (margin == undefined) { 3803 margin = 0; 3804 } 3805 3806 // calculate if there is overlap (collision) 3807 return (item1.left - margin < item2.right && 3808 item1.right + margin > item2.left && 3809 item1.top - margin < item2.bottom && 3810 item1.bottom + margin > item2.top); 3811 } 3812 3813 3814 /** 3815 * fire an event 3816 * @param {String} event The name of an event, for example "rangechange" or "edit" 3817 */ 3818 links.Timeline.prototype.trigger = function (event) { 3819 // built up properties 3820 var properties = null; 3821 switch (event) { 3822 case 'rangechange': 3823 case 'rangechanged': 3824 properties = { 3825 'start': new Date(this.start), 3826 'end': new Date(this.end) 3827 }; 3828 break; 3829 3830 case 'timechange': 3831 case 'timechanged': 3832 properties = { 3833 'time': new Date(this.customTime) 3834 }; 3835 break; 3836 } 3837 3838 // trigger the links event bus 3839 links.events.trigger(this, event, properties); 3840 3841 // trigger the google event bus 3842 if (google && google.visualization && google.visualization.events) { 3843 google.visualization.events.trigger(this, event, properties); 3844 } 3845 } 3846 3847 3848 3849 /** ------------------------------------------------------------------------ **/ 3850 3851 3852 /** 3853 * Event listener (singleton) 3854 */ 3855 links.events = { 3856 'listeners': {}, 3857 3858 /** 3859 * Add an event listener 3860 * @param {Object} object 3861 * @param {String} event The name of an event, for example 'select' 3862 * @param {function} callback The callback method, called when the 3863 * event takes place 3864 */ 3865 addListener: function (object, event, callback) { 3866 var objListeners = this.listeners[object]; 3867 if (!objListeners) { 3868 objListeners = {}; 3869 this.listeners[object] = objListeners; 3870 } 3871 3872 var callbacks = objListeners[event]; 3873 if (!callbacks) { 3874 callbacks = []; 3875 objListeners[event] = callbacks; 3876 } 3877 3878 // add the callback if it does not yet exist 3879 if (callbacks.indexOf(callback) == -1) { 3880 callbacks.push(callback); 3881 } 3882 }, 3883 3884 /** 3885 * Remove an event listener 3886 * @param {Object} object 3887 * @param {String} event The name of an event, for example 'select' 3888 * @param {function} callback The registered callback method 3889 */ 3890 removeListener: function (object, event, callback) { 3891 var objListeners = this.listeners[object]; 3892 if (objListeners) { 3893 var callbacks = objListeners[event]; 3894 if (callbacks) { 3895 var index = callbacks.indexOf(callback); 3896 if (index != -1) { 3897 callbacks.splice(index, 1); 3898 } 3899 3900 // remove the array when empty 3901 if (callbacks.length == 0) { 3902 delete objListeners[event]; 3903 } 3904 } 3905 3906 // count the number of registered events. remove object when empty 3907 var count = 0; 3908 for (var event in objListeners) { 3909 if (objListeners.hasOwnProperty(event)) { 3910 count++; 3911 } 3912 } 3913 if (count == 0) { 3914 delete this.listeners[object]; 3915 } 3916 } 3917 }, 3918 3919 /** 3920 * Remove all registered event listeners 3921 */ 3922 removeAllListeners: function () { 3923 this.listeners = {}; 3924 }, 3925 3926 /** 3927 * Trigger an event. All registered event handlers will be called 3928 * @param {Object} object 3929 * @param {String} event 3930 * @param {Object} properties (optional) 3931 */ 3932 trigger: function (object, event, properties) { 3933 var objListeners = this.listeners[object]; 3934 if (objListeners) { 3935 var callbacks = objListeners[event]; 3936 if (callbacks) { 3937 for (var i = 0, iMax = callbacks.length; i < iMax; i++) { 3938 callbacks[i](properties); 3939 } 3940 } 3941 } 3942 } 3943 }; 3944 3945 3946 3947 3948 /** ------------------------------------------------------------------------ **/ 3949 3950 /** 3951 * @class StepDate 3952 * The class StepDate is an iterator for dates. You provide a start date and an 3953 * end date. The class itself determines the best scale (step size) based on the 3954 * provided start Date, end Date, and minimumStep. 3955 * 3956 * If minimumStep is provided, the step size is chosen as close as possible 3957 * to the minimumStep but larger than minimumStep. If minimumStep is not 3958 * provided, the scale is set to 1 DAY. 3959 * The minimumStep should correspond with the onscreen size of about 6 characters 3960 * 3961 * Alternatively, you can set a scale by hand. 3962 * After creation, you can initialize the class by executing start(). Then you 3963 * can iterate from the start date to the end date via next(). You can check if 3964 * the end date is reached with the function end(). After each step, you can 3965 * retrieve the current date via get(). 3966 * The class step has scales ranging from milliseconds, seconds, minutes, hours, 3967 * days, to years. 3968 * 3969 * Version: 0.9 3970 * 3971 * @param {Date} start The start date, for example new Date(2010, 9, 21) 3972 * or new Date(2010, 9,21,23,45,00) 3973 * @param {Date} end The end date 3974 * @param {int} minimumStep Optional. Minimum step size in milliseconds 3975 */ 3976 links.Timeline.StepDate = function(start, end, minimumStep) { 3977 3978 // variables 3979 this.current = new Date(); 3980 this._start = new Date(); 3981 this._end = new Date(); 3982 3983 this.autoScale = true; 3984 this.scale = links.Timeline.StepDate.SCALE.DAY; 3985 this.step = 1; 3986 3987 // initialize the range 3988 this.setRange(start, end, minimumStep); 3989 } 3990 3991 /// enum scale 3992 links.Timeline.StepDate.SCALE = { MILLISECOND : 1, 3993 SECOND : 2, 3994 MINUTE : 3, 3995 HOUR : 4, 3996 DAY : 5, 3997 MONTH : 6, 3998 YEAR : 7}; 3999 4000 4001 /** 4002 * Set a new range 4003 * If minimumStep is provided, the step size is chosen as close as possible 4004 * to the minimumStep but larger than minimumStep. If minimumStep is not 4005 * provided, the scale is set to 1 DAY. 4006 * The minimumStep should correspond with the onscreen size of about 6 characters 4007 * @param {Date} start The start date and time. 4008 * @param {Date} end The end date and time. 4009 * @param {int} minimumStep Optional. Minimum step size in milliseconds 4010 */ 4011 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) { 4012 if (isNaN(start) || isNaN(end)) { 4013 //throw "No legal start or end date in method setRange"; 4014 return; 4015 } 4016 4017 this._start = (start != undefined) ? new Date(start) : new Date(); 4018 this._end = (end != undefined) ? new Date(end) : new Date(); 4019 4020 if (this.autoScale) { 4021 this.setMinimumStep(minimumStep); 4022 } 4023 } 4024 4025 /** 4026 * Set the step iterator to the start date. 4027 */ 4028 links.Timeline.StepDate.prototype.start = function() { 4029 this.current = new Date(this._start); 4030 this.roundToMinor(); 4031 } 4032 4033 /** 4034 * Round the current date to the first minor date value 4035 * This must be executed once when the current date is set to start Date 4036 */ 4037 links.Timeline.StepDate.prototype.roundToMinor = function() { 4038 // round to floor 4039 // IMPORTANT: we have no breaks in this switch! (this is no bug) 4040 switch (this.scale) { 4041 case links.Timeline.StepDate.SCALE.YEAR: 4042 this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); 4043 this.current.setMonth(0); 4044 case links.Timeline.StepDate.SCALE.MONTH: this.current.setDate(1); 4045 case links.Timeline.StepDate.SCALE.DAY: this.current.setHours(0); 4046 case links.Timeline.StepDate.SCALE.HOUR: this.current.setMinutes(0); 4047 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setSeconds(0); 4048 case links.Timeline.StepDate.SCALE.SECOND: this.current.setMilliseconds(0); 4049 //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds 4050 } 4051 4052 if (this.step != 1) { 4053 // round down to the first minor value that is a multiple of the current step size 4054 switch (this.scale) { 4055 case links.Timeline.StepDate.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; 4056 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; 4057 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; 4058 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; 4059 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; 4060 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; 4061 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; 4062 default: break; 4063 } 4064 } 4065 } 4066 4067 /** 4068 * Check if the end date is reached 4069 * @return {boolean} true if the current date has passed the end date 4070 */ 4071 links.Timeline.StepDate.prototype.end = function () { 4072 return (this.current.getTime() > this._end.getTime()); 4073 } 4074 4075 /** 4076 * Do the next step 4077 */ 4078 links.Timeline.StepDate.prototype.next = function() { 4079 var prev = this.current.getTime(); 4080 4081 // Two cases, needed to prevent issues with switching daylight savings 4082 // (end of March and end of October) 4083 if (this.current.getMonth() < 6) { 4084 switch (this.scale) 4085 { 4086 case links.Timeline.StepDate.SCALE.MILLISECOND: 4087 4088 this.current = new Date(this.current.getTime() + this.step); break; 4089 case links.Timeline.StepDate.SCALE.SECOND: this.current = new Date(this.current.getTime() + this.step * 1000); break; 4090 case links.Timeline.StepDate.SCALE.MINUTE: this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break; 4091 case links.Timeline.StepDate.SCALE.HOUR: 4092 this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60); 4093 // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) 4094 var h = this.current.getHours(); 4095 this.current.setHours(h - (h % this.step)); 4096 break; 4097 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 4098 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 4099 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 4100 default: break; 4101 } 4102 } 4103 else { 4104 switch (this.scale) 4105 { 4106 case links.Timeline.StepDate.SCALE.MILLISECOND: 4107 4108 this.current = new Date(this.current.getTime() + this.step); break; 4109 case links.Timeline.StepDate.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; 4110 case links.Timeline.StepDate.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; 4111 case links.Timeline.StepDate.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; 4112 case links.Timeline.StepDate.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; 4113 case links.Timeline.StepDate.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; 4114 case links.Timeline.StepDate.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; 4115 default: break; 4116 } 4117 } 4118 4119 if (this.step != 1) { 4120 // round down to the correct major value 4121 switch (this.scale) { 4122 case links.Timeline.StepDate.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; 4123 case links.Timeline.StepDate.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; 4124 case links.Timeline.StepDate.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; 4125 case links.Timeline.StepDate.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; 4126 case links.Timeline.StepDate.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; 4127 case links.Timeline.StepDate.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; 4128 case links.Timeline.StepDate.SCALE.YEAR: break; // nothing to do for year 4129 default: break; 4130 } 4131 } 4132 4133 // safety mechanism: if current time is still unchanged, move to the end 4134 if (this.current.getTime() == prev) { 4135 this.current = new Date(this._end); 4136 } 4137 } 4138 4139 4140 /** 4141 * Get the current datetime 4142 * @return {Date} current The current date 4143 */ 4144 links.Timeline.StepDate.prototype.getCurrent = function() { 4145 return this.current; 4146 } 4147 4148 /** 4149 * Set a custom scale. Autoscaling will be disabled. 4150 * For example setScale(SCALE.MINUTES, 5) will result 4151 * in minor steps of 5 minutes, and major steps of an hour. 4152 * 4153 * @param {Step.SCALE} newScale A scale. Choose from SCALE.MILLISECOND, 4154 * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, 4155 * SCALE.DAY, SCALE.MONTH, SCALE.YEAR. 4156 * @param {int} newStep A step size, by default 1. Choose for 4157 * example 1, 2, 5, or 10. 4158 */ 4159 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) { 4160 this.scale = newScale; 4161 4162 if (newStep > 0) 4163 this.step = newStep; 4164 4165 this.autoScale = false; 4166 } 4167 4168 /** 4169 * Enable or disable autoscaling 4170 * @param {boolean} enable If true, autoascaling is set true 4171 */ 4172 links.Timeline.StepDate.prototype.setAutoScale = function (enable) { 4173 this.autoScale = enable; 4174 } 4175 4176 4177 /** 4178 * Automatically determine the scale that bests fits the provided minimum step 4179 * @param {int} minimumStep The minimum step size in milliseconds 4180 */ 4181 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) { 4182 if (minimumStep == undefined) 4183 return; 4184 4185 var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); 4186 var stepMonth = (1000 * 60 * 60 * 24 * 30); 4187 var stepDay = (1000 * 60 * 60 * 24); 4188 var stepHour = (1000 * 60 * 60); 4189 var stepMinute = (1000 * 60); 4190 var stepSecond = (1000); 4191 var stepMillisecond= (1); 4192 4193 // find the smallest step that is larger than the provided minimumStep 4194 if (stepYear*1000 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;} 4195 if (stepYear*500 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 500;} 4196 if (stepYear*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 100;} 4197 if (stepYear*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 50;} 4198 if (stepYear*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 10;} 4199 if (stepYear*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 5;} 4200 if (stepYear > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.YEAR; this.step = 1;} 4201 if (stepMonth*3 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 3;} 4202 if (stepMonth > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MONTH; this.step = 1;} 4203 if (stepDay*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 5;} 4204 if (stepDay*2 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 2;} 4205 if (stepDay > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.DAY; this.step = 1;} 4206 if (stepHour*4 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 4;} 4207 if (stepHour > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.HOUR; this.step = 1;} 4208 if (stepMinute*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;} 4209 if (stepMinute*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;} 4210 if (stepMinute*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;} 4211 if (stepMinute > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;} 4212 if (stepSecond*15 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 15;} 4213 if (stepSecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 10;} 4214 if (stepSecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 5;} 4215 if (stepSecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.SECOND; this.step = 1;} 4216 if (stepMillisecond*200 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;} 4217 if (stepMillisecond*100 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;} 4218 if (stepMillisecond*50 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;} 4219 if (stepMillisecond*10 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;} 4220 if (stepMillisecond*5 > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;} 4221 if (stepMillisecond > minimumStep) {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;} 4222 } 4223 4224 /** 4225 * Snap a date to a rounded value. The snap intervals are dependent on the 4226 * current scale and step. 4227 * @param {Date} date the date to be snapped 4228 */ 4229 links.Timeline.StepDate.prototype.snap = function(date) { 4230 if (this.scale == links.Timeline.StepDate.SCALE.YEAR) { 4231 var year = date.getFullYear() + Math.round(date.getMonth() / 12); 4232 date.setFullYear(Math.round(year / this.step) * this.step); 4233 date.setMonth(0); 4234 date.setDate(0); 4235 date.setHours(0); 4236 date.setMinutes(0); 4237 date.setSeconds(0); 4238 date.setMilliseconds(0); 4239 } 4240 else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) { 4241 if (date.getDate() > 15) { 4242 date.setDate(1); 4243 date.setMonth(date.getMonth() + 1); 4244 // important: first set Date to 1, after that change the month. 4245 } 4246 else { 4247 date.setDate(1); 4248 } 4249 4250 date.setHours(0); 4251 date.setMinutes(0); 4252 date.setSeconds(0); 4253 date.setMilliseconds(0); 4254 } 4255 else if (this.scale == links.Timeline.StepDate.SCALE.DAY) { 4256 switch (this.step) { 4257 case 5: 4258 case 2: 4259 date.setHours(Math.round(date.getHours() / 24) * 24); break; 4260 default: 4261 date.setHours(Math.round(date.getHours() / 12) * 12); break; 4262 } 4263 date.setMinutes(0); 4264 date.setSeconds(0); 4265 date.setMilliseconds(0); 4266 } 4267 else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) { 4268 switch (this.step) { 4269 case 4: 4270 date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break; 4271 default: 4272 date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break; 4273 } 4274 date.setSeconds(0); 4275 date.setMilliseconds(0); 4276 } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) { 4277 switch (this.step) { 4278 case 15: 4279 case 10: 4280 date.setMinutes(Math.round(date.getMinutes() / 5) * 5); 4281 date.setSeconds(0); 4282 break; 4283 case 5: 4284 date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break; 4285 default: 4286 date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break; 4287 } 4288 date.setMilliseconds(0); 4289 } 4290 else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) { 4291 switch (this.step) { 4292 case 15: 4293 case 10: 4294 date.setSeconds(Math.round(date.getSeconds() / 5) * 5); 4295 date.setMilliseconds(0); 4296 break; 4297 case 5: 4298 date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break; 4299 default: 4300 date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break; 4301 } 4302 } 4303 else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) { 4304 var step = this.step > 5 ? this.step / 2 : 1; 4305 date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step); 4306 } 4307 } 4308 4309 /** 4310 * Check if the current step is a major step (for example when the step 4311 * is DAY, a major step is each first day of the MONTH) 4312 * @return true if current date is major, else false. 4313 */ 4314 links.Timeline.StepDate.prototype.isMajor = function() { 4315 switch (this.scale) 4316 { 4317 case links.Timeline.StepDate.SCALE.MILLISECOND: 4318 return (this.current.getMilliseconds() == 0); 4319 case links.Timeline.StepDate.SCALE.SECOND: 4320 return (this.current.getSeconds() == 0); 4321 case links.Timeline.StepDate.SCALE.MINUTE: 4322 return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); 4323 // Note: this is no bug. Major label is equal for both minute and hour scale 4324 case links.Timeline.StepDate.SCALE.HOUR: 4325 return (this.current.getHours() == 0); 4326 case links.Timeline.StepDate.SCALE.DAY: 4327 return (this.current.getDate() == 1); 4328 case links.Timeline.StepDate.SCALE.MONTH: 4329 return (this.current.getMonth() == 0); 4330 case links.Timeline.StepDate.SCALE.YEAR: 4331 return false 4332 default: 4333 return false; 4334 } 4335 } 4336 4337 4338 /** 4339 * Returns formatted text for the minor axislabel, depending on the current 4340 * date and the scale. For example when scale is MINUTE, the current time is 4341 * formatted as "hh:mm". 4342 * @param {Date} optional custom date. if not provided, current date is taken 4343 * @return {string} minor axislabel 4344 */ 4345 links.Timeline.StepDate.prototype.getLabelMinor = function(date) { 4346 var MONTHS_SHORT = new Array("Jan", "Feb", "Mar", 4347 "Apr", "May", "Jun", 4348 "Jul", "Aug", "Sep", 4349 "Oct", "Nov", "Dec"); 4350 4351 if (date == undefined) { 4352 date = this.current; 4353 } 4354 4355 switch (this.scale) 4356 { 4357 case links.Timeline.StepDate.SCALE.MILLISECOND: return String(date.getMilliseconds()); 4358 case links.Timeline.StepDate.SCALE.SECOND: return String(date.getSeconds()); 4359 case links.Timeline.StepDate.SCALE.MINUTE: return this.addZeros(date.getHours(), 2) + ":" + 4360 this.addZeros(date.getMinutes(), 2); 4361 case links.Timeline.StepDate.SCALE.HOUR: return this.addZeros(date.getHours(), 2) + ":" + 4362 this.addZeros(date.getMinutes(), 2); 4363 case links.Timeline.StepDate.SCALE.DAY: return String(date.getDate()); 4364 case links.Timeline.StepDate.SCALE.MONTH: return MONTHS_SHORT[date.getMonth()]; // month is zero based 4365 case links.Timeline.StepDate.SCALE.YEAR: return String(date.getFullYear()); 4366 default: return ""; 4367 } 4368 } 4369 4370 4371 /** 4372 * Returns formatted text for the major axislabel, depending on the current 4373 * date and the scale. For example when scale is MINUTE, the major scale is 4374 * hours, and the hour will be formatted as "hh". 4375 * @param {Date} optional custom date. if not provided, current date is taken 4376 * @return {string} major axislabel 4377 */ 4378 links.Timeline.StepDate.prototype.getLabelMajor = function(date) { 4379 var MONTHS = new Array("January", "February", "March", 4380 "April", "May", "June", 4381 "July", "August", "September", 4382 "October", "November", "December"); 4383 var DAYS = new Array("Sunday", "Monday", "Tuesday", 4384 "Wednesday", "Thursday", "Friday", "Saturday"); 4385 4386 if (date == undefined) { 4387 date = this.current; 4388 } 4389 4390 switch (this.scale) { 4391 case links.Timeline.StepDate.SCALE.MILLISECOND: 4392 return this.addZeros(date.getHours(), 2) + ":" + 4393 this.addZeros(date.getMinutes(), 2) + ":" + 4394 this.addZeros(date.getSeconds(), 2); 4395 case links.Timeline.StepDate.SCALE.SECOND: 4396 return date.getDate() + " " + 4397 MONTHS[date.getMonth()] + " " + 4398 this.addZeros(date.getHours(), 2) + ":" + 4399 this.addZeros(date.getMinutes(), 2); 4400 case links.Timeline.StepDate.SCALE.MINUTE: 4401 return DAYS[date.getDay()] + " " + 4402 date.getDate() + " " + 4403 MONTHS[date.getMonth()] + " " + 4404 date.getFullYear(); 4405 case links.Timeline.StepDate.SCALE.HOUR: 4406 return DAYS[date.getDay()] + " " + 4407 date.getDate() + " " + 4408 MONTHS[date.getMonth()] + " " + 4409 date.getFullYear(); 4410 case links.Timeline.StepDate.SCALE.DAY: 4411 return MONTHS[date.getMonth()] + " " + 4412 date.getFullYear(); 4413 case links.Timeline.StepDate.SCALE.MONTH: 4414 return String(date.getFullYear()); 4415 default: 4416 return ""; 4417 } 4418 } 4419 4420 /** 4421 * Add leading zeros to the given value to match the desired length. 4422 * For example addZeros(123, 5) returns "00123" 4423 * @param {int} value A value 4424 * @param {int} len Desired final length 4425 * @return {string} value with leading zeros 4426 */ 4427 links.Timeline.StepDate.prototype.addZeros = function(value, len) { 4428 var str = "" + value; 4429 while (str.length < len) { 4430 str = "0" + str; 4431 } 4432 return str; 4433 } 4434 4435 4436 /** ------------------------------------------------------------------------ **/ 4437 4438 4439 /** 4440 * Add and event listener. Works for all browsers 4441 * @param {DOM Element} element An html element 4442 * @param {string} action The action, for example "click", 4443 * without the prefix "on" 4444 * @param {function} listener The callback function to be executed 4445 * @param {boolean} useCapture 4446 */ 4447 links.Timeline.addEventListener = function (element, action, listener, useCapture) { 4448 if (element.addEventListener) { 4449 if (useCapture === undefined) 4450 useCapture = false; 4451 4452 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 4453 action = "DOMMouseScroll"; // For Firefox 4454 } 4455 4456 element.addEventListener(action, listener, useCapture); 4457 } else { 4458 element.attachEvent("on" + action, listener); // IE browsers 4459 } 4460 }; 4461 4462 /** 4463 * Remove an event listener from an element 4464 * @param {DOM element} element An html dom element 4465 * @param {string} action The name of the event, for example "mousedown" 4466 * @param {function} listener The listener function 4467 * @param {boolean} useCapture 4468 */ 4469 links.Timeline.removeEventListener = function(element, action, listener, useCapture) { 4470 if (element.removeEventListener) { 4471 // non-IE browsers 4472 if (useCapture === undefined) 4473 useCapture = false; 4474 4475 if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { 4476 action = "DOMMouseScroll"; // For Firefox 4477 } 4478 4479 element.removeEventListener(action, listener, useCapture); 4480 } else { 4481 // IE browsers 4482 element.detachEvent("on" + action, listener); 4483 } 4484 }; 4485 4486 4487 /** 4488 * Get HTML element which is the target of the event 4489 * @param {MouseEvent} event 4490 * @return {HTML DOM} target element 4491 */ 4492 links.Timeline.getTarget = function (event) { 4493 // code from http://www.quirksmode.org/js/events_properties.html 4494 if (!event) { 4495 var event = window.event; 4496 } 4497 4498 var target; 4499 4500 if (event.target) { 4501 target = event.target; 4502 } 4503 else if (event.srcElement) { 4504 target = event.srcElement; 4505 } 4506 4507 if (target.nodeType !== undefined && target.nodeType == 3) { 4508 // defeat Safari bug 4509 target = target.parentNode; 4510 } 4511 4512 return target; 4513 } 4514 4515 /** 4516 * Stop event propagation 4517 */ 4518 links.Timeline.stopPropagation = function (event) { 4519 if (!event) 4520 var event = window.event; 4521 4522 if (event.stopPropagation) { 4523 event.stopPropagation(); // non-IE browsers 4524 } 4525 else { 4526 event.cancelBubble = true; // IE browsers 4527 } 4528 } 4529 4530 4531 /** 4532 * Cancels the event if it is cancelable, without stopping further propagation of the event. 4533 */ 4534 links.Timeline.prevent ult = function (event) { 4535 if (!event) 4536 var event = window.event; 4537 4538 if (event.prevent ult) { 4539 event.prevent ult(); // non-IE browsers 4540 } 4541 else { 4542 event.returnValue = false; // IE browsers 4543 } 4544 } 4545 4546 4547 /** 4548 * Retrieve the absolute left value of a DOM element 4549 * @param {DOM element} elem A dom element, for example a div 4550 * @return {number} left The absolute left position of this element 4551 * in the browser page. 4552 */ 4553 links.Timeline.getAbsoluteLeft = function(elem) 4554 { 4555 var left = 0; 4556 while( elem != null ) { 4557 left += elem.offsetLeft; 4558 //left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too 4559 elem = elem.offsetParent; 4560 } 4561 return left; 4562 } 4563 4564 /** 4565 * Retrieve the absolute top value of a DOM element 4566 * @param {DOM element} elem A dom element, for example a div 4567 * @return {number} top The absolute top position of this element 4568 * in the browser page. 4569 */ 4570 links.Timeline.getAbsoluteTop = function(elem) 4571 { 4572 var top = 0; 4573 while( elem != null ) { 4574 top += elem.offsetTop; 4575 //left -= elem.srcollLeft; // TODO: adjust for scroll positions. check if it works in IE too 4576 elem = elem.offsetParent; 4577 } 4578 return top; 4579 } 4580