vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11964 lines
324 KiB

  1. /**
  2. * vis.js module imports
  3. */
  4. // Try to load dependencies from the global window object.
  5. // If not available there, load via require.
  6. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  7. var Hammer;
  8. if (typeof window !== 'undefined') {
  9. // load hammer.js only when running in a browser (where window is available)
  10. Hammer = window['Hammer'] || require('hammerjs');
  11. }
  12. else {
  13. Hammer = function () {
  14. throw Error('hammer.js is only available in a browser, not in node.js.');
  15. }
  16. }
  17. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  18. // it here in that case.
  19. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  20. if(!Array.prototype.indexOf) {
  21. Array.prototype.indexOf = function(obj){
  22. for(var i = 0; i < this.length; i++){
  23. if(this[i] == obj){
  24. return i;
  25. }
  26. }
  27. return -1;
  28. };
  29. try {
  30. console.log("Warning: Ancient browser detected. Please update your browser");
  31. }
  32. catch (err) {
  33. }
  34. }
  35. // Internet Explorer 8 and older does not support Array.forEach, so we define
  36. // it here in that case.
  37. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  38. if (!Array.prototype.forEach) {
  39. Array.prototype.forEach = function(fn, scope) {
  40. for(var i = 0, len = this.length; i < len; ++i) {
  41. fn.call(scope || this, this[i], i, this);
  42. }
  43. }
  44. }
  45. // Internet Explorer 8 and older does not support Array.map, so we define it
  46. // here in that case.
  47. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  48. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  49. // Reference: http://es5.github.com/#x15.4.4.19
  50. if (!Array.prototype.map) {
  51. Array.prototype.map = function(callback, thisArg) {
  52. var T, A, k;
  53. if (this == null) {
  54. throw new TypeError(" this is null or not defined");
  55. }
  56. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  57. var O = Object(this);
  58. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  59. // 3. Let len be ToUint32(lenValue).
  60. var len = O.length >>> 0;
  61. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  62. // See: http://es5.github.com/#x9.11
  63. if (typeof callback !== "function") {
  64. throw new TypeError(callback + " is not a function");
  65. }
  66. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  67. if (thisArg) {
  68. T = thisArg;
  69. }
  70. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  71. // the standard built-in constructor with that name and len is the value of len.
  72. A = new Array(len);
  73. // 7. Let k be 0
  74. k = 0;
  75. // 8. Repeat, while k < len
  76. while(k < len) {
  77. var kValue, mappedValue;
  78. // a. Let Pk be ToString(k).
  79. // This is implicit for LHS operands of the in operator
  80. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  81. // This step can be combined with c
  82. // c. If kPresent is true, then
  83. if (k in O) {
  84. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  85. kValue = O[ k ];
  86. // ii. Let mappedValue be the result of calling the Call internal method of callback
  87. // with T as the this value and argument list containing kValue, k, and O.
  88. mappedValue = callback.call(T, kValue, k, O);
  89. // iii. Call the DefineOwnProperty internal method of A with arguments
  90. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  91. // and false.
  92. // In browsers that support Object.defineProperty, use the following:
  93. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  94. // For best browser support, use the following:
  95. A[ k ] = mappedValue;
  96. }
  97. // d. Increase k by 1.
  98. k++;
  99. }
  100. // 9. return A
  101. return A;
  102. };
  103. }
  104. // Internet Explorer 8 and older does not support Array.filter, so we define it
  105. // here in that case.
  106. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  107. if (!Array.prototype.filter) {
  108. Array.prototype.filter = function(fun /*, thisp */) {
  109. "use strict";
  110. if (this == null) {
  111. throw new TypeError();
  112. }
  113. var t = Object(this);
  114. var len = t.length >>> 0;
  115. if (typeof fun != "function") {
  116. throw new TypeError();
  117. }
  118. var res = [];
  119. var thisp = arguments[1];
  120. for (var i = 0; i < len; i++) {
  121. if (i in t) {
  122. var val = t[i]; // in case fun mutates this
  123. if (fun.call(thisp, val, i, t))
  124. res.push(val);
  125. }
  126. }
  127. return res;
  128. };
  129. }
  130. // Internet Explorer 8 and older does not support Object.keys, so we define it
  131. // here in that case.
  132. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  133. if (!Object.keys) {
  134. Object.keys = (function () {
  135. var hasOwnProperty = Object.prototype.hasOwnProperty,
  136. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  137. dontEnums = [
  138. 'toString',
  139. 'toLocaleString',
  140. 'valueOf',
  141. 'hasOwnProperty',
  142. 'isPrototypeOf',
  143. 'propertyIsEnumerable',
  144. 'constructor'
  145. ],
  146. dontEnumsLength = dontEnums.length;
  147. return function (obj) {
  148. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  149. throw new TypeError('Object.keys called on non-object');
  150. }
  151. var result = [];
  152. for (var prop in obj) {
  153. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  154. }
  155. if (hasDontEnumBug) {
  156. for (var i=0; i < dontEnumsLength; i++) {
  157. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  158. }
  159. }
  160. return result;
  161. }
  162. })()
  163. }
  164. // Internet Explorer 8 and older does not support Array.isArray,
  165. // so we define it here in that case.
  166. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  167. if(!Array.isArray) {
  168. Array.isArray = function (vArg) {
  169. return Object.prototype.toString.call(vArg) === "[object Array]";
  170. };
  171. }
  172. // Internet Explorer 8 and older does not support Function.bind,
  173. // so we define it here in that case.
  174. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  175. if (!Function.prototype.bind) {
  176. Function.prototype.bind = function (oThis) {
  177. if (typeof this !== "function") {
  178. // closest thing possible to the ECMAScript 5 internal IsCallable function
  179. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  180. }
  181. var aArgs = Array.prototype.slice.call(arguments, 1),
  182. fToBind = this,
  183. fNOP = function () {},
  184. fBound = function () {
  185. return fToBind.apply(this instanceof fNOP && oThis
  186. ? this
  187. : oThis,
  188. aArgs.concat(Array.prototype.slice.call(arguments)));
  189. };
  190. fNOP.prototype = this.prototype;
  191. fBound.prototype = new fNOP();
  192. return fBound;
  193. };
  194. }
  195. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  196. if (!Object.create) {
  197. Object.create = function (o) {
  198. if (arguments.length > 1) {
  199. throw new Error('Object.create implementation only accepts the first parameter.');
  200. }
  201. function F() {}
  202. F.prototype = o;
  203. return new F();
  204. };
  205. }
  206. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  207. if (!Function.prototype.bind) {
  208. Function.prototype.bind = function (oThis) {
  209. if (typeof this !== "function") {
  210. // closest thing possible to the ECMAScript 5 internal IsCallable function
  211. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  212. }
  213. var aArgs = Array.prototype.slice.call(arguments, 1),
  214. fToBind = this,
  215. fNOP = function () {},
  216. fBound = function () {
  217. return fToBind.apply(this instanceof fNOP && oThis
  218. ? this
  219. : oThis,
  220. aArgs.concat(Array.prototype.slice.call(arguments)));
  221. };
  222. fNOP.prototype = this.prototype;
  223. fBound.prototype = new fNOP();
  224. return fBound;
  225. };
  226. }
  227. /**
  228. * utility functions
  229. */
  230. var util = {};
  231. /**
  232. * Test whether given object is a number
  233. * @param {*} object
  234. * @return {Boolean} isNumber
  235. */
  236. util.isNumber = function isNumber(object) {
  237. return (object instanceof Number || typeof object == 'number');
  238. };
  239. /**
  240. * Test whether given object is a string
  241. * @param {*} object
  242. * @return {Boolean} isString
  243. */
  244. util.isString = function isString(object) {
  245. return (object instanceof String || typeof object == 'string');
  246. };
  247. /**
  248. * Test whether given object is a Date, or a String containing a Date
  249. * @param {Date | String} object
  250. * @return {Boolean} isDate
  251. */
  252. util.isDate = function isDate(object) {
  253. if (object instanceof Date) {
  254. return true;
  255. }
  256. else if (util.isString(object)) {
  257. // test whether this string contains a date
  258. var match = ASPDateRegex.exec(object);
  259. if (match) {
  260. return true;
  261. }
  262. else if (!isNaN(Date.parse(object))) {
  263. return true;
  264. }
  265. }
  266. return false;
  267. };
  268. /**
  269. * Test whether given object is an instance of google.visualization.DataTable
  270. * @param {*} object
  271. * @return {Boolean} isDataTable
  272. */
  273. util.isDataTable = function isDataTable(object) {
  274. return (typeof (google) !== 'undefined') &&
  275. (google.visualization) &&
  276. (google.visualization.DataTable) &&
  277. (object instanceof google.visualization.DataTable);
  278. };
  279. /**
  280. * Create a semi UUID
  281. * source: http://stackoverflow.com/a/105074/1262753
  282. * @return {String} uuid
  283. */
  284. util.randomUUID = function randomUUID () {
  285. var S4 = function () {
  286. return Math.floor(
  287. Math.random() * 0x10000 /* 65536 */
  288. ).toString(16);
  289. };
  290. return (
  291. S4() + S4() + '-' +
  292. S4() + '-' +
  293. S4() + '-' +
  294. S4() + '-' +
  295. S4() + S4() + S4()
  296. );
  297. };
  298. /**
  299. * Extend object a with the properties of object b or a series of objects
  300. * Only properties with defined values are copied
  301. * @param {Object} a
  302. * @param {... Object} b
  303. * @return {Object} a
  304. */
  305. util.extend = function (a, b) {
  306. for (var i = 1, len = arguments.length; i < len; i++) {
  307. var other = arguments[i];
  308. for (var prop in other) {
  309. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  310. a[prop] = other[prop];
  311. }
  312. }
  313. }
  314. return a;
  315. };
  316. /**
  317. * Convert an object to another type
  318. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  319. * @param {String | undefined} type Name of the type. Available types:
  320. * 'Boolean', 'Number', 'String',
  321. * 'Date', 'Moment', ISODate', 'ASPDate'.
  322. * @return {*} object
  323. * @throws Error
  324. */
  325. util.convert = function convert(object, type) {
  326. var match;
  327. if (object === undefined) {
  328. return undefined;
  329. }
  330. if (object === null) {
  331. return null;
  332. }
  333. if (!type) {
  334. return object;
  335. }
  336. if (!(typeof type === 'string') && !(type instanceof String)) {
  337. throw new Error('Type must be a string');
  338. }
  339. //noinspection FallthroughInSwitchStatementJS
  340. switch (type) {
  341. case 'boolean':
  342. case 'Boolean':
  343. return Boolean(object);
  344. case 'number':
  345. case 'Number':
  346. return Number(object.valueOf());
  347. case 'string':
  348. case 'String':
  349. return String(object);
  350. case 'Date':
  351. if (util.isNumber(object)) {
  352. return new Date(object);
  353. }
  354. if (object instanceof Date) {
  355. return new Date(object.valueOf());
  356. }
  357. else if (moment.isMoment(object)) {
  358. return new Date(object.valueOf());
  359. }
  360. if (util.isString(object)) {
  361. match = ASPDateRegex.exec(object);
  362. if (match) {
  363. // object is an ASP date
  364. return new Date(Number(match[1])); // parse number
  365. }
  366. else {
  367. return moment(object).toDate(); // parse string
  368. }
  369. }
  370. else {
  371. throw new Error(
  372. 'Cannot convert object of type ' + util.getType(object) +
  373. ' to type Date');
  374. }
  375. case 'Moment':
  376. if (util.isNumber(object)) {
  377. return moment(object);
  378. }
  379. if (object instanceof Date) {
  380. return moment(object.valueOf());
  381. }
  382. else if (moment.isMoment(object)) {
  383. return moment(object);
  384. }
  385. if (util.isString(object)) {
  386. match = ASPDateRegex.exec(object);
  387. if (match) {
  388. // object is an ASP date
  389. return moment(Number(match[1])); // parse number
  390. }
  391. else {
  392. return moment(object); // parse string
  393. }
  394. }
  395. else {
  396. throw new Error(
  397. 'Cannot convert object of type ' + util.getType(object) +
  398. ' to type Date');
  399. }
  400. case 'ISODate':
  401. if (util.isNumber(object)) {
  402. return new Date(object);
  403. }
  404. else if (object instanceof Date) {
  405. return object.toISOString();
  406. }
  407. else if (moment.isMoment(object)) {
  408. return object.toDate().toISOString();
  409. }
  410. else if (util.isString(object)) {
  411. match = ASPDateRegex.exec(object);
  412. if (match) {
  413. // object is an ASP date
  414. return new Date(Number(match[1])).toISOString(); // parse number
  415. }
  416. else {
  417. return new Date(object).toISOString(); // parse string
  418. }
  419. }
  420. else {
  421. throw new Error(
  422. 'Cannot convert object of type ' + util.getType(object) +
  423. ' to type ISODate');
  424. }
  425. case 'ASPDate':
  426. if (util.isNumber(object)) {
  427. return '/Date(' + object + ')/';
  428. }
  429. else if (object instanceof Date) {
  430. return '/Date(' + object.valueOf() + ')/';
  431. }
  432. else if (util.isString(object)) {
  433. match = ASPDateRegex.exec(object);
  434. var value;
  435. if (match) {
  436. // object is an ASP date
  437. value = new Date(Number(match[1])).valueOf(); // parse number
  438. }
  439. else {
  440. value = new Date(object).valueOf(); // parse string
  441. }
  442. return '/Date(' + value + ')/';
  443. }
  444. else {
  445. throw new Error(
  446. 'Cannot convert object of type ' + util.getType(object) +
  447. ' to type ASPDate');
  448. }
  449. default:
  450. throw new Error('Cannot convert object of type ' + util.getType(object) +
  451. ' to type "' + type + '"');
  452. }
  453. };
  454. // parse ASP.Net Date pattern,
  455. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  456. // code from http://momentjs.com/
  457. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  458. /**
  459. * Get the type of an object, for example util.getType([]) returns 'Array'
  460. * @param {*} object
  461. * @return {String} type
  462. */
  463. util.getType = function getType(object) {
  464. var type = typeof object;
  465. if (type == 'object') {
  466. if (object == null) {
  467. return 'null';
  468. }
  469. if (object instanceof Boolean) {
  470. return 'Boolean';
  471. }
  472. if (object instanceof Number) {
  473. return 'Number';
  474. }
  475. if (object instanceof String) {
  476. return 'String';
  477. }
  478. if (object instanceof Array) {
  479. return 'Array';
  480. }
  481. if (object instanceof Date) {
  482. return 'Date';
  483. }
  484. return 'Object';
  485. }
  486. else if (type == 'number') {
  487. return 'Number';
  488. }
  489. else if (type == 'boolean') {
  490. return 'Boolean';
  491. }
  492. else if (type == 'string') {
  493. return 'String';
  494. }
  495. return type;
  496. };
  497. /**
  498. * Retrieve the absolute left value of a DOM element
  499. * @param {Element} elem A dom element, for example a div
  500. * @return {number} left The absolute left position of this element
  501. * in the browser page.
  502. */
  503. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  504. var doc = document.documentElement;
  505. var body = document.body;
  506. var left = elem.offsetLeft;
  507. var e = elem.offsetParent;
  508. while (e != null && e != body && e != doc) {
  509. left += e.offsetLeft;
  510. left -= e.scrollLeft;
  511. e = e.offsetParent;
  512. }
  513. return left;
  514. };
  515. /**
  516. * Retrieve the absolute top value of a DOM element
  517. * @param {Element} elem A dom element, for example a div
  518. * @return {number} top The absolute top position of this element
  519. * in the browser page.
  520. */
  521. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  522. var doc = document.documentElement;
  523. var body = document.body;
  524. var top = elem.offsetTop;
  525. var e = elem.offsetParent;
  526. while (e != null && e != body && e != doc) {
  527. top += e.offsetTop;
  528. top -= e.scrollTop;
  529. e = e.offsetParent;
  530. }
  531. return top;
  532. };
  533. /**
  534. * Get the absolute, vertical mouse position from an event.
  535. * @param {Event} event
  536. * @return {Number} pageY
  537. */
  538. util.getPageY = function getPageY (event) {
  539. if ('pageY' in event) {
  540. return event.pageY;
  541. }
  542. else {
  543. var clientY;
  544. if (('targetTouches' in event) && event.targetTouches.length) {
  545. clientY = event.targetTouches[0].clientY;
  546. }
  547. else {
  548. clientY = event.clientY;
  549. }
  550. var doc = document.documentElement;
  551. var body = document.body;
  552. return clientY +
  553. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  554. ( doc && doc.clientTop || body && body.clientTop || 0 );
  555. }
  556. };
  557. /**
  558. * Get the absolute, horizontal mouse position from an event.
  559. * @param {Event} event
  560. * @return {Number} pageX
  561. */
  562. util.getPageX = function getPageX (event) {
  563. if ('pageY' in event) {
  564. return event.pageX;
  565. }
  566. else {
  567. var clientX;
  568. if (('targetTouches' in event) && event.targetTouches.length) {
  569. clientX = event.targetTouches[0].clientX;
  570. }
  571. else {
  572. clientX = event.clientX;
  573. }
  574. var doc = document.documentElement;
  575. var body = document.body;
  576. return clientX +
  577. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  578. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  579. }
  580. };
  581. /**
  582. * add a className to the given elements style
  583. * @param {Element} elem
  584. * @param {String} className
  585. */
  586. util.addClassName = function addClassName(elem, className) {
  587. var classes = elem.className.split(' ');
  588. if (classes.indexOf(className) == -1) {
  589. classes.push(className); // add the class to the array
  590. elem.className = classes.join(' ');
  591. }
  592. };
  593. /**
  594. * add a className to the given elements style
  595. * @param {Element} elem
  596. * @param {String} className
  597. */
  598. util.removeClassName = function removeClassname(elem, className) {
  599. var classes = elem.className.split(' ');
  600. var index = classes.indexOf(className);
  601. if (index != -1) {
  602. classes.splice(index, 1); // remove the class from the array
  603. elem.className = classes.join(' ');
  604. }
  605. };
  606. /**
  607. * For each method for both arrays and objects.
  608. * In case of an array, the built-in Array.forEach() is applied.
  609. * In case of an Object, the method loops over all properties of the object.
  610. * @param {Object | Array} object An Object or Array
  611. * @param {function} callback Callback method, called for each item in
  612. * the object or array with three parameters:
  613. * callback(value, index, object)
  614. */
  615. util.forEach = function forEach (object, callback) {
  616. var i,
  617. len;
  618. if (object instanceof Array) {
  619. // array
  620. for (i = 0, len = object.length; i < len; i++) {
  621. callback(object[i], i, object);
  622. }
  623. }
  624. else {
  625. // object
  626. for (i in object) {
  627. if (object.hasOwnProperty(i)) {
  628. callback(object[i], i, object);
  629. }
  630. }
  631. }
  632. };
  633. /**
  634. * Update a property in an object
  635. * @param {Object} object
  636. * @param {String} key
  637. * @param {*} value
  638. * @return {Boolean} changed
  639. */
  640. util.updateProperty = function updateProp (object, key, value) {
  641. if (object[key] !== value) {
  642. object[key] = value;
  643. return true;
  644. }
  645. else {
  646. return false;
  647. }
  648. };
  649. /**
  650. * Add and event listener. Works for all browsers
  651. * @param {Element} element An html element
  652. * @param {string} action The action, for example "click",
  653. * without the prefix "on"
  654. * @param {function} listener The callback function to be executed
  655. * @param {boolean} [useCapture]
  656. */
  657. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  658. if (element.addEventListener) {
  659. if (useCapture === undefined)
  660. useCapture = false;
  661. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  662. action = "DOMMouseScroll"; // For Firefox
  663. }
  664. element.addEventListener(action, listener, useCapture);
  665. } else {
  666. element.attachEvent("on" + action, listener); // IE browsers
  667. }
  668. };
  669. /**
  670. * Remove an event listener from an element
  671. * @param {Element} element An html dom element
  672. * @param {string} action The name of the event, for example "mousedown"
  673. * @param {function} listener The listener function
  674. * @param {boolean} [useCapture]
  675. */
  676. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  677. if (element.removeEventListener) {
  678. // non-IE browsers
  679. if (useCapture === undefined)
  680. useCapture = false;
  681. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  682. action = "DOMMouseScroll"; // For Firefox
  683. }
  684. element.removeEventListener(action, listener, useCapture);
  685. } else {
  686. // IE browsers
  687. element.detachEvent("on" + action, listener);
  688. }
  689. };
  690. /**
  691. * Get HTML element which is the target of the event
  692. * @param {Event} event
  693. * @return {Element} target element
  694. */
  695. util.getTarget = function getTarget(event) {
  696. // code from http://www.quirksmode.org/js/events_properties.html
  697. if (!event) {
  698. event = window.event;
  699. }
  700. var target;
  701. if (event.target) {
  702. target = event.target;
  703. }
  704. else if (event.srcElement) {
  705. target = event.srcElement;
  706. }
  707. if (target.nodeType != undefined && target.nodeType == 3) {
  708. // defeat Safari bug
  709. target = target.parentNode;
  710. }
  711. return target;
  712. };
  713. /**
  714. * Stop event propagation
  715. */
  716. util.stopPropagation = function stopPropagation(event) {
  717. if (!event)
  718. event = window.event;
  719. if (event.stopPropagation) {
  720. event.stopPropagation(); // non-IE browsers
  721. }
  722. else {
  723. event.cancelBubble = true; // IE browsers
  724. }
  725. };
  726. /**
  727. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  728. */
  729. util.preventDefault = function preventDefault (event) {
  730. if (!event)
  731. event = window.event;
  732. if (event.preventDefault) {
  733. event.preventDefault(); // non-IE browsers
  734. }
  735. else {
  736. event.returnValue = false; // IE browsers
  737. }
  738. };
  739. util.option = {};
  740. /**
  741. * Convert a value into a boolean
  742. * @param {Boolean | function | undefined} value
  743. * @param {Boolean} [defaultValue]
  744. * @returns {Boolean} bool
  745. */
  746. util.option.asBoolean = function (value, defaultValue) {
  747. if (typeof value == 'function') {
  748. value = value();
  749. }
  750. if (value != null) {
  751. return (value != false);
  752. }
  753. return defaultValue || null;
  754. };
  755. /**
  756. * Convert a value into a number
  757. * @param {Boolean | function | undefined} value
  758. * @param {Number} [defaultValue]
  759. * @returns {Number} number
  760. */
  761. util.option.asNumber = function (value, defaultValue) {
  762. if (typeof value == 'function') {
  763. value = value();
  764. }
  765. if (value != null) {
  766. return Number(value) || defaultValue || null;
  767. }
  768. return defaultValue || null;
  769. };
  770. /**
  771. * Convert a value into a string
  772. * @param {String | function | undefined} value
  773. * @param {String} [defaultValue]
  774. * @returns {String} str
  775. */
  776. util.option.asString = function (value, defaultValue) {
  777. if (typeof value == 'function') {
  778. value = value();
  779. }
  780. if (value != null) {
  781. return String(value);
  782. }
  783. return defaultValue || null;
  784. };
  785. /**
  786. * Convert a size or location into a string with pixels or a percentage
  787. * @param {String | Number | function | undefined} value
  788. * @param {String} [defaultValue]
  789. * @returns {String} size
  790. */
  791. util.option.asSize = function (value, defaultValue) {
  792. if (typeof value == 'function') {
  793. value = value();
  794. }
  795. if (util.isString(value)) {
  796. return value;
  797. }
  798. else if (util.isNumber(value)) {
  799. return value + 'px';
  800. }
  801. else {
  802. return defaultValue || null;
  803. }
  804. };
  805. /**
  806. * Convert a value into a DOM element
  807. * @param {HTMLElement | function | undefined} value
  808. * @param {HTMLElement} [defaultValue]
  809. * @returns {HTMLElement | null} dom
  810. */
  811. util.option.asElement = function (value, defaultValue) {
  812. if (typeof value == 'function') {
  813. value = value();
  814. }
  815. return value || defaultValue || null;
  816. };
  817. /**
  818. * load css from text
  819. * @param {String} css Text containing css
  820. */
  821. util.loadCss = function (css) {
  822. if (typeof document === 'undefined') {
  823. return;
  824. }
  825. // get the script location, and built the css file name from the js file name
  826. // http://stackoverflow.com/a/2161748/1262753
  827. // var scripts = document.getElementsByTagName('script');
  828. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  829. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  830. // inject css
  831. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  832. var style = document.createElement('style');
  833. style.type = 'text/css';
  834. if (style.styleSheet){
  835. style.styleSheet.cssText = css;
  836. } else {
  837. style.appendChild(document.createTextNode(css));
  838. }
  839. document.getElementsByTagName('head')[0].appendChild(style);
  840. };
  841. /**
  842. * Event listener (singleton)
  843. */
  844. // TODO: replace usage of the event listener for the EventBus
  845. var events = {
  846. 'listeners': [],
  847. /**
  848. * Find a single listener by its object
  849. * @param {Object} object
  850. * @return {Number} index -1 when not found
  851. */
  852. 'indexOf': function (object) {
  853. var listeners = this.listeners;
  854. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  855. var listener = listeners[i];
  856. if (listener && listener.object == object) {
  857. return i;
  858. }
  859. }
  860. return -1;
  861. },
  862. /**
  863. * Add an event listener
  864. * @param {Object} object
  865. * @param {String} event The name of an event, for example 'select'
  866. * @param {function} callback The callback method, called when the
  867. * event takes place
  868. */
  869. 'addListener': function (object, event, callback) {
  870. var index = this.indexOf(object);
  871. var listener = this.listeners[index];
  872. if (!listener) {
  873. listener = {
  874. 'object': object,
  875. 'events': {}
  876. };
  877. this.listeners.push(listener);
  878. }
  879. var callbacks = listener.events[event];
  880. if (!callbacks) {
  881. callbacks = [];
  882. listener.events[event] = callbacks;
  883. }
  884. // add the callback if it does not yet exist
  885. if (callbacks.indexOf(callback) == -1) {
  886. callbacks.push(callback);
  887. }
  888. },
  889. /**
  890. * Remove an event listener
  891. * @param {Object} object
  892. * @param {String} event The name of an event, for example 'select'
  893. * @param {function} callback The registered callback method
  894. */
  895. 'removeListener': function (object, event, callback) {
  896. var index = this.indexOf(object);
  897. var listener = this.listeners[index];
  898. if (listener) {
  899. var callbacks = listener.events[event];
  900. if (callbacks) {
  901. index = callbacks.indexOf(callback);
  902. if (index != -1) {
  903. callbacks.splice(index, 1);
  904. }
  905. // remove the array when empty
  906. if (callbacks.length == 0) {
  907. delete listener.events[event];
  908. }
  909. }
  910. // count the number of registered events. remove listener when empty
  911. var count = 0;
  912. var events = listener.events;
  913. for (var e in events) {
  914. if (events.hasOwnProperty(e)) {
  915. count++;
  916. }
  917. }
  918. if (count == 0) {
  919. delete this.listeners[index];
  920. }
  921. }
  922. },
  923. /**
  924. * Remove all registered event listeners
  925. */
  926. 'removeAllListeners': function () {
  927. this.listeners = [];
  928. },
  929. /**
  930. * Trigger an event. All registered event handlers will be called
  931. * @param {Object} object
  932. * @param {String} event
  933. * @param {Object} properties (optional)
  934. */
  935. 'trigger': function (object, event, properties) {
  936. var index = this.indexOf(object);
  937. var listener = this.listeners[index];
  938. if (listener) {
  939. var callbacks = listener.events[event];
  940. if (callbacks) {
  941. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  942. callbacks[i](properties);
  943. }
  944. }
  945. }
  946. }
  947. };
  948. /**
  949. * An event bus can be used to emit events, and to subscribe to events
  950. * @constructor EventBus
  951. */
  952. function EventBus() {
  953. this.subscriptions = [];
  954. }
  955. /**
  956. * Subscribe to an event
  957. * @param {String | RegExp} event The event can be a regular expression, or
  958. * a string with wildcards, like 'server.*'.
  959. * @param {function} callback. Callback are called with three parameters:
  960. * {String} event, {*} [data], {*} [source]
  961. * @param {*} [target]
  962. * @returns {String} id A subscription id
  963. */
  964. EventBus.prototype.on = function (event, callback, target) {
  965. var regexp = (event instanceof RegExp) ?
  966. event :
  967. new RegExp(event.replace('*', '\\w+'));
  968. var subscription = {
  969. id: util.randomUUID(),
  970. event: event,
  971. regexp: regexp,
  972. callback: (typeof callback === 'function') ? callback : null,
  973. target: target
  974. };
  975. this.subscriptions.push(subscription);
  976. return subscription.id;
  977. };
  978. /**
  979. * Unsubscribe from an event
  980. * @param {String | Object} filter Filter for subscriptions to be removed
  981. * Filter can be a string containing a
  982. * subscription id, or an object containing
  983. * one or more of the fields id, event,
  984. * callback, and target.
  985. */
  986. EventBus.prototype.off = function (filter) {
  987. var i = 0;
  988. while (i < this.subscriptions.length) {
  989. var subscription = this.subscriptions[i];
  990. var match = true;
  991. if (filter instanceof Object) {
  992. // filter is an object. All fields must match
  993. for (var prop in filter) {
  994. if (filter.hasOwnProperty(prop)) {
  995. if (filter[prop] !== subscription[prop]) {
  996. match = false;
  997. }
  998. }
  999. }
  1000. }
  1001. else {
  1002. // filter is a string, filter on id
  1003. match = (subscription.id == filter);
  1004. }
  1005. if (match) {
  1006. this.subscriptions.splice(i, 1);
  1007. }
  1008. else {
  1009. i++;
  1010. }
  1011. }
  1012. };
  1013. /**
  1014. * Emit an event
  1015. * @param {String} event
  1016. * @param {*} [data]
  1017. * @param {*} [source]
  1018. */
  1019. EventBus.prototype.emit = function (event, data, source) {
  1020. for (var i =0; i < this.subscriptions.length; i++) {
  1021. var subscription = this.subscriptions[i];
  1022. if (subscription.regexp.test(event)) {
  1023. if (subscription.callback) {
  1024. subscription.callback(event, data, source);
  1025. }
  1026. }
  1027. }
  1028. };
  1029. /**
  1030. * DataSet
  1031. *
  1032. * Usage:
  1033. * var dataSet = new DataSet({
  1034. * fieldId: '_id',
  1035. * convert: {
  1036. * // ...
  1037. * }
  1038. * });
  1039. *
  1040. * dataSet.add(item);
  1041. * dataSet.add(data);
  1042. * dataSet.update(item);
  1043. * dataSet.update(data);
  1044. * dataSet.remove(id);
  1045. * dataSet.remove(ids);
  1046. * var data = dataSet.get();
  1047. * var data = dataSet.get(id);
  1048. * var data = dataSet.get(ids);
  1049. * var data = dataSet.get(ids, options, data);
  1050. * dataSet.clear();
  1051. *
  1052. * A data set can:
  1053. * - add/remove/update data
  1054. * - gives triggers upon changes in the data
  1055. * - can import/export data in various data formats
  1056. *
  1057. * @param {Object} [options] Available options:
  1058. * {String} fieldId Field name of the id in the
  1059. * items, 'id' by default.
  1060. * {Object.<String, String} convert
  1061. * A map with field names as key,
  1062. * and the field type as value.
  1063. * @constructor DataSet
  1064. */
  1065. // TODO: add a DataSet constructor DataSet(data, options)
  1066. function DataSet (options) {
  1067. this.id = util.randomUUID();
  1068. this.options = options || {};
  1069. this.data = {}; // map with data indexed by id
  1070. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1071. this.convert = {}; // field types by field name
  1072. if (this.options.convert) {
  1073. for (var field in this.options.convert) {
  1074. if (this.options.convert.hasOwnProperty(field)) {
  1075. var value = this.options.convert[field];
  1076. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1077. this.convert[field] = 'Date';
  1078. }
  1079. else {
  1080. this.convert[field] = value;
  1081. }
  1082. }
  1083. }
  1084. }
  1085. // event subscribers
  1086. this.subscribers = {};
  1087. this.internalIds = {}; // internally generated id's
  1088. }
  1089. /**
  1090. * Subscribe to an event, add an event listener
  1091. * @param {String} event Event name. Available events: 'put', 'update',
  1092. * 'remove'
  1093. * @param {function} callback Callback method. Called with three parameters:
  1094. * {String} event
  1095. * {Object | null} params
  1096. * {String | Number} senderId
  1097. */
  1098. DataSet.prototype.subscribe = function (event, callback) {
  1099. var subscribers = this.subscribers[event];
  1100. if (!subscribers) {
  1101. subscribers = [];
  1102. this.subscribers[event] = subscribers;
  1103. }
  1104. subscribers.push({
  1105. callback: callback
  1106. });
  1107. };
  1108. /**
  1109. * Unsubscribe from an event, remove an event listener
  1110. * @param {String} event
  1111. * @param {function} callback
  1112. */
  1113. DataSet.prototype.unsubscribe = function (event, callback) {
  1114. var subscribers = this.subscribers[event];
  1115. if (subscribers) {
  1116. this.subscribers[event] = subscribers.filter(function (listener) {
  1117. return (listener.callback != callback);
  1118. });
  1119. }
  1120. };
  1121. /**
  1122. * Trigger an event
  1123. * @param {String} event
  1124. * @param {Object | null} params
  1125. * @param {String} [senderId] Optional id of the sender.
  1126. * @private
  1127. */
  1128. DataSet.prototype._trigger = function (event, params, senderId) {
  1129. if (event == '*') {
  1130. throw new Error('Cannot trigger event *');
  1131. }
  1132. var subscribers = [];
  1133. if (event in this.subscribers) {
  1134. subscribers = subscribers.concat(this.subscribers[event]);
  1135. }
  1136. if ('*' in this.subscribers) {
  1137. subscribers = subscribers.concat(this.subscribers['*']);
  1138. }
  1139. for (var i = 0; i < subscribers.length; i++) {
  1140. var subscriber = subscribers[i];
  1141. if (subscriber.callback) {
  1142. subscriber.callback(event, params, senderId || null);
  1143. }
  1144. }
  1145. };
  1146. /**
  1147. * Add data.
  1148. * Adding an item will fail when there already is an item with the same id.
  1149. * @param {Object | Array | DataTable} data
  1150. * @param {String} [senderId] Optional sender id
  1151. * @return {Array} addedIds Array with the ids of the added items
  1152. */
  1153. DataSet.prototype.add = function (data, senderId) {
  1154. var addedIds = [],
  1155. id,
  1156. me = this;
  1157. if (data instanceof Array) {
  1158. // Array
  1159. for (var i = 0, len = data.length; i < len; i++) {
  1160. id = me._addItem(data[i]);
  1161. addedIds.push(id);
  1162. }
  1163. }
  1164. else if (util.isDataTable(data)) {
  1165. // Google DataTable
  1166. var columns = this._getColumnNames(data);
  1167. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1168. var item = {};
  1169. for (var col = 0, cols = columns.length; col < cols; col++) {
  1170. var field = columns[col];
  1171. item[field] = data.getValue(row, col);
  1172. }
  1173. id = me._addItem(item);
  1174. addedIds.push(id);
  1175. }
  1176. }
  1177. else if (data instanceof Object) {
  1178. // Single item
  1179. id = me._addItem(data);
  1180. addedIds.push(id);
  1181. }
  1182. else {
  1183. throw new Error('Unknown dataType');
  1184. }
  1185. if (addedIds.length) {
  1186. this._trigger('add', {items: addedIds}, senderId);
  1187. }
  1188. return addedIds;
  1189. };
  1190. /**
  1191. * Update existing items. When an item does not exist, it will be created
  1192. * @param {Object | Array | DataTable} data
  1193. * @param {String} [senderId] Optional sender id
  1194. * @return {Array} updatedIds The ids of the added or updated items
  1195. */
  1196. DataSet.prototype.update = function (data, senderId) {
  1197. var addedIds = [],
  1198. updatedIds = [],
  1199. me = this,
  1200. fieldId = me.fieldId;
  1201. var addOrUpdate = function (item) {
  1202. var id = item[fieldId];
  1203. if (me.data[id]) {
  1204. // update item
  1205. id = me._updateItem(item);
  1206. updatedIds.push(id);
  1207. }
  1208. else {
  1209. // add new item
  1210. id = me._addItem(item);
  1211. addedIds.push(id);
  1212. }
  1213. };
  1214. if (data instanceof Array) {
  1215. // Array
  1216. for (var i = 0, len = data.length; i < len; i++) {
  1217. addOrUpdate(data[i]);
  1218. }
  1219. }
  1220. else if (util.isDataTable(data)) {
  1221. // Google DataTable
  1222. var columns = this._getColumnNames(data);
  1223. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1224. var item = {};
  1225. for (var col = 0, cols = columns.length; col < cols; col++) {
  1226. var field = columns[col];
  1227. item[field] = data.getValue(row, col);
  1228. }
  1229. addOrUpdate(item);
  1230. }
  1231. }
  1232. else if (data instanceof Object) {
  1233. // Single item
  1234. addOrUpdate(data);
  1235. }
  1236. else {
  1237. throw new Error('Unknown dataType');
  1238. }
  1239. if (addedIds.length) {
  1240. this._trigger('add', {items: addedIds}, senderId);
  1241. }
  1242. if (updatedIds.length) {
  1243. this._trigger('update', {items: updatedIds}, senderId);
  1244. }
  1245. return addedIds.concat(updatedIds);
  1246. };
  1247. /**
  1248. * Get a data item or multiple items.
  1249. *
  1250. * Usage:
  1251. *
  1252. * get()
  1253. * get(options: Object)
  1254. * get(options: Object, data: Array | DataTable)
  1255. *
  1256. * get(id: Number | String)
  1257. * get(id: Number | String, options: Object)
  1258. * get(id: Number | String, options: Object, data: Array | DataTable)
  1259. *
  1260. * get(ids: Number[] | String[])
  1261. * get(ids: Number[] | String[], options: Object)
  1262. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1263. *
  1264. * Where:
  1265. *
  1266. * {Number | String} id The id of an item
  1267. * {Number[] | String{}} ids An array with ids of items
  1268. * {Object} options An Object with options. Available options:
  1269. * {String} [type] Type of data to be returned. Can
  1270. * be 'DataTable' or 'Array' (default)
  1271. * {Object.<String, String>} [convert]
  1272. * {String[]} [fields] field names to be returned
  1273. * {function} [filter] filter items
  1274. * {String | function} [order] Order the items by
  1275. * a field name or custom sort function.
  1276. * {Array | DataTable} [data] If provided, items will be appended to this
  1277. * array or table. Required in case of Google
  1278. * DataTable.
  1279. *
  1280. * @throws Error
  1281. */
  1282. DataSet.prototype.get = function (args) {
  1283. var me = this;
  1284. // parse the arguments
  1285. var id, ids, options, data;
  1286. var firstType = util.getType(arguments[0]);
  1287. if (firstType == 'String' || firstType == 'Number') {
  1288. // get(id [, options] [, data])
  1289. id = arguments[0];
  1290. options = arguments[1];
  1291. data = arguments[2];
  1292. }
  1293. else if (firstType == 'Array') {
  1294. // get(ids [, options] [, data])
  1295. ids = arguments[0];
  1296. options = arguments[1];
  1297. data = arguments[2];
  1298. }
  1299. else {
  1300. // get([, options] [, data])
  1301. options = arguments[0];
  1302. data = arguments[1];
  1303. }
  1304. // determine the return type
  1305. var type;
  1306. if (options && options.type) {
  1307. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1308. if (data && (type != util.getType(data))) {
  1309. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1310. 'does not correspond with specified options.type (' + options.type + ')');
  1311. }
  1312. if (type == 'DataTable' && !util.isDataTable(data)) {
  1313. throw new Error('Parameter "data" must be a DataTable ' +
  1314. 'when options.type is "DataTable"');
  1315. }
  1316. }
  1317. else if (data) {
  1318. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1319. }
  1320. else {
  1321. type = 'Array';
  1322. }
  1323. // build options
  1324. var convert = options && options.convert || this.options.convert;
  1325. var filter = options && options.filter;
  1326. var items = [], item, itemId, i, len;
  1327. // convert items
  1328. if (id != undefined) {
  1329. // return a single item
  1330. item = me._getItem(id, convert);
  1331. if (filter && !filter(item)) {
  1332. item = null;
  1333. }
  1334. }
  1335. else if (ids != undefined) {
  1336. // return a subset of items
  1337. for (i = 0, len = ids.length; i < len; i++) {
  1338. item = me._getItem(ids[i], convert);
  1339. if (!filter || filter(item)) {
  1340. items.push(item);
  1341. }
  1342. }
  1343. }
  1344. else {
  1345. // return all items
  1346. for (itemId in this.data) {
  1347. if (this.data.hasOwnProperty(itemId)) {
  1348. item = me._getItem(itemId, convert);
  1349. if (!filter || filter(item)) {
  1350. items.push(item);
  1351. }
  1352. }
  1353. }
  1354. }
  1355. // order the results
  1356. if (options && options.order && id == undefined) {
  1357. this._sort(items, options.order);
  1358. }
  1359. // filter fields of the items
  1360. if (options && options.fields) {
  1361. var fields = options.fields;
  1362. if (id != undefined) {
  1363. item = this._filterFields(item, fields);
  1364. }
  1365. else {
  1366. for (i = 0, len = items.length; i < len; i++) {
  1367. items[i] = this._filterFields(items[i], fields);
  1368. }
  1369. }
  1370. }
  1371. // return the results
  1372. if (type == 'DataTable') {
  1373. var columns = this._getColumnNames(data);
  1374. if (id != undefined) {
  1375. // append a single item to the data table
  1376. me._appendRow(data, columns, item);
  1377. }
  1378. else {
  1379. // copy the items to the provided data table
  1380. for (i = 0, len = items.length; i < len; i++) {
  1381. me._appendRow(data, columns, items[i]);
  1382. }
  1383. }
  1384. return data;
  1385. }
  1386. else {
  1387. // return an array
  1388. if (id != undefined) {
  1389. // a single item
  1390. return item;
  1391. }
  1392. else {
  1393. // multiple items
  1394. if (data) {
  1395. // copy the items to the provided array
  1396. for (i = 0, len = items.length; i < len; i++) {
  1397. data.push(items[i]);
  1398. }
  1399. return data;
  1400. }
  1401. else {
  1402. // just return our array
  1403. return items;
  1404. }
  1405. }
  1406. }
  1407. };
  1408. /**
  1409. * Get ids of all items or from a filtered set of items.
  1410. * @param {Object} [options] An Object with options. Available options:
  1411. * {function} [filter] filter items
  1412. * {String | function} [order] Order the items by
  1413. * a field name or custom sort function.
  1414. * @return {Array} ids
  1415. */
  1416. DataSet.prototype.getIds = function (options) {
  1417. var data = this.data,
  1418. filter = options && options.filter,
  1419. order = options && options.order,
  1420. convert = options && options.convert || this.options.convert,
  1421. i,
  1422. len,
  1423. id,
  1424. item,
  1425. items,
  1426. ids = [];
  1427. if (filter) {
  1428. // get filtered items
  1429. if (order) {
  1430. // create ordered list
  1431. items = [];
  1432. for (id in data) {
  1433. if (data.hasOwnProperty(id)) {
  1434. item = this._getItem(id, convert);
  1435. if (filter(item)) {
  1436. items.push(item);
  1437. }
  1438. }
  1439. }
  1440. this._sort(items, order);
  1441. for (i = 0, len = items.length; i < len; i++) {
  1442. ids[i] = items[i][this.fieldId];
  1443. }
  1444. }
  1445. else {
  1446. // create unordered list
  1447. for (id in data) {
  1448. if (data.hasOwnProperty(id)) {
  1449. item = this._getItem(id, convert);
  1450. if (filter(item)) {
  1451. ids.push(item[this.fieldId]);
  1452. }
  1453. }
  1454. }
  1455. }
  1456. }
  1457. else {
  1458. // get all items
  1459. if (order) {
  1460. // create an ordered list
  1461. items = [];
  1462. for (id in data) {
  1463. if (data.hasOwnProperty(id)) {
  1464. items.push(data[id]);
  1465. }
  1466. }
  1467. this._sort(items, order);
  1468. for (i = 0, len = items.length; i < len; i++) {
  1469. ids[i] = items[i][this.fieldId];
  1470. }
  1471. }
  1472. else {
  1473. // create unordered list
  1474. for (id in data) {
  1475. if (data.hasOwnProperty(id)) {
  1476. item = data[id];
  1477. ids.push(item[this.fieldId]);
  1478. }
  1479. }
  1480. }
  1481. }
  1482. return ids;
  1483. };
  1484. /**
  1485. * Execute a callback function for every item in the dataset.
  1486. * The order of the items is not determined.
  1487. * @param {function} callback
  1488. * @param {Object} [options] Available options:
  1489. * {Object.<String, String>} [convert]
  1490. * {String[]} [fields] filter fields
  1491. * {function} [filter] filter items
  1492. * {String | function} [order] Order the items by
  1493. * a field name or custom sort function.
  1494. */
  1495. DataSet.prototype.forEach = function (callback, options) {
  1496. var filter = options && options.filter,
  1497. convert = options && options.convert || this.options.convert,
  1498. data = this.data,
  1499. item,
  1500. id;
  1501. if (options && options.order) {
  1502. // execute forEach on ordered list
  1503. var items = this.get(options);
  1504. for (var i = 0, len = items.length; i < len; i++) {
  1505. item = items[i];
  1506. id = item[this.fieldId];
  1507. callback(item, id);
  1508. }
  1509. }
  1510. else {
  1511. // unordered
  1512. for (id in data) {
  1513. if (data.hasOwnProperty(id)) {
  1514. item = this._getItem(id, convert);
  1515. if (!filter || filter(item)) {
  1516. callback(item, id);
  1517. }
  1518. }
  1519. }
  1520. }
  1521. };
  1522. /**
  1523. * Map every item in the dataset.
  1524. * @param {function} callback
  1525. * @param {Object} [options] Available options:
  1526. * {Object.<String, String>} [convert]
  1527. * {String[]} [fields] filter fields
  1528. * {function} [filter] filter items
  1529. * {String | function} [order] Order the items by
  1530. * a field name or custom sort function.
  1531. * @return {Object[]} mappedItems
  1532. */
  1533. DataSet.prototype.map = function (callback, options) {
  1534. var filter = options && options.filter,
  1535. convert = options && options.convert || this.options.convert,
  1536. mappedItems = [],
  1537. data = this.data,
  1538. item;
  1539. // convert and filter items
  1540. for (var id in data) {
  1541. if (data.hasOwnProperty(id)) {
  1542. item = this._getItem(id, convert);
  1543. if (!filter || filter(item)) {
  1544. mappedItems.push(callback(item, id));
  1545. }
  1546. }
  1547. }
  1548. // order items
  1549. if (options && options.order) {
  1550. this._sort(mappedItems, options.order);
  1551. }
  1552. return mappedItems;
  1553. };
  1554. /**
  1555. * Filter the fields of an item
  1556. * @param {Object} item
  1557. * @param {String[]} fields Field names
  1558. * @return {Object} filteredItem
  1559. * @private
  1560. */
  1561. DataSet.prototype._filterFields = function (item, fields) {
  1562. var filteredItem = {};
  1563. for (var field in item) {
  1564. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1565. filteredItem[field] = item[field];
  1566. }
  1567. }
  1568. return filteredItem;
  1569. };
  1570. /**
  1571. * Sort the provided array with items
  1572. * @param {Object[]} items
  1573. * @param {String | function} order A field name or custom sort function.
  1574. * @private
  1575. */
  1576. DataSet.prototype._sort = function (items, order) {
  1577. if (util.isString(order)) {
  1578. // order by provided field name
  1579. var name = order; // field name
  1580. items.sort(function (a, b) {
  1581. var av = a[name];
  1582. var bv = b[name];
  1583. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1584. });
  1585. }
  1586. else if (typeof order === 'function') {
  1587. // order by sort function
  1588. items.sort(order);
  1589. }
  1590. // TODO: extend order by an Object {field:String, direction:String}
  1591. // where direction can be 'asc' or 'desc'
  1592. else {
  1593. throw new TypeError('Order must be a function or a string');
  1594. }
  1595. };
  1596. /**
  1597. * Remove an object by pointer or by id
  1598. * @param {String | Number | Object | Array} id Object or id, or an array with
  1599. * objects or ids to be removed
  1600. * @param {String} [senderId] Optional sender id
  1601. * @return {Array} removedIds
  1602. */
  1603. DataSet.prototype.remove = function (id, senderId) {
  1604. var removedIds = [],
  1605. i, len, removedId;
  1606. if (id instanceof Array) {
  1607. for (i = 0, len = id.length; i < len; i++) {
  1608. removedId = this._remove(id[i]);
  1609. if (removedId != null) {
  1610. removedIds.push(removedId);
  1611. }
  1612. }
  1613. }
  1614. else {
  1615. removedId = this._remove(id);
  1616. if (removedId != null) {
  1617. removedIds.push(removedId);
  1618. }
  1619. }
  1620. if (removedIds.length) {
  1621. this._trigger('remove', {items: removedIds}, senderId);
  1622. }
  1623. return removedIds;
  1624. };
  1625. /**
  1626. * Remove an item by its id
  1627. * @param {Number | String | Object} id id or item
  1628. * @returns {Number | String | null} id
  1629. * @private
  1630. */
  1631. DataSet.prototype._remove = function (id) {
  1632. if (util.isNumber(id) || util.isString(id)) {
  1633. if (this.data[id]) {
  1634. delete this.data[id];
  1635. delete this.internalIds[id];
  1636. return id;
  1637. }
  1638. }
  1639. else if (id instanceof Object) {
  1640. var itemId = id[this.fieldId];
  1641. if (itemId && this.data[itemId]) {
  1642. delete this.data[itemId];
  1643. delete this.internalIds[itemId];
  1644. return itemId;
  1645. }
  1646. }
  1647. return null;
  1648. };
  1649. /**
  1650. * Clear the data
  1651. * @param {String} [senderId] Optional sender id
  1652. * @return {Array} removedIds The ids of all removed items
  1653. */
  1654. DataSet.prototype.clear = function (senderId) {
  1655. var ids = Object.keys(this.data);
  1656. this.data = {};
  1657. this.internalIds = {};
  1658. this._trigger('remove', {items: ids}, senderId);
  1659. return ids;
  1660. };
  1661. /**
  1662. * Find the item with maximum value of a specified field
  1663. * @param {String} field
  1664. * @return {Object | null} item Item containing max value, or null if no items
  1665. */
  1666. DataSet.prototype.max = function (field) {
  1667. var data = this.data,
  1668. max = null,
  1669. maxField = null;
  1670. for (var id in data) {
  1671. if (data.hasOwnProperty(id)) {
  1672. var item = data[id];
  1673. var itemField = item[field];
  1674. if (itemField != null && (!max || itemField > maxField)) {
  1675. max = item;
  1676. maxField = itemField;
  1677. }
  1678. }
  1679. }
  1680. return max;
  1681. };
  1682. /**
  1683. * Find the item with minimum value of a specified field
  1684. * @param {String} field
  1685. * @return {Object | null} item Item containing max value, or null if no items
  1686. */
  1687. DataSet.prototype.min = function (field) {
  1688. var data = this.data,
  1689. min = null,
  1690. minField = null;
  1691. for (var id in data) {
  1692. if (data.hasOwnProperty(id)) {
  1693. var item = data[id];
  1694. var itemField = item[field];
  1695. if (itemField != null && (!min || itemField < minField)) {
  1696. min = item;
  1697. minField = itemField;
  1698. }
  1699. }
  1700. }
  1701. return min;
  1702. };
  1703. /**
  1704. * Find all distinct values of a specified field
  1705. * @param {String} field
  1706. * @return {Array} values Array containing all distinct values. If the data
  1707. * items do not contain the specified field, an array
  1708. * containing a single value undefined is returned.
  1709. * The returned array is unordered.
  1710. */
  1711. DataSet.prototype.distinct = function (field) {
  1712. var data = this.data,
  1713. values = [],
  1714. fieldType = this.options.convert[field],
  1715. count = 0;
  1716. for (var prop in data) {
  1717. if (data.hasOwnProperty(prop)) {
  1718. var item = data[prop];
  1719. var value = util.convert(item[field], fieldType);
  1720. var exists = false;
  1721. for (var i = 0; i < count; i++) {
  1722. if (values[i] == value) {
  1723. exists = true;
  1724. break;
  1725. }
  1726. }
  1727. if (!exists) {
  1728. values[count] = value;
  1729. count++;
  1730. }
  1731. }
  1732. }
  1733. return values;
  1734. };
  1735. /**
  1736. * Add a single item. Will fail when an item with the same id already exists.
  1737. * @param {Object} item
  1738. * @return {String} id
  1739. * @private
  1740. */
  1741. DataSet.prototype._addItem = function (item) {
  1742. var id = item[this.fieldId];
  1743. if (id != undefined) {
  1744. // check whether this id is already taken
  1745. if (this.data[id]) {
  1746. // item already exists
  1747. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1748. }
  1749. }
  1750. else {
  1751. // generate an id
  1752. id = util.randomUUID();
  1753. item[this.fieldId] = id;
  1754. this.internalIds[id] = item;
  1755. }
  1756. var d = {};
  1757. for (var field in item) {
  1758. if (item.hasOwnProperty(field)) {
  1759. var fieldType = this.convert[field]; // type may be undefined
  1760. d[field] = util.convert(item[field], fieldType);
  1761. }
  1762. }
  1763. this.data[id] = d;
  1764. return id;
  1765. };
  1766. /**
  1767. * Get an item. Fields can be converted to a specific type
  1768. * @param {String} id
  1769. * @param {Object.<String, String>} [convert] field types to convert
  1770. * @return {Object | null} item
  1771. * @private
  1772. */
  1773. DataSet.prototype._getItem = function (id, convert) {
  1774. var field, value;
  1775. // get the item from the dataset
  1776. var raw = this.data[id];
  1777. if (!raw) {
  1778. return null;
  1779. }
  1780. // convert the items field types
  1781. var converted = {},
  1782. fieldId = this.fieldId,
  1783. internalIds = this.internalIds;
  1784. if (convert) {
  1785. for (field in raw) {
  1786. if (raw.hasOwnProperty(field)) {
  1787. value = raw[field];
  1788. // output all fields, except internal ids
  1789. if ((field != fieldId) || !(value in internalIds)) {
  1790. converted[field] = util.convert(value, convert[field]);
  1791. }
  1792. }
  1793. }
  1794. }
  1795. else {
  1796. // no field types specified, no converting needed
  1797. for (field in raw) {
  1798. if (raw.hasOwnProperty(field)) {
  1799. value = raw[field];
  1800. // output all fields, except internal ids
  1801. if ((field != fieldId) || !(value in internalIds)) {
  1802. converted[field] = value;
  1803. }
  1804. }
  1805. }
  1806. }
  1807. return converted;
  1808. };
  1809. /**
  1810. * Update a single item: merge with existing item.
  1811. * Will fail when the item has no id, or when there does not exist an item
  1812. * with the same id.
  1813. * @param {Object} item
  1814. * @return {String} id
  1815. * @private
  1816. */
  1817. DataSet.prototype._updateItem = function (item) {
  1818. var id = item[this.fieldId];
  1819. if (id == undefined) {
  1820. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1821. }
  1822. var d = this.data[id];
  1823. if (!d) {
  1824. // item doesn't exist
  1825. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1826. }
  1827. // merge with current item
  1828. for (var field in item) {
  1829. if (item.hasOwnProperty(field)) {
  1830. var fieldType = this.convert[field]; // type may be undefined
  1831. d[field] = util.convert(item[field], fieldType);
  1832. }
  1833. }
  1834. return id;
  1835. };
  1836. /**
  1837. * Get an array with the column names of a Google DataTable
  1838. * @param {DataTable} dataTable
  1839. * @return {String[]} columnNames
  1840. * @private
  1841. */
  1842. DataSet.prototype._getColumnNames = function (dataTable) {
  1843. var columns = [];
  1844. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1845. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1846. }
  1847. return columns;
  1848. };
  1849. /**
  1850. * Append an item as a row to the dataTable
  1851. * @param dataTable
  1852. * @param columns
  1853. * @param item
  1854. * @private
  1855. */
  1856. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1857. var row = dataTable.addRow();
  1858. for (var col = 0, cols = columns.length; col < cols; col++) {
  1859. var field = columns[col];
  1860. dataTable.setValue(row, col, item[field]);
  1861. }
  1862. };
  1863. /**
  1864. * DataView
  1865. *
  1866. * a dataview offers a filtered view on a dataset or an other dataview.
  1867. *
  1868. * @param {DataSet | DataView} data
  1869. * @param {Object} [options] Available options: see method get
  1870. *
  1871. * @constructor DataView
  1872. */
  1873. function DataView (data, options) {
  1874. this.id = util.randomUUID();
  1875. this.data = null;
  1876. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1877. this.options = options || {};
  1878. this.fieldId = 'id'; // name of the field containing id
  1879. this.subscribers = {}; // event subscribers
  1880. var me = this;
  1881. this.listener = function () {
  1882. me._onEvent.apply(me, arguments);
  1883. };
  1884. this.setData(data);
  1885. }
  1886. // TODO: implement a function .config() to dynamically update things like configured filter
  1887. // and trigger changes accordingly
  1888. /**
  1889. * Set a data source for the view
  1890. * @param {DataSet | DataView} data
  1891. */
  1892. DataView.prototype.setData = function (data) {
  1893. var ids, dataItems, i, len;
  1894. if (this.data) {
  1895. // unsubscribe from current dataset
  1896. if (this.data.unsubscribe) {
  1897. this.data.unsubscribe('*', this.listener);
  1898. }
  1899. // trigger a remove of all items in memory
  1900. ids = [];
  1901. for (var id in this.ids) {
  1902. if (this.ids.hasOwnProperty(id)) {
  1903. ids.push(id);
  1904. }
  1905. }
  1906. this.ids = {};
  1907. this._trigger('remove', {items: ids});
  1908. }
  1909. this.data = data;
  1910. if (this.data) {
  1911. // update fieldId
  1912. this.fieldId = this.options.fieldId ||
  1913. (this.data && this.data.options && this.data.options.fieldId) ||
  1914. 'id';
  1915. // trigger an add of all added items
  1916. ids = this.data.getIds({filter: this.options && this.options.filter});
  1917. for (i = 0, len = ids.length; i < len; i++) {
  1918. id = ids[i];
  1919. this.ids[id] = true;
  1920. }
  1921. this._trigger('add', {items: ids});
  1922. // subscribe to new dataset
  1923. if (this.data.subscribe) {
  1924. this.data.subscribe('*', this.listener);
  1925. }
  1926. }
  1927. };
  1928. /**
  1929. * Get data from the data view
  1930. *
  1931. * Usage:
  1932. *
  1933. * get()
  1934. * get(options: Object)
  1935. * get(options: Object, data: Array | DataTable)
  1936. *
  1937. * get(id: Number)
  1938. * get(id: Number, options: Object)
  1939. * get(id: Number, options: Object, data: Array | DataTable)
  1940. *
  1941. * get(ids: Number[])
  1942. * get(ids: Number[], options: Object)
  1943. * get(ids: Number[], options: Object, data: Array | DataTable)
  1944. *
  1945. * Where:
  1946. *
  1947. * {Number | String} id The id of an item
  1948. * {Number[] | String{}} ids An array with ids of items
  1949. * {Object} options An Object with options. Available options:
  1950. * {String} [type] Type of data to be returned. Can
  1951. * be 'DataTable' or 'Array' (default)
  1952. * {Object.<String, String>} [convert]
  1953. * {String[]} [fields] field names to be returned
  1954. * {function} [filter] filter items
  1955. * {String | function} [order] Order the items by
  1956. * a field name or custom sort function.
  1957. * {Array | DataTable} [data] If provided, items will be appended to this
  1958. * array or table. Required in case of Google
  1959. * DataTable.
  1960. * @param args
  1961. */
  1962. DataView.prototype.get = function (args) {
  1963. var me = this;
  1964. // parse the arguments
  1965. var ids, options, data;
  1966. var firstType = util.getType(arguments[0]);
  1967. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1968. // get(id(s) [, options] [, data])
  1969. ids = arguments[0]; // can be a single id or an array with ids
  1970. options = arguments[1];
  1971. data = arguments[2];
  1972. }
  1973. else {
  1974. // get([, options] [, data])
  1975. options = arguments[0];
  1976. data = arguments[1];
  1977. }
  1978. // extend the options with the default options and provided options
  1979. var viewOptions = util.extend({}, this.options, options);
  1980. // create a combined filter method when needed
  1981. if (this.options.filter && options && options.filter) {
  1982. viewOptions.filter = function (item) {
  1983. return me.options.filter(item) && options.filter(item);
  1984. }
  1985. }
  1986. // build up the call to the linked data set
  1987. var getArguments = [];
  1988. if (ids != undefined) {
  1989. getArguments.push(ids);
  1990. }
  1991. getArguments.push(viewOptions);
  1992. getArguments.push(data);
  1993. return this.data && this.data.get.apply(this.data, getArguments);
  1994. };
  1995. /**
  1996. * Get ids of all items or from a filtered set of items.
  1997. * @param {Object} [options] An Object with options. Available options:
  1998. * {function} [filter] filter items
  1999. * {String | function} [order] Order the items by
  2000. * a field name or custom sort function.
  2001. * @return {Array} ids
  2002. */
  2003. DataView.prototype.getIds = function (options) {
  2004. var ids;
  2005. if (this.data) {
  2006. var defaultFilter = this.options.filter;
  2007. var filter;
  2008. if (options && options.filter) {
  2009. if (defaultFilter) {
  2010. filter = function (item) {
  2011. return defaultFilter(item) && options.filter(item);
  2012. }
  2013. }
  2014. else {
  2015. filter = options.filter;
  2016. }
  2017. }
  2018. else {
  2019. filter = defaultFilter;
  2020. }
  2021. ids = this.data.getIds({
  2022. filter: filter,
  2023. order: options && options.order
  2024. });
  2025. }
  2026. else {
  2027. ids = [];
  2028. }
  2029. return ids;
  2030. };
  2031. /**
  2032. * Event listener. Will propagate all events from the connected data set to
  2033. * the subscribers of the DataView, but will filter the items and only trigger
  2034. * when there are changes in the filtered data set.
  2035. * @param {String} event
  2036. * @param {Object | null} params
  2037. * @param {String} senderId
  2038. * @private
  2039. */
  2040. DataView.prototype._onEvent = function (event, params, senderId) {
  2041. var i, len, id, item,
  2042. ids = params && params.items,
  2043. data = this.data,
  2044. added = [],
  2045. updated = [],
  2046. removed = [];
  2047. if (ids && data) {
  2048. switch (event) {
  2049. case 'add':
  2050. // filter the ids of the added items
  2051. for (i = 0, len = ids.length; i < len; i++) {
  2052. id = ids[i];
  2053. item = this.get(id);
  2054. if (item) {
  2055. this.ids[id] = true;
  2056. added.push(id);
  2057. }
  2058. }
  2059. break;
  2060. case 'update':
  2061. // determine the event from the views viewpoint: an updated
  2062. // item can be added, updated, or removed from this view.
  2063. for (i = 0, len = ids.length; i < len; i++) {
  2064. id = ids[i];
  2065. item = this.get(id);
  2066. if (item) {
  2067. if (this.ids[id]) {
  2068. updated.push(id);
  2069. }
  2070. else {
  2071. this.ids[id] = true;
  2072. added.push(id);
  2073. }
  2074. }
  2075. else {
  2076. if (this.ids[id]) {
  2077. delete this.ids[id];
  2078. removed.push(id);
  2079. }
  2080. else {
  2081. // nothing interesting for me :-(
  2082. }
  2083. }
  2084. }
  2085. break;
  2086. case 'remove':
  2087. // filter the ids of the removed items
  2088. for (i = 0, len = ids.length; i < len; i++) {
  2089. id = ids[i];
  2090. if (this.ids[id]) {
  2091. delete this.ids[id];
  2092. removed.push(id);
  2093. }
  2094. }
  2095. break;
  2096. }
  2097. if (added.length) {
  2098. this._trigger('add', {items: added}, senderId);
  2099. }
  2100. if (updated.length) {
  2101. this._trigger('update', {items: updated}, senderId);
  2102. }
  2103. if (removed.length) {
  2104. this._trigger('remove', {items: removed}, senderId);
  2105. }
  2106. }
  2107. };
  2108. // copy subscription functionality from DataSet
  2109. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2110. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2111. DataView.prototype._trigger = DataSet.prototype._trigger;
  2112. /**
  2113. * @constructor TimeStep
  2114. * The class TimeStep is an iterator for dates. You provide a start date and an
  2115. * end date. The class itself determines the best scale (step size) based on the
  2116. * provided start Date, end Date, and minimumStep.
  2117. *
  2118. * If minimumStep is provided, the step size is chosen as close as possible
  2119. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2120. * provided, the scale is set to 1 DAY.
  2121. * The minimumStep should correspond with the onscreen size of about 6 characters
  2122. *
  2123. * Alternatively, you can set a scale by hand.
  2124. * After creation, you can initialize the class by executing first(). Then you
  2125. * can iterate from the start date to the end date via next(). You can check if
  2126. * the end date is reached with the function hasNext(). After each step, you can
  2127. * retrieve the current date via getCurrent().
  2128. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2129. * days, to years.
  2130. *
  2131. * Version: 1.2
  2132. *
  2133. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2134. * or new Date(2010, 9, 21, 23, 45, 00)
  2135. * @param {Date} [end] The end date
  2136. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2137. */
  2138. TimeStep = function(start, end, minimumStep) {
  2139. // variables
  2140. this.current = new Date();
  2141. this._start = new Date();
  2142. this._end = new Date();
  2143. this.autoScale = true;
  2144. this.scale = TimeStep.SCALE.DAY;
  2145. this.step = 1;
  2146. // initialize the range
  2147. this.setRange(start, end, minimumStep);
  2148. };
  2149. /// enum scale
  2150. TimeStep.SCALE = {
  2151. MILLISECOND: 1,
  2152. SECOND: 2,
  2153. MINUTE: 3,
  2154. HOUR: 4,
  2155. DAY: 5,
  2156. WEEKDAY: 6,
  2157. MONTH: 7,
  2158. YEAR: 8
  2159. };
  2160. /**
  2161. * Set a new range
  2162. * If minimumStep is provided, the step size is chosen as close as possible
  2163. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2164. * provided, the scale is set to 1 DAY.
  2165. * The minimumStep should correspond with the onscreen size of about 6 characters
  2166. * @param {Date} [start] The start date and time.
  2167. * @param {Date} [end] The end date and time.
  2168. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2169. */
  2170. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2171. if (!(start instanceof Date) || !(end instanceof Date)) {
  2172. throw "No legal start or end date in method setRange";
  2173. }
  2174. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2175. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2176. if (this.autoScale) {
  2177. this.setMinimumStep(minimumStep);
  2178. }
  2179. };
  2180. /**
  2181. * Set the range iterator to the start date.
  2182. */
  2183. TimeStep.prototype.first = function() {
  2184. this.current = new Date(this._start.valueOf());
  2185. this.roundToMinor();
  2186. };
  2187. /**
  2188. * Round the current date to the first minor date value
  2189. * This must be executed once when the current date is set to start Date
  2190. */
  2191. TimeStep.prototype.roundToMinor = function() {
  2192. // round to floor
  2193. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2194. //noinspection FallthroughInSwitchStatementJS
  2195. switch (this.scale) {
  2196. case TimeStep.SCALE.YEAR:
  2197. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2198. this.current.setMonth(0);
  2199. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2200. case TimeStep.SCALE.DAY: // intentional fall through
  2201. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2202. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2203. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2204. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2205. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2206. }
  2207. if (this.step != 1) {
  2208. // round down to the first minor value that is a multiple of the current step size
  2209. switch (this.scale) {
  2210. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2211. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2212. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2213. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2214. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2215. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2216. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2217. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2218. default: break;
  2219. }
  2220. }
  2221. };
  2222. /**
  2223. * Check if the there is a next step
  2224. * @return {boolean} true if the current date has not passed the end date
  2225. */
  2226. TimeStep.prototype.hasNext = function () {
  2227. return (this.current.valueOf() <= this._end.valueOf());
  2228. };
  2229. /**
  2230. * Do the next step
  2231. */
  2232. TimeStep.prototype.next = function() {
  2233. var prev = this.current.valueOf();
  2234. // Two cases, needed to prevent issues with switching daylight savings
  2235. // (end of March and end of October)
  2236. if (this.current.getMonth() < 6) {
  2237. switch (this.scale) {
  2238. case TimeStep.SCALE.MILLISECOND:
  2239. this.current = new Date(this.current.valueOf() + this.step); break;
  2240. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2241. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2242. case TimeStep.SCALE.HOUR:
  2243. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2244. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2245. var h = this.current.getHours();
  2246. this.current.setHours(h - (h % this.step));
  2247. break;
  2248. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2249. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2250. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2251. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2252. default: break;
  2253. }
  2254. }
  2255. else {
  2256. switch (this.scale) {
  2257. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2258. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2259. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2260. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2261. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2262. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2263. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2264. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2265. default: break;
  2266. }
  2267. }
  2268. if (this.step != 1) {
  2269. // round down to the correct major value
  2270. switch (this.scale) {
  2271. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2272. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2273. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2274. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2275. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2276. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2277. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2278. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2279. default: break;
  2280. }
  2281. }
  2282. // safety mechanism: if current time is still unchanged, move to the end
  2283. if (this.current.valueOf() == prev) {
  2284. this.current = new Date(this._end.valueOf());
  2285. }
  2286. };
  2287. /**
  2288. * Get the current datetime
  2289. * @return {Date} current The current date
  2290. */
  2291. TimeStep.prototype.getCurrent = function() {
  2292. return this.current;
  2293. };
  2294. /**
  2295. * Set a custom scale. Autoscaling will be disabled.
  2296. * For example setScale(SCALE.MINUTES, 5) will result
  2297. * in minor steps of 5 minutes, and major steps of an hour.
  2298. *
  2299. * @param {TimeStep.SCALE} newScale
  2300. * A scale. Choose from SCALE.MILLISECOND,
  2301. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2302. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2303. * SCALE.YEAR.
  2304. * @param {Number} newStep A step size, by default 1. Choose for
  2305. * example 1, 2, 5, or 10.
  2306. */
  2307. TimeStep.prototype.setScale = function(newScale, newStep) {
  2308. this.scale = newScale;
  2309. if (newStep > 0) {
  2310. this.step = newStep;
  2311. }
  2312. this.autoScale = false;
  2313. };
  2314. /**
  2315. * Enable or disable autoscaling
  2316. * @param {boolean} enable If true, autoascaling is set true
  2317. */
  2318. TimeStep.prototype.setAutoScale = function (enable) {
  2319. this.autoScale = enable;
  2320. };
  2321. /**
  2322. * Automatically determine the scale that bests fits the provided minimum step
  2323. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2324. */
  2325. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2326. if (minimumStep == undefined) {
  2327. return;
  2328. }
  2329. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2330. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2331. var stepDay = (1000 * 60 * 60 * 24);
  2332. var stepHour = (1000 * 60 * 60);
  2333. var stepMinute = (1000 * 60);
  2334. var stepSecond = (1000);
  2335. var stepMillisecond= (1);
  2336. // find the smallest step that is larger than the provided minimumStep
  2337. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2338. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2339. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2340. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2341. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2342. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2343. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2344. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2345. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2346. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2347. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2348. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2349. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2350. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2351. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2352. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2353. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2354. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2355. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2356. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2357. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2358. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2359. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2360. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2361. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2362. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2363. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2364. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2365. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2366. };
  2367. /**
  2368. * Snap a date to a rounded value. The snap intervals are dependent on the
  2369. * current scale and step.
  2370. * @param {Date} date the date to be snapped
  2371. */
  2372. TimeStep.prototype.snap = function(date) {
  2373. if (this.scale == TimeStep.SCALE.YEAR) {
  2374. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2375. date.setFullYear(Math.round(year / this.step) * this.step);
  2376. date.setMonth(0);
  2377. date.setDate(0);
  2378. date.setHours(0);
  2379. date.setMinutes(0);
  2380. date.setSeconds(0);
  2381. date.setMilliseconds(0);
  2382. }
  2383. else if (this.scale == TimeStep.SCALE.MONTH) {
  2384. if (date.getDate() > 15) {
  2385. date.setDate(1);
  2386. date.setMonth(date.getMonth() + 1);
  2387. // important: first set Date to 1, after that change the month.
  2388. }
  2389. else {
  2390. date.setDate(1);
  2391. }
  2392. date.setHours(0);
  2393. date.setMinutes(0);
  2394. date.setSeconds(0);
  2395. date.setMilliseconds(0);
  2396. }
  2397. else if (this.scale == TimeStep.SCALE.DAY ||
  2398. this.scale == TimeStep.SCALE.WEEKDAY) {
  2399. //noinspection FallthroughInSwitchStatementJS
  2400. switch (this.step) {
  2401. case 5:
  2402. case 2:
  2403. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2404. default:
  2405. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2406. }
  2407. date.setMinutes(0);
  2408. date.setSeconds(0);
  2409. date.setMilliseconds(0);
  2410. }
  2411. else if (this.scale == TimeStep.SCALE.HOUR) {
  2412. switch (this.step) {
  2413. case 4:
  2414. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2415. default:
  2416. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2417. }
  2418. date.setSeconds(0);
  2419. date.setMilliseconds(0);
  2420. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2421. //noinspection FallthroughInSwitchStatementJS
  2422. switch (this.step) {
  2423. case 15:
  2424. case 10:
  2425. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2426. date.setSeconds(0);
  2427. break;
  2428. case 5:
  2429. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2430. default:
  2431. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2432. }
  2433. date.setMilliseconds(0);
  2434. }
  2435. else if (this.scale == TimeStep.SCALE.SECOND) {
  2436. //noinspection FallthroughInSwitchStatementJS
  2437. switch (this.step) {
  2438. case 15:
  2439. case 10:
  2440. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2441. date.setMilliseconds(0);
  2442. break;
  2443. case 5:
  2444. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2445. default:
  2446. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2447. }
  2448. }
  2449. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2450. var step = this.step > 5 ? this.step / 2 : 1;
  2451. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2452. }
  2453. };
  2454. /**
  2455. * Check if the current value is a major value (for example when the step
  2456. * is DAY, a major value is each first day of the MONTH)
  2457. * @return {boolean} true if current date is major, else false.
  2458. */
  2459. TimeStep.prototype.isMajor = function() {
  2460. switch (this.scale) {
  2461. case TimeStep.SCALE.MILLISECOND:
  2462. return (this.current.getMilliseconds() == 0);
  2463. case TimeStep.SCALE.SECOND:
  2464. return (this.current.getSeconds() == 0);
  2465. case TimeStep.SCALE.MINUTE:
  2466. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2467. // Note: this is no bug. Major label is equal for both minute and hour scale
  2468. case TimeStep.SCALE.HOUR:
  2469. return (this.current.getHours() == 0);
  2470. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2471. case TimeStep.SCALE.DAY:
  2472. return (this.current.getDate() == 1);
  2473. case TimeStep.SCALE.MONTH:
  2474. return (this.current.getMonth() == 0);
  2475. case TimeStep.SCALE.YEAR:
  2476. return false;
  2477. default:
  2478. return false;
  2479. }
  2480. };
  2481. /**
  2482. * Returns formatted text for the minor axislabel, depending on the current
  2483. * date and the scale. For example when scale is MINUTE, the current time is
  2484. * formatted as "hh:mm".
  2485. * @param {Date} [date] custom date. if not provided, current date is taken
  2486. */
  2487. TimeStep.prototype.getLabelMinor = function(date) {
  2488. if (date == undefined) {
  2489. date = this.current;
  2490. }
  2491. switch (this.scale) {
  2492. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2493. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2494. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2495. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2496. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2497. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2498. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2499. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2500. default: return '';
  2501. }
  2502. };
  2503. /**
  2504. * Returns formatted text for the major axis label, depending on the current
  2505. * date and the scale. For example when scale is MINUTE, the major scale is
  2506. * hours, and the hour will be formatted as "hh".
  2507. * @param {Date} [date] custom date. if not provided, current date is taken
  2508. */
  2509. TimeStep.prototype.getLabelMajor = function(date) {
  2510. if (date == undefined) {
  2511. date = this.current;
  2512. }
  2513. //noinspection FallthroughInSwitchStatementJS
  2514. switch (this.scale) {
  2515. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2516. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2517. case TimeStep.SCALE.MINUTE:
  2518. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2519. case TimeStep.SCALE.WEEKDAY:
  2520. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2521. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2522. case TimeStep.SCALE.YEAR: return '';
  2523. default: return '';
  2524. }
  2525. };
  2526. /**
  2527. * @constructor Stack
  2528. * Stacks items on top of each other.
  2529. * @param {ItemSet} parent
  2530. * @param {Object} [options]
  2531. */
  2532. function Stack (parent, options) {
  2533. this.parent = parent;
  2534. this.options = options || {};
  2535. this.defaultOptions = {
  2536. order: function (a, b) {
  2537. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2538. // Order: ranges over non-ranges, ranged ordered by width, and
  2539. // lastly ordered by start.
  2540. if (a instanceof ItemRange) {
  2541. if (b instanceof ItemRange) {
  2542. var aInt = (a.data.end - a.data.start);
  2543. var bInt = (b.data.end - b.data.start);
  2544. return (aInt - bInt) || (a.data.start - b.data.start);
  2545. }
  2546. else {
  2547. return -1;
  2548. }
  2549. }
  2550. else {
  2551. if (b instanceof ItemRange) {
  2552. return 1;
  2553. }
  2554. else {
  2555. return (a.data.start - b.data.start);
  2556. }
  2557. }
  2558. },
  2559. margin: {
  2560. item: 10
  2561. }
  2562. };
  2563. this.ordered = []; // ordered items
  2564. }
  2565. /**
  2566. * Set options for the stack
  2567. * @param {Object} options Available options:
  2568. * {ItemSet} parent
  2569. * {Number} margin
  2570. * {function} order Stacking order
  2571. */
  2572. Stack.prototype.setOptions = function setOptions (options) {
  2573. util.extend(this.options, options);
  2574. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2575. };
  2576. /**
  2577. * Stack the items such that they don't overlap. The items will have a minimal
  2578. * distance equal to options.margin.item.
  2579. */
  2580. Stack.prototype.update = function update() {
  2581. this._order();
  2582. this._stack();
  2583. };
  2584. /**
  2585. * Order the items. The items are ordered by width first, and by left position
  2586. * second.
  2587. * If a custom order function has been provided via the options, then this will
  2588. * be used.
  2589. * @private
  2590. */
  2591. Stack.prototype._order = function _order () {
  2592. var items = this.parent.items;
  2593. if (!items) {
  2594. throw new Error('Cannot stack items: parent does not contain items');
  2595. }
  2596. // TODO: store the sorted items, to have less work later on
  2597. var ordered = [];
  2598. var index = 0;
  2599. // items is a map (no array)
  2600. util.forEach(items, function (item) {
  2601. if (item.visible) {
  2602. ordered[index] = item;
  2603. index++;
  2604. }
  2605. });
  2606. //if a customer stack order function exists, use it.
  2607. var order = this.options.order || this.defaultOptions.order;
  2608. if (!(typeof order === 'function')) {
  2609. throw new Error('Option order must be a function');
  2610. }
  2611. ordered.sort(order);
  2612. this.ordered = ordered;
  2613. };
  2614. /**
  2615. * Adjust vertical positions of the events such that they don't overlap each
  2616. * other.
  2617. * @private
  2618. */
  2619. Stack.prototype._stack = function _stack () {
  2620. var i,
  2621. iMax,
  2622. ordered = this.ordered,
  2623. options = this.options,
  2624. orientation = options.orientation || this.defaultOptions.orientation,
  2625. axisOnTop = (orientation == 'top'),
  2626. margin;
  2627. if (options.margin && options.margin.item !== undefined) {
  2628. margin = options.margin.item;
  2629. }
  2630. else {
  2631. margin = this.defaultOptions.margin.item
  2632. }
  2633. // calculate new, non-overlapping positions
  2634. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2635. var item = ordered[i];
  2636. var collidingItem = null;
  2637. do {
  2638. // TODO: optimize checking for overlap. when there is a gap without items,
  2639. // you only need to check for items from the next item on, not from zero
  2640. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2641. if (collidingItem != null) {
  2642. // There is a collision. Reposition the event above the colliding element
  2643. if (axisOnTop) {
  2644. item.top = collidingItem.top + collidingItem.height + margin;
  2645. }
  2646. else {
  2647. item.top = collidingItem.top - item.height - margin;
  2648. }
  2649. }
  2650. } while (collidingItem);
  2651. }
  2652. };
  2653. /**
  2654. * Check if the destiny position of given item overlaps with any
  2655. * of the other items from index itemStart to itemEnd.
  2656. * @param {Array} items Array with items
  2657. * @param {int} itemIndex Number of the item to be checked for overlap
  2658. * @param {int} itemStart First item to be checked.
  2659. * @param {int} itemEnd Last item to be checked.
  2660. * @return {Object | null} colliding item, or undefined when no collisions
  2661. * @param {Number} margin A minimum required margin.
  2662. * If margin is provided, the two items will be
  2663. * marked colliding when they overlap or
  2664. * when the margin between the two is smaller than
  2665. * the requested margin.
  2666. */
  2667. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2668. itemStart, itemEnd, margin) {
  2669. var collision = this.collision;
  2670. // we loop from end to start, as we suppose that the chance of a
  2671. // collision is larger for items at the end, so check these first.
  2672. var a = items[itemIndex];
  2673. for (var i = itemEnd; i >= itemStart; i--) {
  2674. var b = items[i];
  2675. if (collision(a, b, margin)) {
  2676. if (i != itemIndex) {
  2677. return b;
  2678. }
  2679. }
  2680. }
  2681. return null;
  2682. };
  2683. /**
  2684. * Test if the two provided items collide
  2685. * The items must have parameters left, width, top, and height.
  2686. * @param {Component} a The first item
  2687. * @param {Component} b The second item
  2688. * @param {Number} margin A minimum required margin.
  2689. * If margin is provided, the two items will be
  2690. * marked colliding when they overlap or
  2691. * when the margin between the two is smaller than
  2692. * the requested margin.
  2693. * @return {boolean} true if a and b collide, else false
  2694. */
  2695. Stack.prototype.collision = function collision (a, b, margin) {
  2696. return ((a.left - margin) < (b.left + b.getWidth()) &&
  2697. (a.left + a.getWidth() + margin) > b.left &&
  2698. (a.top - margin) < (b.top + b.height) &&
  2699. (a.top + a.height + margin) > b.top);
  2700. };
  2701. /**
  2702. * @constructor Range
  2703. * A Range controls a numeric range with a start and end value.
  2704. * The Range adjusts the range based on mouse events or programmatic changes,
  2705. * and triggers events when the range is changing or has been changed.
  2706. * @param {Object} [options] See description at Range.setOptions
  2707. * @extends Controller
  2708. */
  2709. function Range(options) {
  2710. this.id = util.randomUUID();
  2711. this.start = null; // Number
  2712. this.end = null; // Number
  2713. this.options = options || {};
  2714. this.setOptions(options);
  2715. }
  2716. /**
  2717. * Set options for the range controller
  2718. * @param {Object} options Available options:
  2719. * {Number} min Minimum value for start
  2720. * {Number} max Maximum value for end
  2721. * {Number} zoomMin Set a minimum value for
  2722. * (end - start).
  2723. * {Number} zoomMax Set a maximum value for
  2724. * (end - start).
  2725. */
  2726. Range.prototype.setOptions = function (options) {
  2727. util.extend(this.options, options);
  2728. // re-apply range with new limitations
  2729. if (this.start !== null && this.end !== null) {
  2730. this.setRange(this.start, this.end);
  2731. }
  2732. };
  2733. /**
  2734. * Test whether direction has a valid value
  2735. * @param {String} direction 'horizontal' or 'vertical'
  2736. */
  2737. function validateDirection (direction) {
  2738. if (direction != 'horizontal' && direction != 'vertical') {
  2739. throw new TypeError('Unknown direction "' + direction + '". ' +
  2740. 'Choose "horizontal" or "vertical".');
  2741. }
  2742. }
  2743. /**
  2744. * Add listeners for mouse and touch events to the component
  2745. * @param {Component} component
  2746. * @param {String} event Available events: 'move', 'zoom'
  2747. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2748. */
  2749. Range.prototype.subscribe = function (component, event, direction) {
  2750. var me = this;
  2751. if (event == 'move') {
  2752. // drag start listener
  2753. component.on('dragstart', function (event) {
  2754. me._onDragStart(event, component);
  2755. });
  2756. // drag listener
  2757. component.on('drag', function (event) {
  2758. me._onDrag(event, component, direction);
  2759. });
  2760. // drag end listener
  2761. component.on('dragend', function (event) {
  2762. me._onDragEnd(event, component);
  2763. });
  2764. }
  2765. else if (event == 'zoom') {
  2766. // mouse wheel
  2767. function mousewheel (event) {
  2768. me._onMouseWheel(event, component, direction);
  2769. }
  2770. component.on('mousewheel', mousewheel);
  2771. component.on('DOMMouseScroll', mousewheel); // For FF
  2772. // pinch
  2773. component.on('touch', function (event) {
  2774. me._onTouch();
  2775. });
  2776. component.on('pinch', function (event) {
  2777. me._onPinch(event, component, direction);
  2778. });
  2779. }
  2780. else {
  2781. throw new TypeError('Unknown event "' + event + '". ' +
  2782. 'Choose "move" or "zoom".');
  2783. }
  2784. };
  2785. /**
  2786. * Event handler
  2787. * @param {String} event name of the event, for example 'click', 'mousemove'
  2788. * @param {function} callback callback handler, invoked with the raw HTML Event
  2789. * as parameter.
  2790. */
  2791. Range.prototype.on = function (event, callback) {
  2792. events.addListener(this, event, callback);
  2793. };
  2794. /**
  2795. * Trigger an event
  2796. * @param {String} event name of the event, available events: 'rangechange',
  2797. * 'rangechanged'
  2798. * @private
  2799. */
  2800. Range.prototype._trigger = function (event) {
  2801. events.trigger(this, event, {
  2802. start: this.start,
  2803. end: this.end
  2804. });
  2805. };
  2806. /**
  2807. * Set a new start and end range
  2808. * @param {Number} [start]
  2809. * @param {Number} [end]
  2810. */
  2811. Range.prototype.setRange = function(start, end) {
  2812. var changed = this._applyRange(start, end);
  2813. if (changed) {
  2814. this._trigger('rangechange');
  2815. this._trigger('rangechanged');
  2816. }
  2817. };
  2818. /**
  2819. * Set a new start and end range. This method is the same as setRange, but
  2820. * does not trigger a range change and range changed event, and it returns
  2821. * true when the range is changed
  2822. * @param {Number} [start]
  2823. * @param {Number} [end]
  2824. * @return {Boolean} changed
  2825. * @private
  2826. */
  2827. Range.prototype._applyRange = function(start, end) {
  2828. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  2829. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  2830. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2831. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2832. diff;
  2833. // check for valid number
  2834. if (isNaN(newStart) || newStart === null) {
  2835. throw new Error('Invalid start "' + start + '"');
  2836. }
  2837. if (isNaN(newEnd) || newEnd === null) {
  2838. throw new Error('Invalid end "' + end + '"');
  2839. }
  2840. // prevent start < end
  2841. if (newEnd < newStart) {
  2842. newEnd = newStart;
  2843. }
  2844. // prevent start < min
  2845. if (min !== null) {
  2846. if (newStart < min) {
  2847. diff = (min - newStart);
  2848. newStart += diff;
  2849. newEnd += diff;
  2850. // prevent end > max
  2851. if (max != null) {
  2852. if (newEnd > max) {
  2853. newEnd = max;
  2854. }
  2855. }
  2856. }
  2857. }
  2858. // prevent end > max
  2859. if (max !== null) {
  2860. if (newEnd > max) {
  2861. diff = (newEnd - max);
  2862. newStart -= diff;
  2863. newEnd -= diff;
  2864. // prevent start < min
  2865. if (min != null) {
  2866. if (newStart < min) {
  2867. newStart = min;
  2868. }
  2869. }
  2870. }
  2871. }
  2872. // prevent (end-start) < zoomMin
  2873. if (this.options.zoomMin !== null) {
  2874. var zoomMin = parseFloat(this.options.zoomMin);
  2875. if (zoomMin < 0) {
  2876. zoomMin = 0;
  2877. }
  2878. if ((newEnd - newStart) < zoomMin) {
  2879. if ((this.end - this.start) === zoomMin) {
  2880. // ignore this action, we are already zoomed to the minimum
  2881. newStart = this.start;
  2882. newEnd = this.end;
  2883. }
  2884. else {
  2885. // zoom to the minimum
  2886. diff = (zoomMin - (newEnd - newStart));
  2887. newStart -= diff / 2;
  2888. newEnd += diff / 2;
  2889. }
  2890. }
  2891. }
  2892. // prevent (end-start) > zoomMax
  2893. if (this.options.zoomMax !== null) {
  2894. var zoomMax = parseFloat(this.options.zoomMax);
  2895. if (zoomMax < 0) {
  2896. zoomMax = 0;
  2897. }
  2898. if ((newEnd - newStart) > zoomMax) {
  2899. if ((this.end - this.start) === zoomMax) {
  2900. // ignore this action, we are already zoomed to the maximum
  2901. newStart = this.start;
  2902. newEnd = this.end;
  2903. }
  2904. else {
  2905. // zoom to the maximum
  2906. diff = ((newEnd - newStart) - zoomMax);
  2907. newStart += diff / 2;
  2908. newEnd -= diff / 2;
  2909. }
  2910. }
  2911. }
  2912. var changed = (this.start != newStart || this.end != newEnd);
  2913. this.start = newStart;
  2914. this.end = newEnd;
  2915. return changed;
  2916. };
  2917. /**
  2918. * Retrieve the current range.
  2919. * @return {Object} An object with start and end properties
  2920. */
  2921. Range.prototype.getRange = function() {
  2922. return {
  2923. start: this.start,
  2924. end: this.end
  2925. };
  2926. };
  2927. /**
  2928. * Calculate the conversion offset and scale for current range, based on
  2929. * the provided width
  2930. * @param {Number} width
  2931. * @returns {{offset: number, scale: number}} conversion
  2932. */
  2933. Range.prototype.conversion = function (width) {
  2934. return Range.conversion(this.start, this.end, width);
  2935. };
  2936. /**
  2937. * Static method to calculate the conversion offset and scale for a range,
  2938. * based on the provided start, end, and width
  2939. * @param {Number} start
  2940. * @param {Number} end
  2941. * @param {Number} width
  2942. * @returns {{offset: number, scale: number}} conversion
  2943. */
  2944. Range.conversion = function (start, end, width) {
  2945. if (width != 0 && (end - start != 0)) {
  2946. return {
  2947. offset: start,
  2948. scale: width / (end - start)
  2949. }
  2950. }
  2951. else {
  2952. return {
  2953. offset: 0,
  2954. scale: 1
  2955. };
  2956. }
  2957. };
  2958. // global (private) object to store drag params
  2959. var touchParams = {};
  2960. /**
  2961. * Start dragging horizontally or vertically
  2962. * @param {Event} event
  2963. * @param {Object} component
  2964. * @private
  2965. */
  2966. Range.prototype._onDragStart = function(event, component) {
  2967. // refuse to drag when we where pinching to prevent the timeline make a jump
  2968. // when releasing the fingers in opposite order from the touch screen
  2969. if (touchParams.pinching) return;
  2970. touchParams.start = this.start;
  2971. touchParams.end = this.end;
  2972. var frame = component.frame;
  2973. if (frame) {
  2974. frame.style.cursor = 'move';
  2975. }
  2976. };
  2977. /**
  2978. * Perform dragging operating.
  2979. * @param {Event} event
  2980. * @param {Component} component
  2981. * @param {String} direction 'horizontal' or 'vertical'
  2982. * @private
  2983. */
  2984. Range.prototype._onDrag = function (event, component, direction) {
  2985. validateDirection(direction);
  2986. // refuse to drag when we where pinching to prevent the timeline make a jump
  2987. // when releasing the fingers in opposite order from the touch screen
  2988. if (touchParams.pinching) return;
  2989. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  2990. interval = (touchParams.end - touchParams.start),
  2991. width = (direction == 'horizontal') ? component.width : component.height,
  2992. diffRange = -delta / width * interval;
  2993. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  2994. // fire a rangechange event
  2995. this._trigger('rangechange');
  2996. };
  2997. /**
  2998. * Stop dragging operating.
  2999. * @param {event} event
  3000. * @param {Component} component
  3001. * @private
  3002. */
  3003. Range.prototype._onDragEnd = function (event, component) {
  3004. // refuse to drag when we where pinching to prevent the timeline make a jump
  3005. // when releasing the fingers in opposite order from the touch screen
  3006. if (touchParams.pinching) return;
  3007. if (component.frame) {
  3008. component.frame.style.cursor = 'auto';
  3009. }
  3010. // fire a rangechanged event
  3011. this._trigger('rangechanged');
  3012. };
  3013. /**
  3014. * Event handler for mouse wheel event, used to zoom
  3015. * Code from http://adomas.org/javascript-mouse-wheel/
  3016. * @param {Event} event
  3017. * @param {Component} component
  3018. * @param {String} direction 'horizontal' or 'vertical'
  3019. * @private
  3020. */
  3021. Range.prototype._onMouseWheel = function(event, component, direction) {
  3022. validateDirection(direction);
  3023. // retrieve delta
  3024. var delta = 0;
  3025. if (event.wheelDelta) { /* IE/Opera. */
  3026. delta = event.wheelDelta / 120;
  3027. } else if (event.detail) { /* Mozilla case. */
  3028. // In Mozilla, sign of delta is different than in IE.
  3029. // Also, delta is multiple of 3.
  3030. delta = -event.detail / 3;
  3031. }
  3032. // If delta is nonzero, handle it.
  3033. // Basically, delta is now positive if wheel was scrolled up,
  3034. // and negative, if wheel was scrolled down.
  3035. if (delta) {
  3036. // perform the zoom action. Delta is normally 1 or -1
  3037. // adjust a negative delta such that zooming in with delta 0.1
  3038. // equals zooming out with a delta -0.1
  3039. var scale;
  3040. if (delta < 0) {
  3041. scale = 1 - (delta / 5);
  3042. }
  3043. else {
  3044. scale = 1 / (1 + (delta / 5)) ;
  3045. }
  3046. // calculate center, the date to zoom around
  3047. var gesture = Hammer.event.collectEventData(this, 'scroll', event),
  3048. pointer = getPointer(gesture.touches[0], component.frame),
  3049. pointerDate = this._pointerToDate(component, direction, pointer);
  3050. this.zoom(scale, pointerDate);
  3051. }
  3052. // Prevent default actions caused by mouse wheel
  3053. // (else the page and timeline both zoom and scroll)
  3054. util.preventDefault(event);
  3055. };
  3056. /**
  3057. * On start of a touch gesture, initialize scale to 1
  3058. * @private
  3059. */
  3060. Range.prototype._onTouch = function () {
  3061. touchParams.start = this.start;
  3062. touchParams.end = this.end;
  3063. touchParams.pinching = false;
  3064. touchParams.center = null;
  3065. };
  3066. /**
  3067. * Handle pinch event
  3068. * @param {Event} event
  3069. * @param {Component} component
  3070. * @param {String} direction 'horizontal' or 'vertical'
  3071. * @private
  3072. */
  3073. Range.prototype._onPinch = function (event, component, direction) {
  3074. touchParams.pinching = true;
  3075. if (event.gesture.touches.length > 1) {
  3076. if (!touchParams.center) {
  3077. touchParams.center = getPointer(event.gesture.center, component.frame);
  3078. }
  3079. var scale = 1 / event.gesture.scale,
  3080. initDate = this._pointerToDate(component, direction, touchParams.center),
  3081. center = getPointer(event.gesture.center, component.frame),
  3082. date = this._pointerToDate(component, direction, center),
  3083. delta = date - initDate; // TODO: utilize delta
  3084. // calculate new start and end
  3085. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3086. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3087. // apply new range
  3088. this.setRange(newStart, newEnd);
  3089. }
  3090. };
  3091. /**
  3092. * Helper function to calculate the center date for zooming
  3093. * @param {Component} component
  3094. * @param {{x: Number, y: Number}} pointer
  3095. * @param {String} direction 'horizontal' or 'vertical'
  3096. * @return {number} date
  3097. * @private
  3098. */
  3099. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3100. var conversion;
  3101. if (direction == 'horizontal') {
  3102. var width = component.width;
  3103. conversion = this.conversion(width);
  3104. return pointer.x / conversion.scale + conversion.offset;
  3105. }
  3106. else {
  3107. var height = component.height;
  3108. conversion = this.conversion(height);
  3109. return pointer.y / conversion.scale + conversion.offset;
  3110. }
  3111. };
  3112. /**
  3113. * Get the pointer location relative to the location of the dom element
  3114. * @param {{pageX: Number, pageY: Number}} touch
  3115. * @param {Element} element HTML DOM element
  3116. * @return {{x: Number, y: Number}} pointer
  3117. * @private
  3118. */
  3119. function getPointer (touch, element) {
  3120. return {
  3121. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3122. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3123. };
  3124. }
  3125. /**
  3126. * Zoom the range the given scale in or out. Start and end date will
  3127. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3128. * date around which to zoom.
  3129. * For example, try scale = 0.9 or 1.1
  3130. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3131. * values below 1 will zoom in.
  3132. * @param {Number} [center] Value representing a date around which will
  3133. * be zoomed.
  3134. */
  3135. Range.prototype.zoom = function(scale, center) {
  3136. // if centerDate is not provided, take it half between start Date and end Date
  3137. if (center == null) {
  3138. center = (this.start + this.end) / 2;
  3139. }
  3140. // calculate new start and end
  3141. var newStart = center + (this.start - center) * scale;
  3142. var newEnd = center + (this.end - center) * scale;
  3143. this.setRange(newStart, newEnd);
  3144. };
  3145. /**
  3146. * Move the range with a given delta to the left or right. Start and end
  3147. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3148. * @param {Number} delta Moving amount. Positive value will move right,
  3149. * negative value will move left
  3150. */
  3151. Range.prototype.move = function(delta) {
  3152. // zoom start Date and end Date relative to the centerDate
  3153. var diff = (this.end - this.start);
  3154. // apply new values
  3155. var newStart = this.start + diff * delta;
  3156. var newEnd = this.end + diff * delta;
  3157. // TODO: reckon with min and max range
  3158. this.start = newStart;
  3159. this.end = newEnd;
  3160. };
  3161. /**
  3162. * Move the range to a new center point
  3163. * @param {Number} moveTo New center point of the range
  3164. */
  3165. Range.prototype.moveTo = function(moveTo) {
  3166. var center = (this.start + this.end) / 2;
  3167. var diff = center - moveTo;
  3168. // calculate new start and end
  3169. var newStart = this.start - diff;
  3170. var newEnd = this.end - diff;
  3171. this.setRange(newStart, newEnd);
  3172. };
  3173. /**
  3174. * @constructor Controller
  3175. *
  3176. * A Controller controls the reflows and repaints of all visual components
  3177. */
  3178. function Controller () {
  3179. this.id = util.randomUUID();
  3180. this.components = {};
  3181. this.repaintTimer = undefined;
  3182. this.reflowTimer = undefined;
  3183. }
  3184. /**
  3185. * Add a component to the controller
  3186. * @param {Component} component
  3187. */
  3188. Controller.prototype.add = function add(component) {
  3189. // validate the component
  3190. if (component.id == undefined) {
  3191. throw new Error('Component has no field id');
  3192. }
  3193. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3194. throw new TypeError('Component must be an instance of ' +
  3195. 'prototype Component or Controller');
  3196. }
  3197. // add the component
  3198. component.controller = this;
  3199. this.components[component.id] = component;
  3200. };
  3201. /**
  3202. * Remove a component from the controller
  3203. * @param {Component | String} component
  3204. */
  3205. Controller.prototype.remove = function remove(component) {
  3206. var id;
  3207. for (id in this.components) {
  3208. if (this.components.hasOwnProperty(id)) {
  3209. if (id == component || this.components[id] == component) {
  3210. break;
  3211. }
  3212. }
  3213. }
  3214. if (id) {
  3215. delete this.components[id];
  3216. }
  3217. };
  3218. /**
  3219. * Request a reflow. The controller will schedule a reflow
  3220. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3221. * is false.
  3222. */
  3223. Controller.prototype.requestReflow = function requestReflow(force) {
  3224. if (force) {
  3225. this.reflow();
  3226. }
  3227. else {
  3228. if (!this.reflowTimer) {
  3229. var me = this;
  3230. this.reflowTimer = setTimeout(function () {
  3231. me.reflowTimer = undefined;
  3232. me.reflow();
  3233. }, 0);
  3234. }
  3235. }
  3236. };
  3237. /**
  3238. * Request a repaint. The controller will schedule a repaint
  3239. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3240. * is false.
  3241. */
  3242. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3243. if (force) {
  3244. this.repaint();
  3245. }
  3246. else {
  3247. if (!this.repaintTimer) {
  3248. var me = this;
  3249. this.repaintTimer = setTimeout(function () {
  3250. me.repaintTimer = undefined;
  3251. me.repaint();
  3252. }, 0);
  3253. }
  3254. }
  3255. };
  3256. /**
  3257. * Repaint all components
  3258. */
  3259. Controller.prototype.repaint = function repaint() {
  3260. var changed = false;
  3261. // cancel any running repaint request
  3262. if (this.repaintTimer) {
  3263. clearTimeout(this.repaintTimer);
  3264. this.repaintTimer = undefined;
  3265. }
  3266. var done = {};
  3267. function repaint(component, id) {
  3268. if (!(id in done)) {
  3269. // first repaint the components on which this component is dependent
  3270. if (component.depends) {
  3271. component.depends.forEach(function (dep) {
  3272. repaint(dep, dep.id);
  3273. });
  3274. }
  3275. if (component.parent) {
  3276. repaint(component.parent, component.parent.id);
  3277. }
  3278. // repaint the component itself and mark as done
  3279. changed = component.repaint() || changed;
  3280. done[id] = true;
  3281. }
  3282. }
  3283. util.forEach(this.components, repaint);
  3284. // immediately reflow when needed
  3285. if (changed) {
  3286. this.reflow();
  3287. }
  3288. // TODO: limit the number of nested reflows/repaints, prevent loop
  3289. };
  3290. /**
  3291. * Reflow all components
  3292. */
  3293. Controller.prototype.reflow = function reflow() {
  3294. var resized = false;
  3295. // cancel any running repaint request
  3296. if (this.reflowTimer) {
  3297. clearTimeout(this.reflowTimer);
  3298. this.reflowTimer = undefined;
  3299. }
  3300. var done = {};
  3301. function reflow(component, id) {
  3302. if (!(id in done)) {
  3303. // first reflow the components on which this component is dependent
  3304. if (component.depends) {
  3305. component.depends.forEach(function (dep) {
  3306. reflow(dep, dep.id);
  3307. });
  3308. }
  3309. if (component.parent) {
  3310. reflow(component.parent, component.parent.id);
  3311. }
  3312. // reflow the component itself and mark as done
  3313. resized = component.reflow() || resized;
  3314. done[id] = true;
  3315. }
  3316. }
  3317. util.forEach(this.components, reflow);
  3318. // immediately repaint when needed
  3319. if (resized) {
  3320. this.repaint();
  3321. }
  3322. // TODO: limit the number of nested reflows/repaints, prevent loop
  3323. };
  3324. /**
  3325. * Prototype for visual components
  3326. */
  3327. function Component () {
  3328. this.id = null;
  3329. this.parent = null;
  3330. this.depends = null;
  3331. this.controller = null;
  3332. this.options = null;
  3333. this.frame = null; // main DOM element
  3334. this.top = 0;
  3335. this.left = 0;
  3336. this.width = 0;
  3337. this.height = 0;
  3338. }
  3339. /**
  3340. * Set parameters for the frame. Parameters will be merged in current parameter
  3341. * set.
  3342. * @param {Object} options Available parameters:
  3343. * {String | function} [className]
  3344. * {EventBus} [eventBus]
  3345. * {String | Number | function} [left]
  3346. * {String | Number | function} [top]
  3347. * {String | Number | function} [width]
  3348. * {String | Number | function} [height]
  3349. */
  3350. Component.prototype.setOptions = function setOptions(options) {
  3351. if (options) {
  3352. util.extend(this.options, options);
  3353. if (this.controller) {
  3354. this.requestRepaint();
  3355. this.requestReflow();
  3356. }
  3357. }
  3358. };
  3359. /**
  3360. * Get an option value by name
  3361. * The function will first check this.options object, and else will check
  3362. * this.defaultOptions.
  3363. * @param {String} name
  3364. * @return {*} value
  3365. */
  3366. Component.prototype.getOption = function getOption(name) {
  3367. var value;
  3368. if (this.options) {
  3369. value = this.options[name];
  3370. }
  3371. if (value === undefined && this.defaultOptions) {
  3372. value = this.defaultOptions[name];
  3373. }
  3374. return value;
  3375. };
  3376. /**
  3377. * Get the container element of the component, which can be used by a child to
  3378. * add its own widgets. Not all components do have a container for childs, in
  3379. * that case null is returned.
  3380. * @returns {HTMLElement | null} container
  3381. */
  3382. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3383. Component.prototype.getContainer = function getContainer() {
  3384. // should be implemented by the component
  3385. return null;
  3386. };
  3387. /**
  3388. * Get the frame element of the component, the outer HTML DOM element.
  3389. * @returns {HTMLElement | null} frame
  3390. */
  3391. Component.prototype.getFrame = function getFrame() {
  3392. return this.frame;
  3393. };
  3394. /**
  3395. * Repaint the component
  3396. * @return {Boolean} changed
  3397. */
  3398. Component.prototype.repaint = function repaint() {
  3399. // should be implemented by the component
  3400. return false;
  3401. };
  3402. /**
  3403. * Reflow the component
  3404. * @return {Boolean} resized
  3405. */
  3406. Component.prototype.reflow = function reflow() {
  3407. // should be implemented by the component
  3408. return false;
  3409. };
  3410. /**
  3411. * Hide the component from the DOM
  3412. * @return {Boolean} changed
  3413. */
  3414. Component.prototype.hide = function hide() {
  3415. if (this.frame && this.frame.parentNode) {
  3416. this.frame.parentNode.removeChild(this.frame);
  3417. return true;
  3418. }
  3419. else {
  3420. return false;
  3421. }
  3422. };
  3423. /**
  3424. * Show the component in the DOM (when not already visible).
  3425. * A repaint will be executed when the component is not visible
  3426. * @return {Boolean} changed
  3427. */
  3428. Component.prototype.show = function show() {
  3429. if (!this.frame || !this.frame.parentNode) {
  3430. return this.repaint();
  3431. }
  3432. else {
  3433. return false;
  3434. }
  3435. };
  3436. /**
  3437. * Request a repaint. The controller will schedule a repaint
  3438. */
  3439. Component.prototype.requestRepaint = function requestRepaint() {
  3440. if (this.controller) {
  3441. this.controller.requestRepaint();
  3442. }
  3443. else {
  3444. throw new Error('Cannot request a repaint: no controller configured');
  3445. // TODO: just do a repaint when no parent is configured?
  3446. }
  3447. };
  3448. /**
  3449. * Request a reflow. The controller will schedule a reflow
  3450. */
  3451. Component.prototype.requestReflow = function requestReflow() {
  3452. if (this.controller) {
  3453. this.controller.requestReflow();
  3454. }
  3455. else {
  3456. throw new Error('Cannot request a reflow: no controller configured');
  3457. // TODO: just do a reflow when no parent is configured?
  3458. }
  3459. };
  3460. /**
  3461. * A panel can contain components
  3462. * @param {Component} [parent]
  3463. * @param {Component[]} [depends] Components on which this components depends
  3464. * (except for the parent)
  3465. * @param {Object} [options] Available parameters:
  3466. * {String | Number | function} [left]
  3467. * {String | Number | function} [top]
  3468. * {String | Number | function} [width]
  3469. * {String | Number | function} [height]
  3470. * {String | function} [className]
  3471. * @constructor Panel
  3472. * @extends Component
  3473. */
  3474. function Panel(parent, depends, options) {
  3475. this.id = util.randomUUID();
  3476. this.parent = parent;
  3477. this.depends = depends;
  3478. this.options = options || {};
  3479. }
  3480. Panel.prototype = new Component();
  3481. /**
  3482. * Set options. Will extend the current options.
  3483. * @param {Object} [options] Available parameters:
  3484. * {String | function} [className]
  3485. * {String | Number | function} [left]
  3486. * {String | Number | function} [top]
  3487. * {String | Number | function} [width]
  3488. * {String | Number | function} [height]
  3489. */
  3490. Panel.prototype.setOptions = Component.prototype.setOptions;
  3491. /**
  3492. * Get the container element of the panel, which can be used by a child to
  3493. * add its own widgets.
  3494. * @returns {HTMLElement} container
  3495. */
  3496. Panel.prototype.getContainer = function () {
  3497. return this.frame;
  3498. };
  3499. /**
  3500. * Repaint the component
  3501. * @return {Boolean} changed
  3502. */
  3503. Panel.prototype.repaint = function () {
  3504. var changed = 0,
  3505. update = util.updateProperty,
  3506. asSize = util.option.asSize,
  3507. options = this.options,
  3508. frame = this.frame;
  3509. if (!frame) {
  3510. frame = document.createElement('div');
  3511. frame.className = 'panel';
  3512. var className = options.className;
  3513. if (className) {
  3514. if (typeof className == 'function') {
  3515. util.addClassName(frame, String(className()));
  3516. }
  3517. else {
  3518. util.addClassName(frame, String(className));
  3519. }
  3520. }
  3521. this.frame = frame;
  3522. changed += 1;
  3523. }
  3524. if (!frame.parentNode) {
  3525. if (!this.parent) {
  3526. throw new Error('Cannot repaint panel: no parent attached');
  3527. }
  3528. var parentContainer = this.parent.getContainer();
  3529. if (!parentContainer) {
  3530. throw new Error('Cannot repaint panel: parent has no container element');
  3531. }
  3532. parentContainer.appendChild(frame);
  3533. changed += 1;
  3534. }
  3535. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3536. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3537. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3538. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3539. return (changed > 0);
  3540. };
  3541. /**
  3542. * Reflow the component
  3543. * @return {Boolean} resized
  3544. */
  3545. Panel.prototype.reflow = function () {
  3546. var changed = 0,
  3547. update = util.updateProperty,
  3548. frame = this.frame;
  3549. if (frame) {
  3550. changed += update(this, 'top', frame.offsetTop);
  3551. changed += update(this, 'left', frame.offsetLeft);
  3552. changed += update(this, 'width', frame.offsetWidth);
  3553. changed += update(this, 'height', frame.offsetHeight);
  3554. }
  3555. else {
  3556. changed += 1;
  3557. }
  3558. return (changed > 0);
  3559. };
  3560. /**
  3561. * A root panel can hold components. The root panel must be initialized with
  3562. * a DOM element as container.
  3563. * @param {HTMLElement} container
  3564. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3565. * @constructor RootPanel
  3566. * @extends Panel
  3567. */
  3568. function RootPanel(container, options) {
  3569. this.id = util.randomUUID();
  3570. this.container = container;
  3571. this.options = options || {};
  3572. this.defaultOptions = {
  3573. autoResize: true
  3574. };
  3575. this.listeners = {}; // event listeners
  3576. }
  3577. RootPanel.prototype = new Panel();
  3578. /**
  3579. * Set options. Will extend the current options.
  3580. * @param {Object} [options] Available parameters:
  3581. * {String | function} [className]
  3582. * {String | Number | function} [left]
  3583. * {String | Number | function} [top]
  3584. * {String | Number | function} [width]
  3585. * {String | Number | function} [height]
  3586. * {Boolean | function} [autoResize]
  3587. */
  3588. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3589. /**
  3590. * Repaint the component
  3591. * @return {Boolean} changed
  3592. */
  3593. RootPanel.prototype.repaint = function () {
  3594. var changed = 0,
  3595. update = util.updateProperty,
  3596. asSize = util.option.asSize,
  3597. options = this.options,
  3598. frame = this.frame;
  3599. if (!frame) {
  3600. frame = document.createElement('div');
  3601. this.frame = frame;
  3602. changed += 1;
  3603. }
  3604. if (!frame.parentNode) {
  3605. if (!this.container) {
  3606. throw new Error('Cannot repaint root panel: no container attached');
  3607. }
  3608. this.container.appendChild(frame);
  3609. changed += 1;
  3610. }
  3611. frame.className = 'vis timeline rootpanel ' + options.orientation;
  3612. var className = options.className;
  3613. if (className) {
  3614. util.addClassName(frame, util.option.asString(className));
  3615. }
  3616. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3617. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3618. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3619. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3620. this._updateEventEmitters();
  3621. this._updateWatch();
  3622. return (changed > 0);
  3623. };
  3624. /**
  3625. * Reflow the component
  3626. * @return {Boolean} resized
  3627. */
  3628. RootPanel.prototype.reflow = function () {
  3629. var changed = 0,
  3630. update = util.updateProperty,
  3631. frame = this.frame;
  3632. if (frame) {
  3633. changed += update(this, 'top', frame.offsetTop);
  3634. changed += update(this, 'left', frame.offsetLeft);
  3635. changed += update(this, 'width', frame.offsetWidth);
  3636. changed += update(this, 'height', frame.offsetHeight);
  3637. }
  3638. else {
  3639. changed += 1;
  3640. }
  3641. return (changed > 0);
  3642. };
  3643. /**
  3644. * Update watching for resize, depending on the current option
  3645. * @private
  3646. */
  3647. RootPanel.prototype._updateWatch = function () {
  3648. var autoResize = this.getOption('autoResize');
  3649. if (autoResize) {
  3650. this._watch();
  3651. }
  3652. else {
  3653. this._unwatch();
  3654. }
  3655. };
  3656. /**
  3657. * Watch for changes in the size of the frame. On resize, the Panel will
  3658. * automatically redraw itself.
  3659. * @private
  3660. */
  3661. RootPanel.prototype._watch = function () {
  3662. var me = this;
  3663. this._unwatch();
  3664. var checkSize = function () {
  3665. var autoResize = me.getOption('autoResize');
  3666. if (!autoResize) {
  3667. // stop watching when the option autoResize is changed to false
  3668. me._unwatch();
  3669. return;
  3670. }
  3671. if (me.frame) {
  3672. // check whether the frame is resized
  3673. if ((me.frame.clientWidth != me.width) ||
  3674. (me.frame.clientHeight != me.height)) {
  3675. me.requestReflow();
  3676. }
  3677. }
  3678. };
  3679. // TODO: automatically cleanup the event listener when the frame is deleted
  3680. util.addEventListener(window, 'resize', checkSize);
  3681. this.watchTimer = setInterval(checkSize, 1000);
  3682. };
  3683. /**
  3684. * Stop watching for a resize of the frame.
  3685. * @private
  3686. */
  3687. RootPanel.prototype._unwatch = function () {
  3688. if (this.watchTimer) {
  3689. clearInterval(this.watchTimer);
  3690. this.watchTimer = undefined;
  3691. }
  3692. // TODO: remove event listener on window.resize
  3693. };
  3694. /**
  3695. * Event handler
  3696. * @param {String} event name of the event, for example 'click', 'mousemove'
  3697. * @param {function} callback callback handler, invoked with the raw HTML Event
  3698. * as parameter.
  3699. */
  3700. RootPanel.prototype.on = function (event, callback) {
  3701. // register the listener at this component
  3702. var arr = this.listeners[event];
  3703. if (!arr) {
  3704. arr = [];
  3705. this.listeners[event] = arr;
  3706. }
  3707. arr.push(callback);
  3708. this._updateEventEmitters();
  3709. };
  3710. /**
  3711. * Update the event listeners for all event emitters
  3712. * @private
  3713. */
  3714. RootPanel.prototype._updateEventEmitters = function () {
  3715. if (this.listeners) {
  3716. var me = this;
  3717. util.forEach(this.listeners, function (listeners, event) {
  3718. if (!me.emitters) {
  3719. me.emitters = {};
  3720. }
  3721. if (!(event in me.emitters)) {
  3722. // create event
  3723. var frame = me.frame;
  3724. if (frame) {
  3725. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3726. var callback = function(event) {
  3727. listeners.forEach(function (listener) {
  3728. // TODO: filter on event target!
  3729. listener(event);
  3730. });
  3731. };
  3732. me.emitters[event] = callback;
  3733. if (!me.hammer) {
  3734. me.hammer = Hammer(frame, {
  3735. prevent_default: true
  3736. });
  3737. }
  3738. me.hammer.on(event, callback);
  3739. }
  3740. }
  3741. });
  3742. // TODO: be able to delete event listeners
  3743. // TODO: be able to move event listeners to a parent when available
  3744. }
  3745. };
  3746. /**
  3747. * A horizontal time axis
  3748. * @param {Component} parent
  3749. * @param {Component[]} [depends] Components on which this components depends
  3750. * (except for the parent)
  3751. * @param {Object} [options] See TimeAxis.setOptions for the available
  3752. * options.
  3753. * @constructor TimeAxis
  3754. * @extends Component
  3755. */
  3756. function TimeAxis (parent, depends, options) {
  3757. this.id = util.randomUUID();
  3758. this.parent = parent;
  3759. this.depends = depends;
  3760. this.dom = {
  3761. majorLines: [],
  3762. majorTexts: [],
  3763. minorLines: [],
  3764. minorTexts: [],
  3765. redundant: {
  3766. majorLines: [],
  3767. majorTexts: [],
  3768. minorLines: [],
  3769. minorTexts: []
  3770. }
  3771. };
  3772. this.props = {
  3773. range: {
  3774. start: 0,
  3775. end: 0,
  3776. minimumStep: 0
  3777. },
  3778. lineTop: 0
  3779. };
  3780. this.options = options || {};
  3781. this.defaultOptions = {
  3782. orientation: 'bottom', // supported: 'top', 'bottom'
  3783. // TODO: implement timeaxis orientations 'left' and 'right'
  3784. showMinorLabels: true,
  3785. showMajorLabels: true
  3786. };
  3787. this.conversion = null;
  3788. this.range = null;
  3789. }
  3790. TimeAxis.prototype = new Component();
  3791. // TODO: comment options
  3792. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3793. /**
  3794. * Set a range (start and end)
  3795. * @param {Range | Object} range A Range or an object containing start and end.
  3796. */
  3797. TimeAxis.prototype.setRange = function (range) {
  3798. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3799. throw new TypeError('Range must be an instance of Range, ' +
  3800. 'or an object containing start and end.');
  3801. }
  3802. this.range = range;
  3803. };
  3804. /**
  3805. * Convert a position on screen (pixels) to a datetime
  3806. * @param {int} x Position on the screen in pixels
  3807. * @return {Date} time The datetime the corresponds with given position x
  3808. */
  3809. TimeAxis.prototype.toTime = function(x) {
  3810. var conversion = this.conversion;
  3811. return new Date(x / conversion.scale + conversion.offset);
  3812. };
  3813. /**
  3814. * Convert a datetime (Date object) into a position on the screen
  3815. * @param {Date} time A date
  3816. * @return {int} x The position on the screen in pixels which corresponds
  3817. * with the given date.
  3818. * @private
  3819. */
  3820. TimeAxis.prototype.toScreen = function(time) {
  3821. var conversion = this.conversion;
  3822. return (time.valueOf() - conversion.offset) * conversion.scale;
  3823. };
  3824. /**
  3825. * Repaint the component
  3826. * @return {Boolean} changed
  3827. */
  3828. TimeAxis.prototype.repaint = function () {
  3829. var changed = 0,
  3830. update = util.updateProperty,
  3831. asSize = util.option.asSize,
  3832. options = this.options,
  3833. orientation = this.getOption('orientation'),
  3834. props = this.props,
  3835. step = this.step;
  3836. var frame = this.frame;
  3837. if (!frame) {
  3838. frame = document.createElement('div');
  3839. this.frame = frame;
  3840. changed += 1;
  3841. }
  3842. frame.className = 'axis';
  3843. // TODO: custom className?
  3844. if (!frame.parentNode) {
  3845. if (!this.parent) {
  3846. throw new Error('Cannot repaint time axis: no parent attached');
  3847. }
  3848. var parentContainer = this.parent.getContainer();
  3849. if (!parentContainer) {
  3850. throw new Error('Cannot repaint time axis: parent has no container element');
  3851. }
  3852. parentContainer.appendChild(frame);
  3853. changed += 1;
  3854. }
  3855. var parent = frame.parentNode;
  3856. if (parent) {
  3857. var beforeChild = frame.nextSibling;
  3858. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3859. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3860. (this.props.parentHeight - this.height) + 'px' :
  3861. '0px';
  3862. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3863. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3864. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3865. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3866. // get characters width and height
  3867. this._repaintMeasureChars();
  3868. if (this.step) {
  3869. this._repaintStart();
  3870. step.first();
  3871. var xFirstMajorLabel = undefined;
  3872. var max = 0;
  3873. while (step.hasNext() && max < 1000) {
  3874. max++;
  3875. var cur = step.getCurrent(),
  3876. x = this.toScreen(cur),
  3877. isMajor = step.isMajor();
  3878. // TODO: lines must have a width, such that we can create css backgrounds
  3879. if (this.getOption('showMinorLabels')) {
  3880. this._repaintMinorText(x, step.getLabelMinor());
  3881. }
  3882. if (isMajor && this.getOption('showMajorLabels')) {
  3883. if (x > 0) {
  3884. if (xFirstMajorLabel == undefined) {
  3885. xFirstMajorLabel = x;
  3886. }
  3887. this._repaintMajorText(x, step.getLabelMajor());
  3888. }
  3889. this._repaintMajorLine(x);
  3890. }
  3891. else {
  3892. this._repaintMinorLine(x);
  3893. }
  3894. step.next();
  3895. }
  3896. // create a major label on the left when needed
  3897. if (this.getOption('showMajorLabels')) {
  3898. var leftTime = this.toTime(0),
  3899. leftText = step.getLabelMajor(leftTime),
  3900. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3901. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3902. this._repaintMajorText(0, leftText);
  3903. }
  3904. }
  3905. this._repaintEnd();
  3906. }
  3907. this._repaintLine();
  3908. // put frame online again
  3909. if (beforeChild) {
  3910. parent.insertBefore(frame, beforeChild);
  3911. }
  3912. else {
  3913. parent.appendChild(frame)
  3914. }
  3915. }
  3916. return (changed > 0);
  3917. };
  3918. /**
  3919. * Start a repaint. Move all DOM elements to a redundant list, where they
  3920. * can be picked for re-use, or can be cleaned up in the end
  3921. * @private
  3922. */
  3923. TimeAxis.prototype._repaintStart = function () {
  3924. var dom = this.dom,
  3925. redundant = dom.redundant;
  3926. redundant.majorLines = dom.majorLines;
  3927. redundant.majorTexts = dom.majorTexts;
  3928. redundant.minorLines = dom.minorLines;
  3929. redundant.minorTexts = dom.minorTexts;
  3930. dom.majorLines = [];
  3931. dom.majorTexts = [];
  3932. dom.minorLines = [];
  3933. dom.minorTexts = [];
  3934. };
  3935. /**
  3936. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3937. * @private
  3938. */
  3939. TimeAxis.prototype._repaintEnd = function () {
  3940. util.forEach(this.dom.redundant, function (arr) {
  3941. while (arr.length) {
  3942. var elem = arr.pop();
  3943. if (elem && elem.parentNode) {
  3944. elem.parentNode.removeChild(elem);
  3945. }
  3946. }
  3947. });
  3948. };
  3949. /**
  3950. * Create a minor label for the axis at position x
  3951. * @param {Number} x
  3952. * @param {String} text
  3953. * @private
  3954. */
  3955. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3956. // reuse redundant label
  3957. var label = this.dom.redundant.minorTexts.shift();
  3958. if (!label) {
  3959. // create new label
  3960. var content = document.createTextNode('');
  3961. label = document.createElement('div');
  3962. label.appendChild(content);
  3963. label.className = 'text minor';
  3964. this.frame.appendChild(label);
  3965. }
  3966. this.dom.minorTexts.push(label);
  3967. label.childNodes[0].nodeValue = text;
  3968. label.style.left = x + 'px';
  3969. label.style.top = this.props.minorLabelTop + 'px';
  3970. //label.title = title; // TODO: this is a heavy operation
  3971. };
  3972. /**
  3973. * Create a Major label for the axis at position x
  3974. * @param {Number} x
  3975. * @param {String} text
  3976. * @private
  3977. */
  3978. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3979. // reuse redundant label
  3980. var label = this.dom.redundant.majorTexts.shift();
  3981. if (!label) {
  3982. // create label
  3983. var content = document.createTextNode(text);
  3984. label = document.createElement('div');
  3985. label.className = 'text major';
  3986. label.appendChild(content);
  3987. this.frame.appendChild(label);
  3988. }
  3989. this.dom.majorTexts.push(label);
  3990. label.childNodes[0].nodeValue = text;
  3991. label.style.top = this.props.majorLabelTop + 'px';
  3992. label.style.left = x + 'px';
  3993. //label.title = title; // TODO: this is a heavy operation
  3994. };
  3995. /**
  3996. * Create a minor line for the axis at position x
  3997. * @param {Number} x
  3998. * @private
  3999. */
  4000. TimeAxis.prototype._repaintMinorLine = function (x) {
  4001. // reuse redundant line
  4002. var line = this.dom.redundant.minorLines.shift();
  4003. if (!line) {
  4004. // create vertical line
  4005. line = document.createElement('div');
  4006. line.className = 'grid vertical minor';
  4007. this.frame.appendChild(line);
  4008. }
  4009. this.dom.minorLines.push(line);
  4010. var props = this.props;
  4011. line.style.top = props.minorLineTop + 'px';
  4012. line.style.height = props.minorLineHeight + 'px';
  4013. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4014. };
  4015. /**
  4016. * Create a Major line for the axis at position x
  4017. * @param {Number} x
  4018. * @private
  4019. */
  4020. TimeAxis.prototype._repaintMajorLine = function (x) {
  4021. // reuse redundant line
  4022. var line = this.dom.redundant.majorLines.shift();
  4023. if (!line) {
  4024. // create vertical line
  4025. line = document.createElement('DIV');
  4026. line.className = 'grid vertical major';
  4027. this.frame.appendChild(line);
  4028. }
  4029. this.dom.majorLines.push(line);
  4030. var props = this.props;
  4031. line.style.top = props.majorLineTop + 'px';
  4032. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4033. line.style.height = props.majorLineHeight + 'px';
  4034. };
  4035. /**
  4036. * Repaint the horizontal line for the axis
  4037. * @private
  4038. */
  4039. TimeAxis.prototype._repaintLine = function() {
  4040. var line = this.dom.line,
  4041. frame = this.frame,
  4042. options = this.options;
  4043. // line before all axis elements
  4044. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4045. if (line) {
  4046. // put this line at the end of all childs
  4047. frame.removeChild(line);
  4048. frame.appendChild(line);
  4049. }
  4050. else {
  4051. // create the axis line
  4052. line = document.createElement('div');
  4053. line.className = 'grid horizontal major';
  4054. frame.appendChild(line);
  4055. this.dom.line = line;
  4056. }
  4057. line.style.top = this.props.lineTop + 'px';
  4058. }
  4059. else {
  4060. if (line && line.parentElement) {
  4061. frame.removeChild(line.line);
  4062. delete this.dom.line;
  4063. }
  4064. }
  4065. };
  4066. /**
  4067. * Create characters used to determine the size of text on the axis
  4068. * @private
  4069. */
  4070. TimeAxis.prototype._repaintMeasureChars = function () {
  4071. // calculate the width and height of a single character
  4072. // this is used to calculate the step size, and also the positioning of the
  4073. // axis
  4074. var dom = this.dom,
  4075. text;
  4076. if (!dom.measureCharMinor) {
  4077. text = document.createTextNode('0');
  4078. var measureCharMinor = document.createElement('DIV');
  4079. measureCharMinor.className = 'text minor measure';
  4080. measureCharMinor.appendChild(text);
  4081. this.frame.appendChild(measureCharMinor);
  4082. dom.measureCharMinor = measureCharMinor;
  4083. }
  4084. if (!dom.measureCharMajor) {
  4085. text = document.createTextNode('0');
  4086. var measureCharMajor = document.createElement('DIV');
  4087. measureCharMajor.className = 'text major measure';
  4088. measureCharMajor.appendChild(text);
  4089. this.frame.appendChild(measureCharMajor);
  4090. dom.measureCharMajor = measureCharMajor;
  4091. }
  4092. };
  4093. /**
  4094. * Reflow the component
  4095. * @return {Boolean} resized
  4096. */
  4097. TimeAxis.prototype.reflow = function () {
  4098. var changed = 0,
  4099. update = util.updateProperty,
  4100. frame = this.frame,
  4101. range = this.range;
  4102. if (!range) {
  4103. throw new Error('Cannot repaint time axis: no range configured');
  4104. }
  4105. if (frame) {
  4106. changed += update(this, 'top', frame.offsetTop);
  4107. changed += update(this, 'left', frame.offsetLeft);
  4108. // calculate size of a character
  4109. var props = this.props,
  4110. showMinorLabels = this.getOption('showMinorLabels'),
  4111. showMajorLabels = this.getOption('showMajorLabels'),
  4112. measureCharMinor = this.dom.measureCharMinor,
  4113. measureCharMajor = this.dom.measureCharMajor;
  4114. if (measureCharMinor) {
  4115. props.minorCharHeight = measureCharMinor.clientHeight;
  4116. props.minorCharWidth = measureCharMinor.clientWidth;
  4117. }
  4118. if (measureCharMajor) {
  4119. props.majorCharHeight = measureCharMajor.clientHeight;
  4120. props.majorCharWidth = measureCharMajor.clientWidth;
  4121. }
  4122. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4123. if (parentHeight != props.parentHeight) {
  4124. props.parentHeight = parentHeight;
  4125. changed += 1;
  4126. }
  4127. switch (this.getOption('orientation')) {
  4128. case 'bottom':
  4129. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4130. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4131. props.minorLabelTop = 0;
  4132. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4133. props.minorLineTop = -this.top;
  4134. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4135. props.minorLineWidth = 1; // TODO: really calculate width
  4136. props.majorLineTop = -this.top;
  4137. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4138. props.majorLineWidth = 1; // TODO: really calculate width
  4139. props.lineTop = 0;
  4140. break;
  4141. case 'top':
  4142. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4143. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4144. props.majorLabelTop = 0;
  4145. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4146. props.minorLineTop = props.minorLabelTop;
  4147. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4148. props.minorLineWidth = 1; // TODO: really calculate width
  4149. props.majorLineTop = 0;
  4150. props.majorLineHeight = Math.max(parentHeight - this.top);
  4151. props.majorLineWidth = 1; // TODO: really calculate width
  4152. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4153. break;
  4154. default:
  4155. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4156. }
  4157. var height = props.minorLabelHeight + props.majorLabelHeight;
  4158. changed += update(this, 'width', frame.offsetWidth);
  4159. changed += update(this, 'height', height);
  4160. // calculate range and step
  4161. this._updateConversion();
  4162. var start = util.convert(range.start, 'Number'),
  4163. end = util.convert(range.end, 'Number'),
  4164. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4165. -this.toTime(0).valueOf();
  4166. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4167. changed += update(props.range, 'start', start);
  4168. changed += update(props.range, 'end', end);
  4169. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4170. }
  4171. return (changed > 0);
  4172. };
  4173. /**
  4174. * Calculate the scale and offset to convert a position on screen to the
  4175. * corresponding date and vice versa.
  4176. * After the method _updateConversion is executed once, the methods toTime
  4177. * and toScreen can be used.
  4178. * @private
  4179. */
  4180. TimeAxis.prototype._updateConversion = function() {
  4181. var range = this.range;
  4182. if (!range) {
  4183. throw new Error('No range configured');
  4184. }
  4185. if (range.conversion) {
  4186. this.conversion = range.conversion(this.width);
  4187. }
  4188. else {
  4189. this.conversion = Range.conversion(range.start, range.end, this.width);
  4190. }
  4191. };
  4192. /**
  4193. * A current time bar
  4194. * @param {Component} parent
  4195. * @param {Component[]} [depends] Components on which this components depends
  4196. * (except for the parent)
  4197. * @param {Object} [options] Available parameters:
  4198. * {Boolean} [showCurrentTime]
  4199. * @constructor CurrentTime
  4200. * @extends Component
  4201. */
  4202. function CurrentTime (parent, depends, options) {
  4203. this.id = util.randomUUID();
  4204. this.parent = parent;
  4205. this.depends = depends;
  4206. this.options = options || {};
  4207. this.defaultOptions = {
  4208. showCurrentTime: false
  4209. };
  4210. }
  4211. CurrentTime.prototype = new Component();
  4212. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4213. /**
  4214. * Get the container element of the bar, which can be used by a child to
  4215. * add its own widgets.
  4216. * @returns {HTMLElement} container
  4217. */
  4218. CurrentTime.prototype.getContainer = function () {
  4219. return this.frame;
  4220. };
  4221. /**
  4222. * Repaint the component
  4223. * @return {Boolean} changed
  4224. */
  4225. CurrentTime.prototype.repaint = function () {
  4226. var bar = this.frame,
  4227. parent = this.parent,
  4228. parentContainer = parent.parent.getContainer();
  4229. if (!parent) {
  4230. throw new Error('Cannot repaint bar: no parent attached');
  4231. }
  4232. if (!parentContainer) {
  4233. throw new Error('Cannot repaint bar: parent has no container element');
  4234. }
  4235. if (!this.getOption('showCurrentTime')) {
  4236. if (bar) {
  4237. parentContainer.removeChild(bar);
  4238. delete this.frame;
  4239. }
  4240. return;
  4241. }
  4242. if (!bar) {
  4243. bar = document.createElement('div');
  4244. bar.className = 'currenttime';
  4245. bar.style.position = 'absolute';
  4246. bar.style.top = '0px';
  4247. bar.style.height = '100%';
  4248. parentContainer.appendChild(bar);
  4249. this.frame = bar;
  4250. }
  4251. if (!parent.conversion) {
  4252. parent._updateConversion();
  4253. }
  4254. var now = new Date();
  4255. var x = parent.toScreen(now);
  4256. bar.style.left = x + 'px';
  4257. bar.title = 'Current time: ' + now;
  4258. // start a timer to adjust for the new time
  4259. if (this.currentTimeTimer !== undefined) {
  4260. clearTimeout(this.currentTimeTimer);
  4261. delete this.currentTimeTimer;
  4262. }
  4263. var timeline = this;
  4264. var interval = 1 / parent.conversion.scale / 2;
  4265. if (interval < 30) {
  4266. interval = 30;
  4267. }
  4268. this.currentTimeTimer = setTimeout(function() {
  4269. timeline.repaint();
  4270. }, interval);
  4271. return false;
  4272. };
  4273. /**
  4274. * A custom time bar
  4275. * @param {Component} parent
  4276. * @param {Component[]} [depends] Components on which this components depends
  4277. * (except for the parent)
  4278. * @param {Object} [options] Available parameters:
  4279. * {Boolean} [showCustomTime]
  4280. * @constructor CustomTime
  4281. * @extends Component
  4282. */
  4283. function CustomTime (parent, depends, options) {
  4284. this.id = util.randomUUID();
  4285. this.parent = parent;
  4286. this.depends = depends;
  4287. this.options = options || {};
  4288. this.defaultOptions = {
  4289. showCustomTime: false
  4290. };
  4291. this.listeners = [];
  4292. this.customTime = new Date();
  4293. }
  4294. CustomTime.prototype = new Component();
  4295. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4296. /**
  4297. * Get the container element of the bar, which can be used by a child to
  4298. * add its own widgets.
  4299. * @returns {HTMLElement} container
  4300. */
  4301. CustomTime.prototype.getContainer = function () {
  4302. return this.frame;
  4303. };
  4304. /**
  4305. * Repaint the component
  4306. * @return {Boolean} changed
  4307. */
  4308. CustomTime.prototype.repaint = function () {
  4309. var bar = this.frame,
  4310. parent = this.parent,
  4311. parentContainer = parent.parent.getContainer();
  4312. if (!parent) {
  4313. throw new Error('Cannot repaint bar: no parent attached');
  4314. }
  4315. if (!parentContainer) {
  4316. throw new Error('Cannot repaint bar: parent has no container element');
  4317. }
  4318. if (!this.getOption('showCustomTime')) {
  4319. if (bar) {
  4320. parentContainer.removeChild(bar);
  4321. delete this.frame;
  4322. }
  4323. return;
  4324. }
  4325. if (!bar) {
  4326. bar = document.createElement('div');
  4327. bar.className = 'customtime';
  4328. bar.style.position = 'absolute';
  4329. bar.style.top = '0px';
  4330. bar.style.height = '100%';
  4331. parentContainer.appendChild(bar);
  4332. var drag = document.createElement('div');
  4333. drag.style.position = 'relative';
  4334. drag.style.top = '0px';
  4335. drag.style.left = '-10px';
  4336. drag.style.height = '100%';
  4337. drag.style.width = '20px';
  4338. bar.appendChild(drag);
  4339. this.frame = bar;
  4340. this.subscribe(this, 'movetime');
  4341. }
  4342. if (!parent.conversion) {
  4343. parent._updateConversion();
  4344. }
  4345. var x = parent.toScreen(this.customTime);
  4346. bar.style.left = x + 'px';
  4347. bar.title = 'Time: ' + this.customTime;
  4348. return false;
  4349. };
  4350. /**
  4351. * Set custom time.
  4352. * @param {Date} time
  4353. */
  4354. CustomTime.prototype._setCustomTime = function(time) {
  4355. this.customTime = new Date(time.valueOf());
  4356. this.repaint();
  4357. };
  4358. /**
  4359. * Retrieve the current custom time.
  4360. * @return {Date} customTime
  4361. */
  4362. CustomTime.prototype._getCustomTime = function() {
  4363. return new Date(this.customTime.valueOf());
  4364. };
  4365. /**
  4366. * Add listeners for mouse and touch events to the component
  4367. * @param {Component} component
  4368. */
  4369. CustomTime.prototype.subscribe = function (component, event) {
  4370. var me = this;
  4371. var listener = {
  4372. component: component,
  4373. event: event,
  4374. callback: function (event) {
  4375. me._onMouseDown(event, listener);
  4376. },
  4377. params: {}
  4378. };
  4379. component.on('mousedown', listener.callback);
  4380. me.listeners.push(listener);
  4381. };
  4382. /**
  4383. * Event handler
  4384. * @param {String} event name of the event, for example 'click', 'mousemove'
  4385. * @param {function} callback callback handler, invoked with the raw HTML Event
  4386. * as parameter.
  4387. */
  4388. CustomTime.prototype.on = function (event, callback) {
  4389. var bar = this.frame;
  4390. if (!bar) {
  4391. throw new Error('Cannot add event listener: no parent attached');
  4392. }
  4393. events.addListener(this, event, callback);
  4394. util.addEventListener(bar, event, callback);
  4395. };
  4396. /**
  4397. * Start moving horizontally
  4398. * @param {Event} event
  4399. * @param {Object} listener Listener containing the component and params
  4400. * @private
  4401. */
  4402. CustomTime.prototype._onMouseDown = function(event, listener) {
  4403. event = event || window.event;
  4404. var params = listener.params;
  4405. // only react on left mouse button down
  4406. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  4407. if (!leftButtonDown) {
  4408. return;
  4409. }
  4410. // get mouse position
  4411. params.mouseX = util.getPageX(event);
  4412. params.moved = false;
  4413. params.customTime = this.customTime;
  4414. // add event listeners to handle moving the custom time bar
  4415. var me = this;
  4416. if (!params.onMouseMove) {
  4417. params.onMouseMove = function (event) {
  4418. me._onMouseMove(event, listener);
  4419. };
  4420. util.addEventListener(document, 'mousemove', params.onMouseMove);
  4421. }
  4422. if (!params.onMouseUp) {
  4423. params.onMouseUp = function (event) {
  4424. me._onMouseUp(event, listener);
  4425. };
  4426. util.addEventListener(document, 'mouseup', params.onMouseUp);
  4427. }
  4428. util.stopPropagation(event);
  4429. util.preventDefault(event);
  4430. };
  4431. /**
  4432. * Perform moving operating.
  4433. * This function activated from within the funcion CustomTime._onMouseDown().
  4434. * @param {Event} event
  4435. * @param {Object} listener
  4436. * @private
  4437. */
  4438. CustomTime.prototype._onMouseMove = function (event, listener) {
  4439. event = event || window.event;
  4440. var params = listener.params;
  4441. var parent = this.parent;
  4442. // calculate change in mouse position
  4443. var mouseX = util.getPageX(event);
  4444. if (params.mouseX === undefined) {
  4445. params.mouseX = mouseX;
  4446. }
  4447. var diff = mouseX - params.mouseX;
  4448. // if mouse movement is big enough, register it as a "moved" event
  4449. if (Math.abs(diff) >= 1) {
  4450. params.moved = true;
  4451. }
  4452. var x = parent.toScreen(params.customTime);
  4453. var xnew = x + diff;
  4454. var time = parent.toTime(xnew);
  4455. this._setCustomTime(time);
  4456. // fire a timechange event
  4457. events.trigger(this, 'timechange', {customTime: this.customTime});
  4458. util.preventDefault(event);
  4459. };
  4460. /**
  4461. * Stop moving operating.
  4462. * This function activated from within the function CustomTime._onMouseDown().
  4463. * @param {event} event
  4464. * @param {Object} listener
  4465. * @private
  4466. */
  4467. CustomTime.prototype._onMouseUp = function (event, listener) {
  4468. event = event || window.event;
  4469. var params = listener.params;
  4470. // remove event listeners here, important for Safari
  4471. if (params.onMouseMove) {
  4472. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  4473. params.onMouseMove = null;
  4474. }
  4475. if (params.onMouseUp) {
  4476. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  4477. params.onMouseUp = null;
  4478. }
  4479. if (params.moved) {
  4480. // fire a timechanged event
  4481. events.trigger(this, 'timechanged', {customTime: this.customTime});
  4482. }
  4483. };
  4484. /**
  4485. * An ItemSet holds a set of items and ranges which can be displayed in a
  4486. * range. The width is determined by the parent of the ItemSet, and the height
  4487. * is determined by the size of the items.
  4488. * @param {Component} parent
  4489. * @param {Component[]} [depends] Components on which this components depends
  4490. * (except for the parent)
  4491. * @param {Object} [options] See ItemSet.setOptions for the available
  4492. * options.
  4493. * @constructor ItemSet
  4494. * @extends Panel
  4495. */
  4496. // TODO: improve performance by replacing all Array.forEach with a for loop
  4497. function ItemSet(parent, depends, options) {
  4498. this.id = util.randomUUID();
  4499. this.parent = parent;
  4500. this.depends = depends;
  4501. // one options object is shared by this itemset and all its items
  4502. this.options = options || {};
  4503. this.defaultOptions = {
  4504. type: 'box',
  4505. align: 'center',
  4506. orientation: 'bottom',
  4507. margin: {
  4508. axis: 20,
  4509. item: 10
  4510. },
  4511. padding: 5
  4512. };
  4513. this.dom = {};
  4514. var me = this;
  4515. this.itemsData = null; // DataSet
  4516. this.range = null; // Range or Object {start: number, end: number}
  4517. this.listeners = {
  4518. 'add': function (event, params, senderId) {
  4519. if (senderId != me.id) {
  4520. me._onAdd(params.items);
  4521. }
  4522. },
  4523. 'update': function (event, params, senderId) {
  4524. if (senderId != me.id) {
  4525. me._onUpdate(params.items);
  4526. }
  4527. },
  4528. 'remove': function (event, params, senderId) {
  4529. if (senderId != me.id) {
  4530. me._onRemove(params.items);
  4531. }
  4532. }
  4533. };
  4534. this.items = {}; // object with an Item for every data item
  4535. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4536. this.stack = new Stack(this, Object.create(this.options));
  4537. this.conversion = null;
  4538. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4539. }
  4540. ItemSet.prototype = new Panel();
  4541. // available item types will be registered here
  4542. ItemSet.types = {
  4543. box: ItemBox,
  4544. range: ItemRange,
  4545. rangeoverflow: ItemRangeOverflow,
  4546. point: ItemPoint
  4547. };
  4548. /**
  4549. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4550. * @param {Object} [options] The following options are available:
  4551. * {String | function} [className]
  4552. * class name for the itemset
  4553. * {String} [type]
  4554. * Default type for the items. Choose from 'box'
  4555. * (default), 'point', or 'range'. The default
  4556. * Style can be overwritten by individual items.
  4557. * {String} align
  4558. * Alignment for the items, only applicable for
  4559. * ItemBox. Choose 'center' (default), 'left', or
  4560. * 'right'.
  4561. * {String} orientation
  4562. * Orientation of the item set. Choose 'top' or
  4563. * 'bottom' (default).
  4564. * {Number} margin.axis
  4565. * Margin between the axis and the items in pixels.
  4566. * Default is 20.
  4567. * {Number} margin.item
  4568. * Margin between items in pixels. Default is 10.
  4569. * {Number} padding
  4570. * Padding of the contents of an item in pixels.
  4571. * Must correspond with the items css. Default is 5.
  4572. */
  4573. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4574. /**
  4575. * Set range (start and end).
  4576. * @param {Range | Object} range A Range or an object containing start and end.
  4577. */
  4578. ItemSet.prototype.setRange = function setRange(range) {
  4579. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4580. throw new TypeError('Range must be an instance of Range, ' +
  4581. 'or an object containing start and end.');
  4582. }
  4583. this.range = range;
  4584. };
  4585. /**
  4586. * Repaint the component
  4587. * @return {Boolean} changed
  4588. */
  4589. ItemSet.prototype.repaint = function repaint() {
  4590. var changed = 0,
  4591. update = util.updateProperty,
  4592. asSize = util.option.asSize,
  4593. options = this.options,
  4594. orientation = this.getOption('orientation'),
  4595. defaultOptions = this.defaultOptions,
  4596. frame = this.frame;
  4597. if (!frame) {
  4598. frame = document.createElement('div');
  4599. frame.className = 'itemset';
  4600. var className = options.className;
  4601. if (className) {
  4602. util.addClassName(frame, util.option.asString(className));
  4603. }
  4604. // create background panel
  4605. var background = document.createElement('div');
  4606. background.className = 'background';
  4607. frame.appendChild(background);
  4608. this.dom.background = background;
  4609. // create foreground panel
  4610. var foreground = document.createElement('div');
  4611. foreground.className = 'foreground';
  4612. frame.appendChild(foreground);
  4613. this.dom.foreground = foreground;
  4614. // create axis panel
  4615. var axis = document.createElement('div');
  4616. axis.className = 'itemset-axis';
  4617. //frame.appendChild(axis);
  4618. this.dom.axis = axis;
  4619. this.frame = frame;
  4620. changed += 1;
  4621. }
  4622. if (!this.parent) {
  4623. throw new Error('Cannot repaint itemset: no parent attached');
  4624. }
  4625. var parentContainer = this.parent.getContainer();
  4626. if (!parentContainer) {
  4627. throw new Error('Cannot repaint itemset: parent has no container element');
  4628. }
  4629. if (!frame.parentNode) {
  4630. parentContainer.appendChild(frame);
  4631. changed += 1;
  4632. }
  4633. if (!this.dom.axis.parentNode) {
  4634. parentContainer.appendChild(this.dom.axis);
  4635. changed += 1;
  4636. }
  4637. // reposition frame
  4638. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4639. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4640. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4641. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4642. // reposition axis
  4643. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4644. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4645. if (orientation == 'bottom') {
  4646. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4647. }
  4648. else { // orientation == 'top'
  4649. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4650. }
  4651. this._updateConversion();
  4652. var me = this,
  4653. queue = this.queue,
  4654. itemsData = this.itemsData,
  4655. items = this.items,
  4656. dataOptions = {
  4657. // TODO: cleanup
  4658. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  4659. };
  4660. // show/hide added/changed/removed items
  4661. Object.keys(queue).forEach(function (id) {
  4662. //var entry = queue[id];
  4663. var action = queue[id];
  4664. var item = items[id];
  4665. //var item = entry.item;
  4666. //noinspection FallthroughInSwitchStatementJS
  4667. switch (action) {
  4668. case 'add':
  4669. case 'update':
  4670. var itemData = itemsData && itemsData.get(id, dataOptions);
  4671. if (itemData) {
  4672. var type = itemData.type ||
  4673. (itemData.start && itemData.end && 'range') ||
  4674. options.type ||
  4675. 'box';
  4676. var constructor = ItemSet.types[type];
  4677. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4678. if (item) {
  4679. // update item
  4680. if (!constructor || !(item instanceof constructor)) {
  4681. // item type has changed, hide and delete the item
  4682. changed += item.hide();
  4683. item = null;
  4684. }
  4685. else {
  4686. item.data = itemData; // TODO: create a method item.setData ?
  4687. changed++;
  4688. }
  4689. }
  4690. if (!item) {
  4691. // create item
  4692. if (constructor) {
  4693. item = new constructor(me, itemData, options, defaultOptions);
  4694. changed++;
  4695. }
  4696. else {
  4697. throw new TypeError('Unknown item type "' + type + '"');
  4698. }
  4699. }
  4700. // force a repaint (not only a reposition)
  4701. item.repaint();
  4702. items[id] = item;
  4703. }
  4704. // update queue
  4705. delete queue[id];
  4706. break;
  4707. case 'remove':
  4708. if (item) {
  4709. // remove DOM of the item
  4710. changed += item.hide();
  4711. }
  4712. // update lists
  4713. delete items[id];
  4714. delete queue[id];
  4715. break;
  4716. default:
  4717. console.log('Error: unknown action "' + action + '"');
  4718. }
  4719. });
  4720. // reposition all items. Show items only when in the visible area
  4721. util.forEach(this.items, function (item) {
  4722. if (item.visible) {
  4723. changed += item.show();
  4724. item.reposition();
  4725. }
  4726. else {
  4727. changed += item.hide();
  4728. }
  4729. });
  4730. return (changed > 0);
  4731. };
  4732. /**
  4733. * Get the foreground container element
  4734. * @return {HTMLElement} foreground
  4735. */
  4736. ItemSet.prototype.getForeground = function getForeground() {
  4737. return this.dom.foreground;
  4738. };
  4739. /**
  4740. * Get the background container element
  4741. * @return {HTMLElement} background
  4742. */
  4743. ItemSet.prototype.getBackground = function getBackground() {
  4744. return this.dom.background;
  4745. };
  4746. /**
  4747. * Get the axis container element
  4748. * @return {HTMLElement} axis
  4749. */
  4750. ItemSet.prototype.getAxis = function getAxis() {
  4751. return this.dom.axis;
  4752. };
  4753. /**
  4754. * Reflow the component
  4755. * @return {Boolean} resized
  4756. */
  4757. ItemSet.prototype.reflow = function reflow () {
  4758. var changed = 0,
  4759. options = this.options,
  4760. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4761. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4762. update = util.updateProperty,
  4763. asNumber = util.option.asNumber,
  4764. asSize = util.option.asSize,
  4765. frame = this.frame;
  4766. if (frame) {
  4767. this._updateConversion();
  4768. util.forEach(this.items, function (item) {
  4769. changed += item.reflow();
  4770. });
  4771. // TODO: stack.update should be triggered via an event, in stack itself
  4772. // TODO: only update the stack when there are changed items
  4773. this.stack.update();
  4774. var maxHeight = asNumber(options.maxHeight);
  4775. var fixedHeight = (asSize(options.height) != null);
  4776. var height;
  4777. if (fixedHeight) {
  4778. height = frame.offsetHeight;
  4779. }
  4780. else {
  4781. // height is not specified, determine the height from the height and positioned items
  4782. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4783. if (visibleItems.length) {
  4784. var min = visibleItems[0].top;
  4785. var max = visibleItems[0].top + visibleItems[0].height;
  4786. util.forEach(visibleItems, function (item) {
  4787. min = Math.min(min, item.top);
  4788. max = Math.max(max, (item.top + item.height));
  4789. });
  4790. height = (max - min) + marginAxis + marginItem;
  4791. }
  4792. else {
  4793. height = marginAxis + marginItem;
  4794. }
  4795. }
  4796. if (maxHeight != null) {
  4797. height = Math.min(height, maxHeight);
  4798. }
  4799. changed += update(this, 'height', height);
  4800. // calculate height from items
  4801. changed += update(this, 'top', frame.offsetTop);
  4802. changed += update(this, 'left', frame.offsetLeft);
  4803. changed += update(this, 'width', frame.offsetWidth);
  4804. }
  4805. else {
  4806. changed += 1;
  4807. }
  4808. return (changed > 0);
  4809. };
  4810. /**
  4811. * Hide this component from the DOM
  4812. * @return {Boolean} changed
  4813. */
  4814. ItemSet.prototype.hide = function hide() {
  4815. var changed = false;
  4816. // remove the DOM
  4817. if (this.frame && this.frame.parentNode) {
  4818. this.frame.parentNode.removeChild(this.frame);
  4819. changed = true;
  4820. }
  4821. if (this.dom.axis && this.dom.axis.parentNode) {
  4822. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4823. changed = true;
  4824. }
  4825. return changed;
  4826. };
  4827. /**
  4828. * Set items
  4829. * @param {vis.DataSet | null} items
  4830. */
  4831. ItemSet.prototype.setItems = function setItems(items) {
  4832. var me = this,
  4833. ids,
  4834. oldItemsData = this.itemsData;
  4835. // replace the dataset
  4836. if (!items) {
  4837. this.itemsData = null;
  4838. }
  4839. else if (items instanceof DataSet || items instanceof DataView) {
  4840. this.itemsData = items;
  4841. }
  4842. else {
  4843. throw new TypeError('Data must be an instance of DataSet');
  4844. }
  4845. if (oldItemsData) {
  4846. // unsubscribe from old dataset
  4847. util.forEach(this.listeners, function (callback, event) {
  4848. oldItemsData.unsubscribe(event, callback);
  4849. });
  4850. // remove all drawn items
  4851. ids = oldItemsData.getIds();
  4852. this._onRemove(ids);
  4853. }
  4854. if (this.itemsData) {
  4855. // subscribe to new dataset
  4856. var id = this.id;
  4857. util.forEach(this.listeners, function (callback, event) {
  4858. me.itemsData.subscribe(event, callback, id);
  4859. });
  4860. // draw all new items
  4861. ids = this.itemsData.getIds();
  4862. this._onAdd(ids);
  4863. }
  4864. };
  4865. /**
  4866. * Get the current items items
  4867. * @returns {vis.DataSet | null}
  4868. */
  4869. ItemSet.prototype.getItems = function getItems() {
  4870. return this.itemsData;
  4871. };
  4872. /**
  4873. * Handle updated items
  4874. * @param {Number[]} ids
  4875. * @private
  4876. */
  4877. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4878. this._toQueue('update', ids);
  4879. };
  4880. /**
  4881. * Handle changed items
  4882. * @param {Number[]} ids
  4883. * @private
  4884. */
  4885. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4886. this._toQueue('add', ids);
  4887. };
  4888. /**
  4889. * Handle removed items
  4890. * @param {Number[]} ids
  4891. * @private
  4892. */
  4893. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4894. this._toQueue('remove', ids);
  4895. };
  4896. /**
  4897. * Put items in the queue to be added/updated/remove
  4898. * @param {String} action can be 'add', 'update', 'remove'
  4899. * @param {Number[]} ids
  4900. */
  4901. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4902. var queue = this.queue;
  4903. ids.forEach(function (id) {
  4904. queue[id] = action;
  4905. });
  4906. if (this.controller) {
  4907. //this.requestReflow();
  4908. this.requestRepaint();
  4909. }
  4910. };
  4911. /**
  4912. * Calculate the scale and offset to convert a position on screen to the
  4913. * corresponding date and vice versa.
  4914. * After the method _updateConversion is executed once, the methods toTime
  4915. * and toScreen can be used.
  4916. * @private
  4917. */
  4918. ItemSet.prototype._updateConversion = function _updateConversion() {
  4919. var range = this.range;
  4920. if (!range) {
  4921. throw new Error('No range configured');
  4922. }
  4923. if (range.conversion) {
  4924. this.conversion = range.conversion(this.width);
  4925. }
  4926. else {
  4927. this.conversion = Range.conversion(range.start, range.end, this.width);
  4928. }
  4929. };
  4930. /**
  4931. * Convert a position on screen (pixels) to a datetime
  4932. * Before this method can be used, the method _updateConversion must be
  4933. * executed once.
  4934. * @param {int} x Position on the screen in pixels
  4935. * @return {Date} time The datetime the corresponds with given position x
  4936. */
  4937. ItemSet.prototype.toTime = function toTime(x) {
  4938. var conversion = this.conversion;
  4939. return new Date(x / conversion.scale + conversion.offset);
  4940. };
  4941. /**
  4942. * Convert a datetime (Date object) into a position on the screen
  4943. * Before this method can be used, the method _updateConversion must be
  4944. * executed once.
  4945. * @param {Date} time A date
  4946. * @return {int} x The position on the screen in pixels which corresponds
  4947. * with the given date.
  4948. */
  4949. ItemSet.prototype.toScreen = function toScreen(time) {
  4950. var conversion = this.conversion;
  4951. return (time.valueOf() - conversion.offset) * conversion.scale;
  4952. };
  4953. /**
  4954. * @constructor Item
  4955. * @param {ItemSet} parent
  4956. * @param {Object} data Object containing (optional) parameters type,
  4957. * start, end, content, group, className.
  4958. * @param {Object} [options] Options to set initial property values
  4959. * @param {Object} [defaultOptions] default options
  4960. * // TODO: describe available options
  4961. */
  4962. function Item (parent, data, options, defaultOptions) {
  4963. this.parent = parent;
  4964. this.data = data;
  4965. this.dom = null;
  4966. this.options = options || {};
  4967. this.defaultOptions = defaultOptions || {};
  4968. this.selected = false;
  4969. this.visible = false;
  4970. this.top = 0;
  4971. this.left = 0;
  4972. this.width = 0;
  4973. this.height = 0;
  4974. }
  4975. /**
  4976. * Select current item
  4977. */
  4978. Item.prototype.select = function select() {
  4979. this.selected = true;
  4980. };
  4981. /**
  4982. * Unselect current item
  4983. */
  4984. Item.prototype.unselect = function unselect() {
  4985. this.selected = false;
  4986. };
  4987. /**
  4988. * Show the Item in the DOM (when not already visible)
  4989. * @return {Boolean} changed
  4990. */
  4991. Item.prototype.show = function show() {
  4992. return false;
  4993. };
  4994. /**
  4995. * Hide the Item from the DOM (when visible)
  4996. * @return {Boolean} changed
  4997. */
  4998. Item.prototype.hide = function hide() {
  4999. return false;
  5000. };
  5001. /**
  5002. * Repaint the item
  5003. * @return {Boolean} changed
  5004. */
  5005. Item.prototype.repaint = function repaint() {
  5006. // should be implemented by the item
  5007. return false;
  5008. };
  5009. /**
  5010. * Reflow the item
  5011. * @return {Boolean} resized
  5012. */
  5013. Item.prototype.reflow = function reflow() {
  5014. // should be implemented by the item
  5015. return false;
  5016. };
  5017. /**
  5018. * Return the items width
  5019. * @return {Integer} width
  5020. */
  5021. Item.prototype.getWidth = function getWidth() {
  5022. return this.width;
  5023. }
  5024. /**
  5025. * @constructor ItemBox
  5026. * @extends Item
  5027. * @param {ItemSet} parent
  5028. * @param {Object} data Object containing parameters start
  5029. * content, className.
  5030. * @param {Object} [options] Options to set initial property values
  5031. * @param {Object} [defaultOptions] default options
  5032. * // TODO: describe available options
  5033. */
  5034. function ItemBox (parent, data, options, defaultOptions) {
  5035. this.props = {
  5036. dot: {
  5037. left: 0,
  5038. top: 0,
  5039. width: 0,
  5040. height: 0
  5041. },
  5042. line: {
  5043. top: 0,
  5044. left: 0,
  5045. width: 0,
  5046. height: 0
  5047. }
  5048. };
  5049. Item.call(this, parent, data, options, defaultOptions);
  5050. }
  5051. ItemBox.prototype = new Item (null, null);
  5052. /**
  5053. * Select the item
  5054. * @override
  5055. */
  5056. ItemBox.prototype.select = function select() {
  5057. this.selected = true;
  5058. // TODO: select and unselect
  5059. };
  5060. /**
  5061. * Unselect the item
  5062. * @override
  5063. */
  5064. ItemBox.prototype.unselect = function unselect() {
  5065. this.selected = false;
  5066. // TODO: select and unselect
  5067. };
  5068. /**
  5069. * Repaint the item
  5070. * @return {Boolean} changed
  5071. */
  5072. ItemBox.prototype.repaint = function repaint() {
  5073. // TODO: make an efficient repaint
  5074. var changed = false;
  5075. var dom = this.dom;
  5076. if (!dom) {
  5077. this._create();
  5078. dom = this.dom;
  5079. changed = true;
  5080. }
  5081. if (dom) {
  5082. if (!this.parent) {
  5083. throw new Error('Cannot repaint item: no parent attached');
  5084. }
  5085. if (!dom.box.parentNode) {
  5086. var foreground = this.parent.getForeground();
  5087. if (!foreground) {
  5088. throw new Error('Cannot repaint time axis: ' +
  5089. 'parent has no foreground container element');
  5090. }
  5091. foreground.appendChild(dom.box);
  5092. changed = true;
  5093. }
  5094. if (!dom.line.parentNode) {
  5095. var background = this.parent.getBackground();
  5096. if (!background) {
  5097. throw new Error('Cannot repaint time axis: ' +
  5098. 'parent has no background container element');
  5099. }
  5100. background.appendChild(dom.line);
  5101. changed = true;
  5102. }
  5103. if (!dom.dot.parentNode) {
  5104. var axis = this.parent.getAxis();
  5105. if (!background) {
  5106. throw new Error('Cannot repaint time axis: ' +
  5107. 'parent has no axis container element');
  5108. }
  5109. axis.appendChild(dom.dot);
  5110. changed = true;
  5111. }
  5112. // update contents
  5113. if (this.data.content != this.content) {
  5114. this.content = this.data.content;
  5115. if (this.content instanceof Element) {
  5116. dom.content.innerHTML = '';
  5117. dom.content.appendChild(this.content);
  5118. }
  5119. else if (this.data.content != undefined) {
  5120. dom.content.innerHTML = this.content;
  5121. }
  5122. else {
  5123. throw new Error('Property "content" missing in item ' + this.data.id);
  5124. }
  5125. changed = true;
  5126. }
  5127. // update class
  5128. var className = (this.data.className? ' ' + this.data.className : '') +
  5129. (this.selected ? ' selected' : '');
  5130. if (this.className != className) {
  5131. this.className = className;
  5132. dom.box.className = 'item box' + className;
  5133. dom.line.className = 'item line' + className;
  5134. dom.dot.className = 'item dot' + className;
  5135. changed = true;
  5136. }
  5137. }
  5138. return changed;
  5139. };
  5140. /**
  5141. * Show the item in the DOM (when not already visible). The items DOM will
  5142. * be created when needed.
  5143. * @return {Boolean} changed
  5144. */
  5145. ItemBox.prototype.show = function show() {
  5146. if (!this.dom || !this.dom.box.parentNode) {
  5147. return this.repaint();
  5148. }
  5149. else {
  5150. return false;
  5151. }
  5152. };
  5153. /**
  5154. * Hide the item from the DOM (when visible)
  5155. * @return {Boolean} changed
  5156. */
  5157. ItemBox.prototype.hide = function hide() {
  5158. var changed = false,
  5159. dom = this.dom;
  5160. if (dom) {
  5161. if (dom.box.parentNode) {
  5162. dom.box.parentNode.removeChild(dom.box);
  5163. changed = true;
  5164. }
  5165. if (dom.line.parentNode) {
  5166. dom.line.parentNode.removeChild(dom.line);
  5167. }
  5168. if (dom.dot.parentNode) {
  5169. dom.dot.parentNode.removeChild(dom.dot);
  5170. }
  5171. }
  5172. return changed;
  5173. };
  5174. /**
  5175. * Reflow the item: calculate its actual size and position from the DOM
  5176. * @return {boolean} resized returns true if the axis is resized
  5177. * @override
  5178. */
  5179. ItemBox.prototype.reflow = function reflow() {
  5180. var changed = 0,
  5181. update,
  5182. dom,
  5183. props,
  5184. options,
  5185. margin,
  5186. start,
  5187. align,
  5188. orientation,
  5189. top,
  5190. left,
  5191. data,
  5192. range;
  5193. if (this.data.start == undefined) {
  5194. throw new Error('Property "start" missing in item ' + this.data.id);
  5195. }
  5196. data = this.data;
  5197. range = this.parent && this.parent.range;
  5198. if (data && range) {
  5199. // TODO: account for the width of the item
  5200. var interval = (range.end - range.start);
  5201. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5202. }
  5203. else {
  5204. this.visible = false;
  5205. }
  5206. if (this.visible) {
  5207. dom = this.dom;
  5208. if (dom) {
  5209. update = util.updateProperty;
  5210. props = this.props;
  5211. options = this.options;
  5212. start = this.parent.toScreen(this.data.start);
  5213. align = options.align || this.defaultOptions.align;
  5214. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5215. orientation = options.orientation || this.defaultOptions.orientation;
  5216. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5217. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5218. changed += update(props.line, 'width', dom.line.offsetWidth);
  5219. changed += update(props.line, 'height', dom.line.offsetHeight);
  5220. changed += update(props.line, 'top', dom.line.offsetTop);
  5221. changed += update(this, 'width', dom.box.offsetWidth);
  5222. changed += update(this, 'height', dom.box.offsetHeight);
  5223. if (align == 'right') {
  5224. left = start - this.width;
  5225. }
  5226. else if (align == 'left') {
  5227. left = start;
  5228. }
  5229. else {
  5230. // default or 'center'
  5231. left = start - this.width / 2;
  5232. }
  5233. changed += update(this, 'left', left);
  5234. changed += update(props.line, 'left', start - props.line.width / 2);
  5235. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5236. changed += update(props.dot, 'top', -props.dot.height / 2);
  5237. if (orientation == 'top') {
  5238. top = margin;
  5239. changed += update(this, 'top', top);
  5240. }
  5241. else {
  5242. // default or 'bottom'
  5243. var parentHeight = this.parent.height;
  5244. top = parentHeight - this.height - margin;
  5245. changed += update(this, 'top', top);
  5246. }
  5247. }
  5248. else {
  5249. changed += 1;
  5250. }
  5251. }
  5252. return (changed > 0);
  5253. };
  5254. /**
  5255. * Create an items DOM
  5256. * @private
  5257. */
  5258. ItemBox.prototype._create = function _create() {
  5259. var dom = this.dom;
  5260. if (!dom) {
  5261. this.dom = dom = {};
  5262. // create the box
  5263. dom.box = document.createElement('DIV');
  5264. // className is updated in repaint()
  5265. // contents box (inside the background box). used for making margins
  5266. dom.content = document.createElement('DIV');
  5267. dom.content.className = 'content';
  5268. dom.box.appendChild(dom.content);
  5269. // line to axis
  5270. dom.line = document.createElement('DIV');
  5271. dom.line.className = 'line';
  5272. // dot on axis
  5273. dom.dot = document.createElement('DIV');
  5274. dom.dot.className = 'dot';
  5275. }
  5276. };
  5277. /**
  5278. * Reposition the item, recalculate its left, top, and width, using the current
  5279. * range and size of the items itemset
  5280. * @override
  5281. */
  5282. ItemBox.prototype.reposition = function reposition() {
  5283. var dom = this.dom,
  5284. props = this.props,
  5285. orientation = this.options.orientation || this.defaultOptions.orientation;
  5286. if (dom) {
  5287. var box = dom.box,
  5288. line = dom.line,
  5289. dot = dom.dot;
  5290. box.style.left = this.left + 'px';
  5291. box.style.top = this.top + 'px';
  5292. line.style.left = props.line.left + 'px';
  5293. if (orientation == 'top') {
  5294. line.style.top = 0 + 'px';
  5295. line.style.height = this.top + 'px';
  5296. }
  5297. else {
  5298. // orientation 'bottom'
  5299. line.style.top = (this.top + this.height) + 'px';
  5300. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5301. this.props.dot.height / 2, 0) + 'px';
  5302. }
  5303. dot.style.left = props.dot.left + 'px';
  5304. dot.style.top = props.dot.top + 'px';
  5305. }
  5306. };
  5307. /**
  5308. * @constructor ItemPoint
  5309. * @extends Item
  5310. * @param {ItemSet} parent
  5311. * @param {Object} data Object containing parameters start
  5312. * content, className.
  5313. * @param {Object} [options] Options to set initial property values
  5314. * @param {Object} [defaultOptions] default options
  5315. * // TODO: describe available options
  5316. */
  5317. function ItemPoint (parent, data, options, defaultOptions) {
  5318. this.props = {
  5319. dot: {
  5320. top: 0,
  5321. width: 0,
  5322. height: 0
  5323. },
  5324. content: {
  5325. height: 0,
  5326. marginLeft: 0
  5327. }
  5328. };
  5329. Item.call(this, parent, data, options, defaultOptions);
  5330. }
  5331. ItemPoint.prototype = new Item (null, null);
  5332. /**
  5333. * Select the item
  5334. * @override
  5335. */
  5336. ItemPoint.prototype.select = function select() {
  5337. this.selected = true;
  5338. // TODO: select and unselect
  5339. };
  5340. /**
  5341. * Unselect the item
  5342. * @override
  5343. */
  5344. ItemPoint.prototype.unselect = function unselect() {
  5345. this.selected = false;
  5346. // TODO: select and unselect
  5347. };
  5348. /**
  5349. * Repaint the item
  5350. * @return {Boolean} changed
  5351. */
  5352. ItemPoint.prototype.repaint = function repaint() {
  5353. // TODO: make an efficient repaint
  5354. var changed = false;
  5355. var dom = this.dom;
  5356. if (!dom) {
  5357. this._create();
  5358. dom = this.dom;
  5359. changed = true;
  5360. }
  5361. if (dom) {
  5362. if (!this.parent) {
  5363. throw new Error('Cannot repaint item: no parent attached');
  5364. }
  5365. var foreground = this.parent.getForeground();
  5366. if (!foreground) {
  5367. throw new Error('Cannot repaint time axis: ' +
  5368. 'parent has no foreground container element');
  5369. }
  5370. if (!dom.point.parentNode) {
  5371. foreground.appendChild(dom.point);
  5372. foreground.appendChild(dom.point);
  5373. changed = true;
  5374. }
  5375. // update contents
  5376. if (this.data.content != this.content) {
  5377. this.content = this.data.content;
  5378. if (this.content instanceof Element) {
  5379. dom.content.innerHTML = '';
  5380. dom.content.appendChild(this.content);
  5381. }
  5382. else if (this.data.content != undefined) {
  5383. dom.content.innerHTML = this.content;
  5384. }
  5385. else {
  5386. throw new Error('Property "content" missing in item ' + this.data.id);
  5387. }
  5388. changed = true;
  5389. }
  5390. // update class
  5391. var className = (this.data.className? ' ' + this.data.className : '') +
  5392. (this.selected ? ' selected' : '');
  5393. if (this.className != className) {
  5394. this.className = className;
  5395. dom.point.className = 'item point' + className;
  5396. changed = true;
  5397. }
  5398. }
  5399. return changed;
  5400. };
  5401. /**
  5402. * Show the item in the DOM (when not already visible). The items DOM will
  5403. * be created when needed.
  5404. * @return {Boolean} changed
  5405. */
  5406. ItemPoint.prototype.show = function show() {
  5407. if (!this.dom || !this.dom.point.parentNode) {
  5408. return this.repaint();
  5409. }
  5410. else {
  5411. return false;
  5412. }
  5413. };
  5414. /**
  5415. * Hide the item from the DOM (when visible)
  5416. * @return {Boolean} changed
  5417. */
  5418. ItemPoint.prototype.hide = function hide() {
  5419. var changed = false,
  5420. dom = this.dom;
  5421. if (dom) {
  5422. if (dom.point.parentNode) {
  5423. dom.point.parentNode.removeChild(dom.point);
  5424. changed = true;
  5425. }
  5426. }
  5427. return changed;
  5428. };
  5429. /**
  5430. * Reflow the item: calculate its actual size from the DOM
  5431. * @return {boolean} resized returns true if the axis is resized
  5432. * @override
  5433. */
  5434. ItemPoint.prototype.reflow = function reflow() {
  5435. var changed = 0,
  5436. update,
  5437. dom,
  5438. props,
  5439. options,
  5440. margin,
  5441. orientation,
  5442. start,
  5443. top,
  5444. data,
  5445. range;
  5446. if (this.data.start == undefined) {
  5447. throw new Error('Property "start" missing in item ' + this.data.id);
  5448. }
  5449. data = this.data;
  5450. range = this.parent && this.parent.range;
  5451. if (data && range) {
  5452. // TODO: account for the width of the item
  5453. var interval = (range.end - range.start);
  5454. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5455. }
  5456. else {
  5457. this.visible = false;
  5458. }
  5459. if (this.visible) {
  5460. dom = this.dom;
  5461. if (dom) {
  5462. update = util.updateProperty;
  5463. props = this.props;
  5464. options = this.options;
  5465. orientation = options.orientation || this.defaultOptions.orientation;
  5466. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5467. start = this.parent.toScreen(this.data.start);
  5468. changed += update(this, 'width', dom.point.offsetWidth);
  5469. changed += update(this, 'height', dom.point.offsetHeight);
  5470. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5471. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5472. changed += update(props.content, 'height', dom.content.offsetHeight);
  5473. if (orientation == 'top') {
  5474. top = margin;
  5475. }
  5476. else {
  5477. // default or 'bottom'
  5478. var parentHeight = this.parent.height;
  5479. top = Math.max(parentHeight - this.height - margin, 0);
  5480. }
  5481. changed += update(this, 'top', top);
  5482. changed += update(this, 'left', start - props.dot.width / 2);
  5483. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5484. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5485. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5486. }
  5487. else {
  5488. changed += 1;
  5489. }
  5490. }
  5491. return (changed > 0);
  5492. };
  5493. /**
  5494. * Create an items DOM
  5495. * @private
  5496. */
  5497. ItemPoint.prototype._create = function _create() {
  5498. var dom = this.dom;
  5499. if (!dom) {
  5500. this.dom = dom = {};
  5501. // background box
  5502. dom.point = document.createElement('div');
  5503. // className is updated in repaint()
  5504. // contents box, right from the dot
  5505. dom.content = document.createElement('div');
  5506. dom.content.className = 'content';
  5507. dom.point.appendChild(dom.content);
  5508. // dot at start
  5509. dom.dot = document.createElement('div');
  5510. dom.dot.className = 'dot';
  5511. dom.point.appendChild(dom.dot);
  5512. }
  5513. };
  5514. /**
  5515. * Reposition the item, recalculate its left, top, and width, using the current
  5516. * range and size of the items itemset
  5517. * @override
  5518. */
  5519. ItemPoint.prototype.reposition = function reposition() {
  5520. var dom = this.dom,
  5521. props = this.props;
  5522. if (dom) {
  5523. dom.point.style.top = this.top + 'px';
  5524. dom.point.style.left = this.left + 'px';
  5525. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5526. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5527. dom.dot.style.top = props.dot.top + 'px';
  5528. }
  5529. };
  5530. /**
  5531. * @constructor ItemRange
  5532. * @extends Item
  5533. * @param {ItemSet} parent
  5534. * @param {Object} data Object containing parameters start, end
  5535. * content, className.
  5536. * @param {Object} [options] Options to set initial property values
  5537. * @param {Object} [defaultOptions] default options
  5538. * // TODO: describe available options
  5539. */
  5540. function ItemRange (parent, data, options, defaultOptions) {
  5541. this.props = {
  5542. content: {
  5543. left: 0,
  5544. width: 0
  5545. }
  5546. };
  5547. Item.call(this, parent, data, options, defaultOptions);
  5548. }
  5549. ItemRange.prototype = new Item (null, null);
  5550. /**
  5551. * Select the item
  5552. * @override
  5553. */
  5554. ItemRange.prototype.select = function select() {
  5555. this.selected = true;
  5556. // TODO: select and unselect
  5557. };
  5558. /**
  5559. * Unselect the item
  5560. * @override
  5561. */
  5562. ItemRange.prototype.unselect = function unselect() {
  5563. this.selected = false;
  5564. // TODO: select and unselect
  5565. };
  5566. /**
  5567. * Repaint the item
  5568. * @return {Boolean} changed
  5569. */
  5570. ItemRange.prototype.repaint = function repaint() {
  5571. // TODO: make an efficient repaint
  5572. var changed = false;
  5573. var dom = this.dom;
  5574. if (!dom) {
  5575. this._create();
  5576. dom = this.dom;
  5577. changed = true;
  5578. }
  5579. if (dom) {
  5580. if (!this.parent) {
  5581. throw new Error('Cannot repaint item: no parent attached');
  5582. }
  5583. var foreground = this.parent.getForeground();
  5584. if (!foreground) {
  5585. throw new Error('Cannot repaint time axis: ' +
  5586. 'parent has no foreground container element');
  5587. }
  5588. if (!dom.box.parentNode) {
  5589. foreground.appendChild(dom.box);
  5590. changed = true;
  5591. }
  5592. // update content
  5593. if (this.data.content != this.content) {
  5594. this.content = this.data.content;
  5595. if (this.content instanceof Element) {
  5596. dom.content.innerHTML = '';
  5597. dom.content.appendChild(this.content);
  5598. }
  5599. else if (this.data.content != undefined) {
  5600. dom.content.innerHTML = this.content;
  5601. }
  5602. else {
  5603. throw new Error('Property "content" missing in item ' + this.data.id);
  5604. }
  5605. changed = true;
  5606. }
  5607. // update class
  5608. var className = this.data.className ? (' ' + this.data.className) : '';
  5609. if (this.className != className) {
  5610. this.className = className;
  5611. dom.box.className = 'item range' + className;
  5612. changed = true;
  5613. }
  5614. }
  5615. return changed;
  5616. };
  5617. /**
  5618. * Show the item in the DOM (when not already visible). The items DOM will
  5619. * be created when needed.
  5620. * @return {Boolean} changed
  5621. */
  5622. ItemRange.prototype.show = function show() {
  5623. if (!this.dom || !this.dom.box.parentNode) {
  5624. return this.repaint();
  5625. }
  5626. else {
  5627. return false;
  5628. }
  5629. };
  5630. /**
  5631. * Hide the item from the DOM (when visible)
  5632. * @return {Boolean} changed
  5633. */
  5634. ItemRange.prototype.hide = function hide() {
  5635. var changed = false,
  5636. dom = this.dom;
  5637. if (dom) {
  5638. if (dom.box.parentNode) {
  5639. dom.box.parentNode.removeChild(dom.box);
  5640. changed = true;
  5641. }
  5642. }
  5643. return changed;
  5644. };
  5645. /**
  5646. * Reflow the item: calculate its actual size from the DOM
  5647. * @return {boolean} resized returns true if the axis is resized
  5648. * @override
  5649. */
  5650. ItemRange.prototype.reflow = function reflow() {
  5651. var changed = 0,
  5652. dom,
  5653. props,
  5654. options,
  5655. margin,
  5656. padding,
  5657. parent,
  5658. start,
  5659. end,
  5660. data,
  5661. range,
  5662. update,
  5663. box,
  5664. parentWidth,
  5665. contentLeft,
  5666. orientation,
  5667. top;
  5668. if (this.data.start == undefined) {
  5669. throw new Error('Property "start" missing in item ' + this.data.id);
  5670. }
  5671. if (this.data.end == undefined) {
  5672. throw new Error('Property "end" missing in item ' + this.data.id);
  5673. }
  5674. data = this.data;
  5675. range = this.parent && this.parent.range;
  5676. if (data && range) {
  5677. // TODO: account for the width of the item. Take some margin
  5678. this.visible = (data.start < range.end) && (data.end > range.start);
  5679. }
  5680. else {
  5681. this.visible = false;
  5682. }
  5683. if (this.visible) {
  5684. dom = this.dom;
  5685. if (dom) {
  5686. props = this.props;
  5687. options = this.options;
  5688. parent = this.parent;
  5689. start = parent.toScreen(this.data.start);
  5690. end = parent.toScreen(this.data.end);
  5691. update = util.updateProperty;
  5692. box = dom.box;
  5693. parentWidth = parent.width;
  5694. orientation = options.orientation || this.defaultOptions.orientation;
  5695. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5696. padding = options.padding || this.defaultOptions.padding;
  5697. changed += update(props.content, 'width', dom.content.offsetWidth);
  5698. changed += update(this, 'height', box.offsetHeight);
  5699. // limit the width of the this, as browsers cannot draw very wide divs
  5700. if (start < -parentWidth) {
  5701. start = -parentWidth;
  5702. }
  5703. if (end > 2 * parentWidth) {
  5704. end = 2 * parentWidth;
  5705. }
  5706. // when range exceeds left of the window, position the contents at the left of the visible area
  5707. if (start < 0) {
  5708. contentLeft = Math.min(-start,
  5709. (end - start - props.content.width - 2 * padding));
  5710. // TODO: remove the need for options.padding. it's terrible.
  5711. }
  5712. else {
  5713. contentLeft = 0;
  5714. }
  5715. changed += update(props.content, 'left', contentLeft);
  5716. if (orientation == 'top') {
  5717. top = margin;
  5718. changed += update(this, 'top', top);
  5719. }
  5720. else {
  5721. // default or 'bottom'
  5722. top = parent.height - this.height - margin;
  5723. changed += update(this, 'top', top);
  5724. }
  5725. changed += update(this, 'left', start);
  5726. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5727. }
  5728. else {
  5729. changed += 1;
  5730. }
  5731. }
  5732. return (changed > 0);
  5733. };
  5734. /**
  5735. * Create an items DOM
  5736. * @private
  5737. */
  5738. ItemRange.prototype._create = function _create() {
  5739. var dom = this.dom;
  5740. if (!dom) {
  5741. this.dom = dom = {};
  5742. // background box
  5743. dom.box = document.createElement('div');
  5744. // className is updated in repaint()
  5745. // contents box
  5746. dom.content = document.createElement('div');
  5747. dom.content.className = 'content';
  5748. dom.box.appendChild(dom.content);
  5749. }
  5750. };
  5751. /**
  5752. * Reposition the item, recalculate its left, top, and width, using the current
  5753. * range and size of the items itemset
  5754. * @override
  5755. */
  5756. ItemRange.prototype.reposition = function reposition() {
  5757. var dom = this.dom,
  5758. props = this.props;
  5759. if (dom) {
  5760. dom.box.style.top = this.top + 'px';
  5761. dom.box.style.left = this.left + 'px';
  5762. dom.box.style.width = this.width + 'px';
  5763. dom.content.style.left = props.content.left + 'px';
  5764. }
  5765. };
  5766. /**
  5767. * @constructor ItemRangeOverflow
  5768. * @extends ItemRange
  5769. * @param {ItemSet} parent
  5770. * @param {Object} data Object containing parameters start, end
  5771. * content, className.
  5772. * @param {Object} [options] Options to set initial property values
  5773. * @param {Object} [defaultOptions] default options
  5774. * // TODO: describe available options
  5775. */
  5776. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5777. this.props = {
  5778. content: {
  5779. left: 0,
  5780. width: 0
  5781. }
  5782. };
  5783. ItemRange.call(this, parent, data, options, defaultOptions);
  5784. }
  5785. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5786. /**
  5787. * Repaint the item
  5788. * @return {Boolean} changed
  5789. */
  5790. ItemRangeOverflow.prototype.repaint = function repaint() {
  5791. // TODO: make an efficient repaint
  5792. var changed = false;
  5793. var dom = this.dom;
  5794. if (!dom) {
  5795. this._create();
  5796. dom = this.dom;
  5797. changed = true;
  5798. }
  5799. if (dom) {
  5800. if (!this.parent) {
  5801. throw new Error('Cannot repaint item: no parent attached');
  5802. }
  5803. var foreground = this.parent.getForeground();
  5804. if (!foreground) {
  5805. throw new Error('Cannot repaint time axis: ' +
  5806. 'parent has no foreground container element');
  5807. }
  5808. if (!dom.box.parentNode) {
  5809. foreground.appendChild(dom.box);
  5810. changed = true;
  5811. }
  5812. // update content
  5813. if (this.data.content != this.content) {
  5814. this.content = this.data.content;
  5815. if (this.content instanceof Element) {
  5816. dom.content.innerHTML = '';
  5817. dom.content.appendChild(this.content);
  5818. }
  5819. else if (this.data.content != undefined) {
  5820. dom.content.innerHTML = this.content;
  5821. }
  5822. else {
  5823. throw new Error('Property "content" missing in item ' + this.data.id);
  5824. }
  5825. changed = true;
  5826. }
  5827. // update class
  5828. var className = this.data.className ? (' ' + this.data.className) : '';
  5829. if (this.className != className) {
  5830. this.className = className;
  5831. dom.box.className = 'item rangeoverflow' + className;
  5832. changed = true;
  5833. }
  5834. }
  5835. return changed;
  5836. };
  5837. /**
  5838. * Return the items width
  5839. * @return {Number} width
  5840. */
  5841. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  5842. if (this.props.content !== undefined && this.width < this.props.content.width)
  5843. return this.props.content.width;
  5844. else
  5845. return this.width;
  5846. };
  5847. /**
  5848. * @constructor Group
  5849. * @param {GroupSet} parent
  5850. * @param {Number | String} groupId
  5851. * @param {Object} [options] Options to set initial property values
  5852. * // TODO: describe available options
  5853. * @extends Component
  5854. */
  5855. function Group (parent, groupId, options) {
  5856. this.id = util.randomUUID();
  5857. this.parent = parent;
  5858. this.groupId = groupId;
  5859. this.itemset = null; // ItemSet
  5860. this.options = options || {};
  5861. this.options.top = 0;
  5862. this.props = {
  5863. label: {
  5864. width: 0,
  5865. height: 0
  5866. }
  5867. };
  5868. this.top = 0;
  5869. this.left = 0;
  5870. this.width = 0;
  5871. this.height = 0;
  5872. }
  5873. Group.prototype = new Component();
  5874. // TODO: comment
  5875. Group.prototype.setOptions = Component.prototype.setOptions;
  5876. /**
  5877. * Get the container element of the panel, which can be used by a child to
  5878. * add its own widgets.
  5879. * @returns {HTMLElement} container
  5880. */
  5881. Group.prototype.getContainer = function () {
  5882. return this.parent.getContainer();
  5883. };
  5884. /**
  5885. * Set item set for the group. The group will create a view on the itemset,
  5886. * filtered by the groups id.
  5887. * @param {DataSet | DataView} items
  5888. */
  5889. Group.prototype.setItems = function setItems(items) {
  5890. if (this.itemset) {
  5891. // remove current item set
  5892. this.itemset.hide();
  5893. this.itemset.setItems();
  5894. this.parent.controller.remove(this.itemset);
  5895. this.itemset = null;
  5896. }
  5897. if (items) {
  5898. var groupId = this.groupId;
  5899. var itemsetOptions = Object.create(this.options);
  5900. this.itemset = new ItemSet(this, null, itemsetOptions);
  5901. this.itemset.setRange(this.parent.range);
  5902. this.view = new DataView(items, {
  5903. filter: function (item) {
  5904. return item.group == groupId;
  5905. }
  5906. });
  5907. this.itemset.setItems(this.view);
  5908. this.parent.controller.add(this.itemset);
  5909. }
  5910. };
  5911. /**
  5912. * Repaint the item
  5913. * @return {Boolean} changed
  5914. */
  5915. Group.prototype.repaint = function repaint() {
  5916. return false;
  5917. };
  5918. /**
  5919. * Reflow the item
  5920. * @return {Boolean} resized
  5921. */
  5922. Group.prototype.reflow = function reflow() {
  5923. var changed = 0,
  5924. update = util.updateProperty;
  5925. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5926. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5927. // TODO: reckon with the height of the group label
  5928. if (this.label) {
  5929. var inner = this.label.firstChild;
  5930. changed += update(this.props.label, 'width', inner.clientWidth);
  5931. changed += update(this.props.label, 'height', inner.clientHeight);
  5932. }
  5933. else {
  5934. changed += update(this.props.label, 'width', 0);
  5935. changed += update(this.props.label, 'height', 0);
  5936. }
  5937. return (changed > 0);
  5938. };
  5939. /**
  5940. * An GroupSet holds a set of groups
  5941. * @param {Component} parent
  5942. * @param {Component[]} [depends] Components on which this components depends
  5943. * (except for the parent)
  5944. * @param {Object} [options] See GroupSet.setOptions for the available
  5945. * options.
  5946. * @constructor GroupSet
  5947. * @extends Panel
  5948. */
  5949. function GroupSet(parent, depends, options) {
  5950. this.id = util.randomUUID();
  5951. this.parent = parent;
  5952. this.depends = depends;
  5953. this.options = options || {};
  5954. this.range = null; // Range or Object {start: number, end: number}
  5955. this.itemsData = null; // DataSet with items
  5956. this.groupsData = null; // DataSet with groups
  5957. this.groups = {}; // map with groups
  5958. this.dom = {};
  5959. this.props = {
  5960. labels: {
  5961. width: 0
  5962. }
  5963. };
  5964. // TODO: implement right orientation of the labels
  5965. // changes in groups are queued key/value map containing id/action
  5966. this.queue = {};
  5967. var me = this;
  5968. this.listeners = {
  5969. 'add': function (event, params) {
  5970. me._onAdd(params.items);
  5971. },
  5972. 'update': function (event, params) {
  5973. me._onUpdate(params.items);
  5974. },
  5975. 'remove': function (event, params) {
  5976. me._onRemove(params.items);
  5977. }
  5978. };
  5979. }
  5980. GroupSet.prototype = new Panel();
  5981. /**
  5982. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5983. * @param {Object} [options] The following options are available:
  5984. * {String | function} groupsOrder
  5985. * TODO: describe options
  5986. */
  5987. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5988. GroupSet.prototype.setRange = function (range) {
  5989. // TODO: implement setRange
  5990. };
  5991. /**
  5992. * Set items
  5993. * @param {vis.DataSet | null} items
  5994. */
  5995. GroupSet.prototype.setItems = function setItems(items) {
  5996. this.itemsData = items;
  5997. for (var id in this.groups) {
  5998. if (this.groups.hasOwnProperty(id)) {
  5999. var group = this.groups[id];
  6000. group.setItems(items);
  6001. }
  6002. }
  6003. };
  6004. /**
  6005. * Get items
  6006. * @return {vis.DataSet | null} items
  6007. */
  6008. GroupSet.prototype.getItems = function getItems() {
  6009. return this.itemsData;
  6010. };
  6011. /**
  6012. * Set range (start and end).
  6013. * @param {Range | Object} range A Range or an object containing start and end.
  6014. */
  6015. GroupSet.prototype.setRange = function setRange(range) {
  6016. this.range = range;
  6017. };
  6018. /**
  6019. * Set groups
  6020. * @param {vis.DataSet} groups
  6021. */
  6022. GroupSet.prototype.setGroups = function setGroups(groups) {
  6023. var me = this,
  6024. ids;
  6025. // unsubscribe from current dataset
  6026. if (this.groupsData) {
  6027. util.forEach(this.listeners, function (callback, event) {
  6028. me.groupsData.unsubscribe(event, callback);
  6029. });
  6030. // remove all drawn groups
  6031. ids = this.groupsData.getIds();
  6032. this._onRemove(ids);
  6033. }
  6034. // replace the dataset
  6035. if (!groups) {
  6036. this.groupsData = null;
  6037. }
  6038. else if (groups instanceof DataSet) {
  6039. this.groupsData = groups;
  6040. }
  6041. else {
  6042. this.groupsData = new DataSet({
  6043. convert: {
  6044. start: 'Date',
  6045. end: 'Date'
  6046. }
  6047. });
  6048. this.groupsData.add(groups);
  6049. }
  6050. if (this.groupsData) {
  6051. // subscribe to new dataset
  6052. var id = this.id;
  6053. util.forEach(this.listeners, function (callback, event) {
  6054. me.groupsData.subscribe(event, callback, id);
  6055. });
  6056. // draw all new groups
  6057. ids = this.groupsData.getIds();
  6058. this._onAdd(ids);
  6059. }
  6060. };
  6061. /**
  6062. * Get groups
  6063. * @return {vis.DataSet | null} groups
  6064. */
  6065. GroupSet.prototype.getGroups = function getGroups() {
  6066. return this.groupsData;
  6067. };
  6068. /**
  6069. * Repaint the component
  6070. * @return {Boolean} changed
  6071. */
  6072. GroupSet.prototype.repaint = function repaint() {
  6073. var changed = 0,
  6074. i, id, group, label,
  6075. update = util.updateProperty,
  6076. asSize = util.option.asSize,
  6077. asElement = util.option.asElement,
  6078. options = this.options,
  6079. frame = this.dom.frame,
  6080. labels = this.dom.labels,
  6081. labelSet = this.dom.labelSet;
  6082. // create frame
  6083. if (!this.parent) {
  6084. throw new Error('Cannot repaint groupset: no parent attached');
  6085. }
  6086. var parentContainer = this.parent.getContainer();
  6087. if (!parentContainer) {
  6088. throw new Error('Cannot repaint groupset: parent has no container element');
  6089. }
  6090. if (!frame) {
  6091. frame = document.createElement('div');
  6092. frame.className = 'groupset';
  6093. this.dom.frame = frame;
  6094. var className = options.className;
  6095. if (className) {
  6096. util.addClassName(frame, util.option.asString(className));
  6097. }
  6098. changed += 1;
  6099. }
  6100. if (!frame.parentNode) {
  6101. parentContainer.appendChild(frame);
  6102. changed += 1;
  6103. }
  6104. // create labels
  6105. var labelContainer = asElement(options.labelContainer);
  6106. if (!labelContainer) {
  6107. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6108. }
  6109. if (!labels) {
  6110. labels = document.createElement('div');
  6111. labels.className = 'labels';
  6112. this.dom.labels = labels;
  6113. }
  6114. if (!labelSet) {
  6115. labelSet = document.createElement('div');
  6116. labelSet.className = 'label-set';
  6117. labels.appendChild(labelSet);
  6118. this.dom.labelSet = labelSet;
  6119. }
  6120. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6121. if (labels.parentNode) {
  6122. labels.parentNode.removeChild(labels.parentNode);
  6123. }
  6124. labelContainer.appendChild(labels);
  6125. }
  6126. // reposition frame
  6127. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6128. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6129. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6130. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6131. // reposition labels
  6132. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6133. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6134. var me = this,
  6135. queue = this.queue,
  6136. groups = this.groups,
  6137. groupsData = this.groupsData;
  6138. // show/hide added/changed/removed groups
  6139. var ids = Object.keys(queue);
  6140. if (ids.length) {
  6141. ids.forEach(function (id) {
  6142. var action = queue[id];
  6143. var group = groups[id];
  6144. //noinspection FallthroughInSwitchStatementJS
  6145. switch (action) {
  6146. case 'add':
  6147. case 'update':
  6148. if (!group) {
  6149. var groupOptions = Object.create(me.options);
  6150. util.extend(groupOptions, {
  6151. height: null,
  6152. maxHeight: null
  6153. });
  6154. group = new Group(me, id, groupOptions);
  6155. group.setItems(me.itemsData); // attach items data
  6156. groups[id] = group;
  6157. me.controller.add(group);
  6158. }
  6159. // TODO: update group data
  6160. group.data = groupsData.get(id);
  6161. delete queue[id];
  6162. break;
  6163. case 'remove':
  6164. if (group) {
  6165. group.setItems(); // detach items data
  6166. delete groups[id];
  6167. me.controller.remove(group);
  6168. }
  6169. // update lists
  6170. delete queue[id];
  6171. break;
  6172. default:
  6173. console.log('Error: unknown action "' + action + '"');
  6174. }
  6175. });
  6176. // the groupset depends on each of the groups
  6177. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6178. // TODO: apply dependencies of the groupset
  6179. // update the top positions of the groups in the correct order
  6180. var orderedGroups = this.groupsData.getIds({
  6181. order: this.options.groupOrder
  6182. });
  6183. for (i = 0; i < orderedGroups.length; i++) {
  6184. (function (group, prevGroup) {
  6185. var top = 0;
  6186. if (prevGroup) {
  6187. top = function () {
  6188. // TODO: top must reckon with options.maxHeight
  6189. return prevGroup.top + prevGroup.height;
  6190. }
  6191. }
  6192. group.setOptions({
  6193. top: top
  6194. });
  6195. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6196. }
  6197. // (re)create the labels
  6198. while (labelSet.firstChild) {
  6199. labelSet.removeChild(labelSet.firstChild);
  6200. }
  6201. for (i = 0; i < orderedGroups.length; i++) {
  6202. id = orderedGroups[i];
  6203. label = this._createLabel(id);
  6204. labelSet.appendChild(label);
  6205. }
  6206. changed++;
  6207. }
  6208. // reposition the labels
  6209. // TODO: labels are not displayed correctly when orientation=='top'
  6210. // TODO: width of labelPanel is not immediately updated on a change in groups
  6211. for (id in groups) {
  6212. if (groups.hasOwnProperty(id)) {
  6213. group = groups[id];
  6214. label = group.label;
  6215. if (label) {
  6216. label.style.top = group.top + 'px';
  6217. label.style.height = group.height + 'px';
  6218. }
  6219. }
  6220. }
  6221. return (changed > 0);
  6222. };
  6223. /**
  6224. * Create a label for group with given id
  6225. * @param {Number} id
  6226. * @return {Element} label
  6227. * @private
  6228. */
  6229. GroupSet.prototype._createLabel = function(id) {
  6230. var group = this.groups[id];
  6231. var label = document.createElement('div');
  6232. label.className = 'label';
  6233. var inner = document.createElement('div');
  6234. inner.className = 'inner';
  6235. label.appendChild(inner);
  6236. var content = group.data && group.data.content;
  6237. if (content instanceof Element) {
  6238. inner.appendChild(content);
  6239. }
  6240. else if (content != undefined) {
  6241. inner.innerHTML = content;
  6242. }
  6243. var className = group.data && group.data.className;
  6244. if (className) {
  6245. util.addClassName(label, className);
  6246. }
  6247. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6248. return label;
  6249. };
  6250. /**
  6251. * Get container element
  6252. * @return {HTMLElement} container
  6253. */
  6254. GroupSet.prototype.getContainer = function getContainer() {
  6255. return this.dom.frame;
  6256. };
  6257. /**
  6258. * Get the width of the group labels
  6259. * @return {Number} width
  6260. */
  6261. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6262. return this.props.labels.width;
  6263. };
  6264. /**
  6265. * Reflow the component
  6266. * @return {Boolean} resized
  6267. */
  6268. GroupSet.prototype.reflow = function reflow() {
  6269. var changed = 0,
  6270. id, group,
  6271. options = this.options,
  6272. update = util.updateProperty,
  6273. asNumber = util.option.asNumber,
  6274. asSize = util.option.asSize,
  6275. frame = this.dom.frame;
  6276. if (frame) {
  6277. var maxHeight = asNumber(options.maxHeight);
  6278. var fixedHeight = (asSize(options.height) != null);
  6279. var height;
  6280. if (fixedHeight) {
  6281. height = frame.offsetHeight;
  6282. }
  6283. else {
  6284. // height is not specified, calculate the sum of the height of all groups
  6285. height = 0;
  6286. for (id in this.groups) {
  6287. if (this.groups.hasOwnProperty(id)) {
  6288. group = this.groups[id];
  6289. height += group.height;
  6290. }
  6291. }
  6292. }
  6293. if (maxHeight != null) {
  6294. height = Math.min(height, maxHeight);
  6295. }
  6296. changed += update(this, 'height', height);
  6297. changed += update(this, 'top', frame.offsetTop);
  6298. changed += update(this, 'left', frame.offsetLeft);
  6299. changed += update(this, 'width', frame.offsetWidth);
  6300. }
  6301. // calculate the maximum width of the labels
  6302. var width = 0;
  6303. for (id in this.groups) {
  6304. if (this.groups.hasOwnProperty(id)) {
  6305. group = this.groups[id];
  6306. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  6307. width = Math.max(width, labelWidth);
  6308. }
  6309. }
  6310. changed += update(this.props.labels, 'width', width);
  6311. return (changed > 0);
  6312. };
  6313. /**
  6314. * Hide the component from the DOM
  6315. * @return {Boolean} changed
  6316. */
  6317. GroupSet.prototype.hide = function hide() {
  6318. if (this.dom.frame && this.dom.frame.parentNode) {
  6319. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6320. return true;
  6321. }
  6322. else {
  6323. return false;
  6324. }
  6325. };
  6326. /**
  6327. * Show the component in the DOM (when not already visible).
  6328. * A repaint will be executed when the component is not visible
  6329. * @return {Boolean} changed
  6330. */
  6331. GroupSet.prototype.show = function show() {
  6332. if (!this.dom.frame || !this.dom.frame.parentNode) {
  6333. return this.repaint();
  6334. }
  6335. else {
  6336. return false;
  6337. }
  6338. };
  6339. /**
  6340. * Handle updated groups
  6341. * @param {Number[]} ids
  6342. * @private
  6343. */
  6344. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6345. this._toQueue(ids, 'update');
  6346. };
  6347. /**
  6348. * Handle changed groups
  6349. * @param {Number[]} ids
  6350. * @private
  6351. */
  6352. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6353. this._toQueue(ids, 'add');
  6354. };
  6355. /**
  6356. * Handle removed groups
  6357. * @param {Number[]} ids
  6358. * @private
  6359. */
  6360. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6361. this._toQueue(ids, 'remove');
  6362. };
  6363. /**
  6364. * Put groups in the queue to be added/updated/remove
  6365. * @param {Number[]} ids
  6366. * @param {String} action can be 'add', 'update', 'remove'
  6367. */
  6368. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  6369. var queue = this.queue;
  6370. ids.forEach(function (id) {
  6371. queue[id] = action;
  6372. });
  6373. if (this.controller) {
  6374. //this.requestReflow();
  6375. this.requestRepaint();
  6376. }
  6377. };
  6378. /**
  6379. * Create a timeline visualization
  6380. * @param {HTMLElement} container
  6381. * @param {vis.DataSet | Array | DataTable} [items]
  6382. * @param {Object} [options] See Timeline.setOptions for the available options.
  6383. * @constructor
  6384. */
  6385. function Timeline (container, items, options) {
  6386. var me = this;
  6387. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6388. this.options = {
  6389. orientation: 'bottom',
  6390. min: null,
  6391. max: null,
  6392. zoomMin: 10, // milliseconds
  6393. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6394. // moveable: true, // TODO: option moveable
  6395. // zoomable: true, // TODO: option zoomable
  6396. showMinorLabels: true,
  6397. showMajorLabels: true,
  6398. showCurrentTime: false,
  6399. showCustomTime: false,
  6400. autoResize: false
  6401. };
  6402. // controller
  6403. this.controller = new Controller();
  6404. // root panel
  6405. if (!container) {
  6406. throw new Error('No container element provided');
  6407. }
  6408. var rootOptions = Object.create(this.options);
  6409. rootOptions.height = function () {
  6410. // TODO: change to height
  6411. if (me.options.height) {
  6412. // fixed height
  6413. return me.options.height;
  6414. }
  6415. else {
  6416. // auto height
  6417. return (me.timeaxis.height + me.content.height) + 'px';
  6418. }
  6419. };
  6420. this.rootPanel = new RootPanel(container, rootOptions);
  6421. this.controller.add(this.rootPanel);
  6422. // item panel
  6423. var itemOptions = Object.create(this.options);
  6424. itemOptions.left = function () {
  6425. return me.labelPanel.width;
  6426. };
  6427. itemOptions.width = function () {
  6428. return me.rootPanel.width - me.labelPanel.width;
  6429. };
  6430. itemOptions.top = null;
  6431. itemOptions.height = null;
  6432. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6433. this.controller.add(this.itemPanel);
  6434. // label panel
  6435. var labelOptions = Object.create(this.options);
  6436. labelOptions.top = null;
  6437. labelOptions.left = null;
  6438. labelOptions.height = null;
  6439. labelOptions.width = function () {
  6440. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6441. return me.content.getLabelsWidth();
  6442. }
  6443. else {
  6444. return 0;
  6445. }
  6446. };
  6447. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6448. this.controller.add(this.labelPanel);
  6449. // range
  6450. var rangeOptions = Object.create(this.options);
  6451. this.range = new Range(rangeOptions);
  6452. this.range.setRange(
  6453. now.clone().add('days', -3).valueOf(),
  6454. now.clone().add('days', 4).valueOf()
  6455. );
  6456. // TODO: reckon with options moveable and zoomable
  6457. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  6458. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  6459. this.range.on('rangechange', function () {
  6460. var force = true;
  6461. me.controller.requestReflow(force);
  6462. });
  6463. this.range.on('rangechanged', function () {
  6464. var force = true;
  6465. me.controller.requestReflow(force);
  6466. });
  6467. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  6468. // time axis
  6469. var timeaxisOptions = Object.create(rootOptions);
  6470. timeaxisOptions.range = this.range;
  6471. timeaxisOptions.left = null;
  6472. timeaxisOptions.top = null;
  6473. timeaxisOptions.width = '100%';
  6474. timeaxisOptions.height = null;
  6475. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6476. this.timeaxis.setRange(this.range);
  6477. this.controller.add(this.timeaxis);
  6478. // current time bar
  6479. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  6480. this.controller.add(this.currenttime);
  6481. // custom time bar
  6482. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  6483. this.controller.add(this.customtime);
  6484. // create groupset
  6485. this.setGroups(null);
  6486. this.itemsData = null; // DataSet
  6487. this.groupsData = null; // DataSet
  6488. // apply options
  6489. if (options) {
  6490. this.setOptions(options);
  6491. }
  6492. // create itemset and groupset
  6493. if (items) {
  6494. this.setItems(items);
  6495. }
  6496. }
  6497. /**
  6498. * Set options
  6499. * @param {Object} options TODO: describe the available options
  6500. */
  6501. Timeline.prototype.setOptions = function (options) {
  6502. util.extend(this.options, options);
  6503. // force update of range
  6504. // options.start and options.end can be undefined
  6505. //this.range.setRange(options.start, options.end);
  6506. this.range.setRange();
  6507. this.controller.reflow();
  6508. this.controller.repaint();
  6509. };
  6510. /**
  6511. * Set a custom time bar
  6512. * @param {Date} time
  6513. */
  6514. Timeline.prototype.setCustomTime = function (time) {
  6515. this.customtime._setCustomTime(time);
  6516. };
  6517. /**
  6518. * Retrieve the current custom time.
  6519. * @return {Date} customTime
  6520. */
  6521. Timeline.prototype.getCustomTime = function() {
  6522. return new Date(this.customtime.customTime.valueOf());
  6523. };
  6524. /**
  6525. * Set items
  6526. * @param {vis.DataSet | Array | DataTable | null} items
  6527. */
  6528. Timeline.prototype.setItems = function(items) {
  6529. var initialLoad = (this.itemsData == null);
  6530. // convert to type DataSet when needed
  6531. var newItemSet;
  6532. if (!items) {
  6533. newItemSet = null;
  6534. }
  6535. else if (items instanceof DataSet) {
  6536. newItemSet = items;
  6537. }
  6538. if (!(items instanceof DataSet)) {
  6539. newItemSet = new DataSet({
  6540. convert: {
  6541. start: 'Date',
  6542. end: 'Date'
  6543. }
  6544. });
  6545. newItemSet.add(items);
  6546. }
  6547. // set items
  6548. this.itemsData = newItemSet;
  6549. this.content.setItems(newItemSet);
  6550. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6551. // apply the data range as range
  6552. var dataRange = this.getItemRange();
  6553. // add 5% space on both sides
  6554. var min = dataRange.min;
  6555. var max = dataRange.max;
  6556. if (min != null && max != null) {
  6557. var interval = (max.valueOf() - min.valueOf());
  6558. if (interval <= 0) {
  6559. // prevent an empty interval
  6560. interval = 24 * 60 * 60 * 1000; // 1 day
  6561. }
  6562. min = new Date(min.valueOf() - interval * 0.05);
  6563. max = new Date(max.valueOf() + interval * 0.05);
  6564. }
  6565. // override specified start and/or end date
  6566. if (this.options.start != undefined) {
  6567. min = util.convert(this.options.start, 'Date');
  6568. }
  6569. if (this.options.end != undefined) {
  6570. max = util.convert(this.options.end, 'Date');
  6571. }
  6572. // apply range if there is a min or max available
  6573. if (min != null || max != null) {
  6574. this.range.setRange(min, max);
  6575. }
  6576. }
  6577. };
  6578. /**
  6579. * Set groups
  6580. * @param {vis.DataSet | Array | DataTable} groups
  6581. */
  6582. Timeline.prototype.setGroups = function(groups) {
  6583. var me = this;
  6584. this.groupsData = groups;
  6585. // switch content type between ItemSet or GroupSet when needed
  6586. var Type = this.groupsData ? GroupSet : ItemSet;
  6587. if (!(this.content instanceof Type)) {
  6588. // remove old content set
  6589. if (this.content) {
  6590. this.content.hide();
  6591. if (this.content.setItems) {
  6592. this.content.setItems(); // disconnect from items
  6593. }
  6594. if (this.content.setGroups) {
  6595. this.content.setGroups(); // disconnect from groups
  6596. }
  6597. this.controller.remove(this.content);
  6598. }
  6599. // create new content set
  6600. var options = Object.create(this.options);
  6601. util.extend(options, {
  6602. top: function () {
  6603. if (me.options.orientation == 'top') {
  6604. return me.timeaxis.height;
  6605. }
  6606. else {
  6607. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  6608. }
  6609. },
  6610. left: null,
  6611. width: '100%',
  6612. height: function () {
  6613. if (me.options.height) {
  6614. // fixed height
  6615. return me.itemPanel.height - me.timeaxis.height;
  6616. }
  6617. else {
  6618. // auto height
  6619. return null;
  6620. }
  6621. },
  6622. maxHeight: function () {
  6623. // TODO: change maxHeight to be a css string like '100%' or '300px'
  6624. if (me.options.maxHeight) {
  6625. if (!util.isNumber(me.options.maxHeight)) {
  6626. throw new TypeError('Number expected for property maxHeight');
  6627. }
  6628. return me.options.maxHeight - me.timeaxis.height;
  6629. }
  6630. else {
  6631. return null;
  6632. }
  6633. },
  6634. labelContainer: function () {
  6635. return me.labelPanel.getContainer();
  6636. }
  6637. });
  6638. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  6639. if (this.content.setRange) {
  6640. this.content.setRange(this.range);
  6641. }
  6642. if (this.content.setItems) {
  6643. this.content.setItems(this.itemsData);
  6644. }
  6645. if (this.content.setGroups) {
  6646. this.content.setGroups(this.groupsData);
  6647. }
  6648. this.controller.add(this.content);
  6649. }
  6650. };
  6651. /**
  6652. * Get the data range of the item set.
  6653. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6654. * When no minimum is found, min==null
  6655. * When no maximum is found, max==null
  6656. */
  6657. Timeline.prototype.getItemRange = function getItemRange() {
  6658. // calculate min from start filed
  6659. var itemsData = this.itemsData,
  6660. min = null,
  6661. max = null;
  6662. if (itemsData) {
  6663. // calculate the minimum value of the field 'start'
  6664. var minItem = itemsData.min('start');
  6665. min = minItem ? minItem.start.valueOf() : null;
  6666. // calculate maximum value of fields 'start' and 'end'
  6667. var maxStartItem = itemsData.max('start');
  6668. if (maxStartItem) {
  6669. max = maxStartItem.start.valueOf();
  6670. }
  6671. var maxEndItem = itemsData.max('end');
  6672. if (maxEndItem) {
  6673. if (max == null) {
  6674. max = maxEndItem.end.valueOf();
  6675. }
  6676. else {
  6677. max = Math.max(max, maxEndItem.end.valueOf());
  6678. }
  6679. }
  6680. }
  6681. return {
  6682. min: (min != null) ? new Date(min) : null,
  6683. max: (max != null) ? new Date(max) : null
  6684. };
  6685. };
  6686. (function(exports) {
  6687. /**
  6688. * Parse a text source containing data in DOT language into a JSON object.
  6689. * The object contains two lists: one with nodes and one with edges.
  6690. *
  6691. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6692. *
  6693. * @param {String} data Text containing a graph in DOT-notation
  6694. * @return {Object} graph An object containing two parameters:
  6695. * {Object[]} nodes
  6696. * {Object[]} edges
  6697. */
  6698. function parseDOT (data) {
  6699. dot = data;
  6700. return parseGraph();
  6701. }
  6702. // token types enumeration
  6703. var TOKENTYPE = {
  6704. NULL : 0,
  6705. DELIMITER : 1,
  6706. IDENTIFIER: 2,
  6707. UNKNOWN : 3
  6708. };
  6709. // map with all delimiters
  6710. var DELIMITERS = {
  6711. '{': true,
  6712. '}': true,
  6713. '[': true,
  6714. ']': true,
  6715. ';': true,
  6716. '=': true,
  6717. ',': true,
  6718. '->': true,
  6719. '--': true
  6720. };
  6721. var dot = ''; // current dot file
  6722. var index = 0; // current index in dot file
  6723. var c = ''; // current token character in expr
  6724. var token = ''; // current token
  6725. var tokenType = TOKENTYPE.NULL; // type of the token
  6726. /**
  6727. * Get the first character from the dot file.
  6728. * The character is stored into the char c. If the end of the dot file is
  6729. * reached, the function puts an empty string in c.
  6730. */
  6731. function first() {
  6732. index = 0;
  6733. c = dot.charAt(0);
  6734. }
  6735. /**
  6736. * Get the next character from the dot file.
  6737. * The character is stored into the char c. If the end of the dot file is
  6738. * reached, the function puts an empty string in c.
  6739. */
  6740. function next() {
  6741. index++;
  6742. c = dot.charAt(index);
  6743. }
  6744. /**
  6745. * Preview the next character from the dot file.
  6746. * @return {String} cNext
  6747. */
  6748. function nextPreview() {
  6749. return dot.charAt(index + 1);
  6750. }
  6751. /**
  6752. * Test whether given character is alphabetic or numeric
  6753. * @param {String} c
  6754. * @return {Boolean} isAlphaNumeric
  6755. */
  6756. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6757. function isAlphaNumeric(c) {
  6758. return regexAlphaNumeric.test(c);
  6759. }
  6760. /**
  6761. * Merge all properties of object b into object b
  6762. * @param {Object} a
  6763. * @param {Object} b
  6764. * @return {Object} a
  6765. */
  6766. function merge (a, b) {
  6767. if (!a) {
  6768. a = {};
  6769. }
  6770. if (b) {
  6771. for (var name in b) {
  6772. if (b.hasOwnProperty(name)) {
  6773. a[name] = b[name];
  6774. }
  6775. }
  6776. }
  6777. return a;
  6778. }
  6779. /**
  6780. * Set a value in an object, where the provided parameter name can be a
  6781. * path with nested parameters. For example:
  6782. *
  6783. * var obj = {a: 2};
  6784. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6785. *
  6786. * @param {Object} obj
  6787. * @param {String} path A parameter name or dot-separated parameter path,
  6788. * like "color.highlight.border".
  6789. * @param {*} value
  6790. */
  6791. function setValue(obj, path, value) {
  6792. var keys = path.split('.');
  6793. var o = obj;
  6794. while (keys.length) {
  6795. var key = keys.shift();
  6796. if (keys.length) {
  6797. // this isn't the end point
  6798. if (!o[key]) {
  6799. o[key] = {};
  6800. }
  6801. o = o[key];
  6802. }
  6803. else {
  6804. // this is the end point
  6805. o[key] = value;
  6806. }
  6807. }
  6808. }
  6809. /**
  6810. * Add a node to a graph object. If there is already a node with
  6811. * the same id, their attributes will be merged.
  6812. * @param {Object} graph
  6813. * @param {Object} node
  6814. */
  6815. function addNode(graph, node) {
  6816. var i, len;
  6817. var current = null;
  6818. // find root graph (in case of subgraph)
  6819. var graphs = [graph]; // list with all graphs from current graph to root graph
  6820. var root = graph;
  6821. while (root.parent) {
  6822. graphs.push(root.parent);
  6823. root = root.parent;
  6824. }
  6825. // find existing node (at root level) by its id
  6826. if (root.nodes) {
  6827. for (i = 0, len = root.nodes.length; i < len; i++) {
  6828. if (node.id === root.nodes[i].id) {
  6829. current = root.nodes[i];
  6830. break;
  6831. }
  6832. }
  6833. }
  6834. if (!current) {
  6835. // this is a new node
  6836. current = {
  6837. id: node.id
  6838. };
  6839. if (graph.node) {
  6840. // clone default attributes
  6841. current.attr = merge(current.attr, graph.node);
  6842. }
  6843. }
  6844. // add node to this (sub)graph and all its parent graphs
  6845. for (i = graphs.length - 1; i >= 0; i--) {
  6846. var g = graphs[i];
  6847. if (!g.nodes) {
  6848. g.nodes = [];
  6849. }
  6850. if (g.nodes.indexOf(current) == -1) {
  6851. g.nodes.push(current);
  6852. }
  6853. }
  6854. // merge attributes
  6855. if (node.attr) {
  6856. current.attr = merge(current.attr, node.attr);
  6857. }
  6858. }
  6859. /**
  6860. * Add an edge to a graph object
  6861. * @param {Object} graph
  6862. * @param {Object} edge
  6863. */
  6864. function addEdge(graph, edge) {
  6865. if (!graph.edges) {
  6866. graph.edges = [];
  6867. }
  6868. graph.edges.push(edge);
  6869. if (graph.edge) {
  6870. var attr = merge({}, graph.edge); // clone default attributes
  6871. edge.attr = merge(attr, edge.attr); // merge attributes
  6872. }
  6873. }
  6874. /**
  6875. * Create an edge to a graph object
  6876. * @param {Object} graph
  6877. * @param {String | Number | Object} from
  6878. * @param {String | Number | Object} to
  6879. * @param {String} type
  6880. * @param {Object | null} attr
  6881. * @return {Object} edge
  6882. */
  6883. function createEdge(graph, from, to, type, attr) {
  6884. var edge = {
  6885. from: from,
  6886. to: to,
  6887. type: type
  6888. };
  6889. if (graph.edge) {
  6890. edge.attr = merge({}, graph.edge); // clone default attributes
  6891. }
  6892. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6893. return edge;
  6894. }
  6895. /**
  6896. * Get next token in the current dot file.
  6897. * The token and token type are available as token and tokenType
  6898. */
  6899. function getToken() {
  6900. tokenType = TOKENTYPE.NULL;
  6901. token = '';
  6902. // skip over whitespaces
  6903. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6904. next();
  6905. }
  6906. do {
  6907. var isComment = false;
  6908. // skip comment
  6909. if (c == '#') {
  6910. // find the previous non-space character
  6911. var i = index - 1;
  6912. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  6913. i--;
  6914. }
  6915. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  6916. // the # is at the start of a line, this is indeed a line comment
  6917. while (c != '' && c != '\n') {
  6918. next();
  6919. }
  6920. isComment = true;
  6921. }
  6922. }
  6923. if (c == '/' && nextPreview() == '/') {
  6924. // skip line comment
  6925. while (c != '' && c != '\n') {
  6926. next();
  6927. }
  6928. isComment = true;
  6929. }
  6930. if (c == '/' && nextPreview() == '*') {
  6931. // skip block comment
  6932. while (c != '') {
  6933. if (c == '*' && nextPreview() == '/') {
  6934. // end of block comment found. skip these last two characters
  6935. next();
  6936. next();
  6937. break;
  6938. }
  6939. else {
  6940. next();
  6941. }
  6942. }
  6943. isComment = true;
  6944. }
  6945. // skip over whitespaces
  6946. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6947. next();
  6948. }
  6949. }
  6950. while (isComment);
  6951. // check for end of dot file
  6952. if (c == '') {
  6953. // token is still empty
  6954. tokenType = TOKENTYPE.DELIMITER;
  6955. return;
  6956. }
  6957. // check for delimiters consisting of 2 characters
  6958. var c2 = c + nextPreview();
  6959. if (DELIMITERS[c2]) {
  6960. tokenType = TOKENTYPE.DELIMITER;
  6961. token = c2;
  6962. next();
  6963. next();
  6964. return;
  6965. }
  6966. // check for delimiters consisting of 1 character
  6967. if (DELIMITERS[c]) {
  6968. tokenType = TOKENTYPE.DELIMITER;
  6969. token = c;
  6970. next();
  6971. return;
  6972. }
  6973. // check for an identifier (number or string)
  6974. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  6975. if (isAlphaNumeric(c) || c == '-') {
  6976. token += c;
  6977. next();
  6978. while (isAlphaNumeric(c)) {
  6979. token += c;
  6980. next();
  6981. }
  6982. if (token == 'false') {
  6983. token = false; // convert to boolean
  6984. }
  6985. else if (token == 'true') {
  6986. token = true; // convert to boolean
  6987. }
  6988. else if (!isNaN(Number(token))) {
  6989. token = Number(token); // convert to number
  6990. }
  6991. tokenType = TOKENTYPE.IDENTIFIER;
  6992. return;
  6993. }
  6994. // check for a string enclosed by double quotes
  6995. if (c == '"') {
  6996. next();
  6997. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  6998. token += c;
  6999. if (c == '"') { // skip the escape character
  7000. next();
  7001. }
  7002. next();
  7003. }
  7004. if (c != '"') {
  7005. throw newSyntaxError('End of string " expected');
  7006. }
  7007. next();
  7008. tokenType = TOKENTYPE.IDENTIFIER;
  7009. return;
  7010. }
  7011. // something unknown is found, wrong characters, a syntax error
  7012. tokenType = TOKENTYPE.UNKNOWN;
  7013. while (c != '') {
  7014. token += c;
  7015. next();
  7016. }
  7017. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7018. }
  7019. /**
  7020. * Parse a graph.
  7021. * @returns {Object} graph
  7022. */
  7023. function parseGraph() {
  7024. var graph = {};
  7025. first();
  7026. getToken();
  7027. // optional strict keyword
  7028. if (token == 'strict') {
  7029. graph.strict = true;
  7030. getToken();
  7031. }
  7032. // graph or digraph keyword
  7033. if (token == 'graph' || token == 'digraph') {
  7034. graph.type = token;
  7035. getToken();
  7036. }
  7037. // optional graph id
  7038. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7039. graph.id = token;
  7040. getToken();
  7041. }
  7042. // open angle bracket
  7043. if (token != '{') {
  7044. throw newSyntaxError('Angle bracket { expected');
  7045. }
  7046. getToken();
  7047. // statements
  7048. parseStatements(graph);
  7049. // close angle bracket
  7050. if (token != '}') {
  7051. throw newSyntaxError('Angle bracket } expected');
  7052. }
  7053. getToken();
  7054. // end of file
  7055. if (token !== '') {
  7056. throw newSyntaxError('End of file expected');
  7057. }
  7058. getToken();
  7059. // remove temporary default properties
  7060. delete graph.node;
  7061. delete graph.edge;
  7062. delete graph.graph;
  7063. return graph;
  7064. }
  7065. /**
  7066. * Parse a list with statements.
  7067. * @param {Object} graph
  7068. */
  7069. function parseStatements (graph) {
  7070. while (token !== '' && token != '}') {
  7071. parseStatement(graph);
  7072. if (token == ';') {
  7073. getToken();
  7074. }
  7075. }
  7076. }
  7077. /**
  7078. * Parse a single statement. Can be a an attribute statement, node
  7079. * statement, a series of node statements and edge statements, or a
  7080. * parameter.
  7081. * @param {Object} graph
  7082. */
  7083. function parseStatement(graph) {
  7084. // parse subgraph
  7085. var subgraph = parseSubgraph(graph);
  7086. if (subgraph) {
  7087. // edge statements
  7088. parseEdge(graph, subgraph);
  7089. return;
  7090. }
  7091. // parse an attribute statement
  7092. var attr = parseAttributeStatement(graph);
  7093. if (attr) {
  7094. return;
  7095. }
  7096. // parse node
  7097. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7098. throw newSyntaxError('Identifier expected');
  7099. }
  7100. var id = token; // id can be a string or a number
  7101. getToken();
  7102. if (token == '=') {
  7103. // id statement
  7104. getToken();
  7105. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7106. throw newSyntaxError('Identifier expected');
  7107. }
  7108. graph[id] = token;
  7109. getToken();
  7110. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7111. }
  7112. else {
  7113. parseNodeStatement(graph, id);
  7114. }
  7115. }
  7116. /**
  7117. * Parse a subgraph
  7118. * @param {Object} graph parent graph object
  7119. * @return {Object | null} subgraph
  7120. */
  7121. function parseSubgraph (graph) {
  7122. var subgraph = null;
  7123. // optional subgraph keyword
  7124. if (token == 'subgraph') {
  7125. subgraph = {};
  7126. subgraph.type = 'subgraph';
  7127. getToken();
  7128. // optional graph id
  7129. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7130. subgraph.id = token;
  7131. getToken();
  7132. }
  7133. }
  7134. // open angle bracket
  7135. if (token == '{') {
  7136. getToken();
  7137. if (!subgraph) {
  7138. subgraph = {};
  7139. }
  7140. subgraph.parent = graph;
  7141. subgraph.node = graph.node;
  7142. subgraph.edge = graph.edge;
  7143. subgraph.graph = graph.graph;
  7144. // statements
  7145. parseStatements(subgraph);
  7146. // close angle bracket
  7147. if (token != '}') {
  7148. throw newSyntaxError('Angle bracket } expected');
  7149. }
  7150. getToken();
  7151. // remove temporary default properties
  7152. delete subgraph.node;
  7153. delete subgraph.edge;
  7154. delete subgraph.graph;
  7155. delete subgraph.parent;
  7156. // register at the parent graph
  7157. if (!graph.subgraphs) {
  7158. graph.subgraphs = [];
  7159. }
  7160. graph.subgraphs.push(subgraph);
  7161. }
  7162. return subgraph;
  7163. }
  7164. /**
  7165. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7166. * Available keywords are 'node', 'edge', 'graph'.
  7167. * The previous list with default attributes will be replaced
  7168. * @param {Object} graph
  7169. * @returns {String | null} keyword Returns the name of the parsed attribute
  7170. * (node, edge, graph), or null if nothing
  7171. * is parsed.
  7172. */
  7173. function parseAttributeStatement (graph) {
  7174. // attribute statements
  7175. if (token == 'node') {
  7176. getToken();
  7177. // node attributes
  7178. graph.node = parseAttributeList();
  7179. return 'node';
  7180. }
  7181. else if (token == 'edge') {
  7182. getToken();
  7183. // edge attributes
  7184. graph.edge = parseAttributeList();
  7185. return 'edge';
  7186. }
  7187. else if (token == 'graph') {
  7188. getToken();
  7189. // graph attributes
  7190. graph.graph = parseAttributeList();
  7191. return 'graph';
  7192. }
  7193. return null;
  7194. }
  7195. /**
  7196. * parse a node statement
  7197. * @param {Object} graph
  7198. * @param {String | Number} id
  7199. */
  7200. function parseNodeStatement(graph, id) {
  7201. // node statement
  7202. var node = {
  7203. id: id
  7204. };
  7205. var attr = parseAttributeList();
  7206. if (attr) {
  7207. node.attr = attr;
  7208. }
  7209. addNode(graph, node);
  7210. // edge statements
  7211. parseEdge(graph, id);
  7212. }
  7213. /**
  7214. * Parse an edge or a series of edges
  7215. * @param {Object} graph
  7216. * @param {String | Number} from Id of the from node
  7217. */
  7218. function parseEdge(graph, from) {
  7219. while (token == '->' || token == '--') {
  7220. var to;
  7221. var type = token;
  7222. getToken();
  7223. var subgraph = parseSubgraph(graph);
  7224. if (subgraph) {
  7225. to = subgraph;
  7226. }
  7227. else {
  7228. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7229. throw newSyntaxError('Identifier or subgraph expected');
  7230. }
  7231. to = token;
  7232. addNode(graph, {
  7233. id: to
  7234. });
  7235. getToken();
  7236. }
  7237. // parse edge attributes
  7238. var attr = parseAttributeList();
  7239. // create edge
  7240. var edge = createEdge(graph, from, to, type, attr);
  7241. addEdge(graph, edge);
  7242. from = to;
  7243. }
  7244. }
  7245. /**
  7246. * Parse a set with attributes,
  7247. * for example [label="1.000", shape=solid]
  7248. * @return {Object | null} attr
  7249. */
  7250. function parseAttributeList() {
  7251. var attr = null;
  7252. while (token == '[') {
  7253. getToken();
  7254. attr = {};
  7255. while (token !== '' && token != ']') {
  7256. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7257. throw newSyntaxError('Attribute name expected');
  7258. }
  7259. var name = token;
  7260. getToken();
  7261. if (token != '=') {
  7262. throw newSyntaxError('Equal sign = expected');
  7263. }
  7264. getToken();
  7265. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7266. throw newSyntaxError('Attribute value expected');
  7267. }
  7268. var value = token;
  7269. setValue(attr, name, value); // name can be a path
  7270. getToken();
  7271. if (token ==',') {
  7272. getToken();
  7273. }
  7274. }
  7275. if (token != ']') {
  7276. throw newSyntaxError('Bracket ] expected');
  7277. }
  7278. getToken();
  7279. }
  7280. return attr;
  7281. }
  7282. /**
  7283. * Create a syntax error with extra information on current token and index.
  7284. * @param {String} message
  7285. * @returns {SyntaxError} err
  7286. */
  7287. function newSyntaxError(message) {
  7288. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7289. }
  7290. /**
  7291. * Chop off text after a maximum length
  7292. * @param {String} text
  7293. * @param {Number} maxLength
  7294. * @returns {String}
  7295. */
  7296. function chop (text, maxLength) {
  7297. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7298. }
  7299. /**
  7300. * Execute a function fn for each pair of elements in two arrays
  7301. * @param {Array | *} array1
  7302. * @param {Array | *} array2
  7303. * @param {function} fn
  7304. */
  7305. function forEach2(array1, array2, fn) {
  7306. if (array1 instanceof Array) {
  7307. array1.forEach(function (elem1) {
  7308. if (array2 instanceof Array) {
  7309. array2.forEach(function (elem2) {
  7310. fn(elem1, elem2);
  7311. });
  7312. }
  7313. else {
  7314. fn(elem1, array2);
  7315. }
  7316. });
  7317. }
  7318. else {
  7319. if (array2 instanceof Array) {
  7320. array2.forEach(function (elem2) {
  7321. fn(array1, elem2);
  7322. });
  7323. }
  7324. else {
  7325. fn(array1, array2);
  7326. }
  7327. }
  7328. }
  7329. /**
  7330. * Convert a string containing a graph in DOT language into a map containing
  7331. * with nodes and edges in the format of graph.
  7332. * @param {String} data Text containing a graph in DOT-notation
  7333. * @return {Object} graphData
  7334. */
  7335. function DOTToGraph (data) {
  7336. // parse the DOT file
  7337. var dotData = parseDOT(data);
  7338. var graphData = {
  7339. nodes: [],
  7340. edges: [],
  7341. options: {}
  7342. };
  7343. // copy the nodes
  7344. if (dotData.nodes) {
  7345. dotData.nodes.forEach(function (dotNode) {
  7346. var graphNode = {
  7347. id: dotNode.id,
  7348. label: String(dotNode.label || dotNode.id)
  7349. };
  7350. merge(graphNode, dotNode.attr);
  7351. if (graphNode.image) {
  7352. graphNode.shape = 'image';
  7353. }
  7354. graphData.nodes.push(graphNode);
  7355. });
  7356. }
  7357. // copy the edges
  7358. if (dotData.edges) {
  7359. /**
  7360. * Convert an edge in DOT format to an edge with VisGraph format
  7361. * @param {Object} dotEdge
  7362. * @returns {Object} graphEdge
  7363. */
  7364. function convertEdge(dotEdge) {
  7365. var graphEdge = {
  7366. from: dotEdge.from,
  7367. to: dotEdge.to
  7368. };
  7369. merge(graphEdge, dotEdge.attr);
  7370. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7371. return graphEdge;
  7372. }
  7373. dotData.edges.forEach(function (dotEdge) {
  7374. var from, to;
  7375. if (dotEdge.from instanceof Object) {
  7376. from = dotEdge.from.nodes;
  7377. }
  7378. else {
  7379. from = {
  7380. id: dotEdge.from
  7381. }
  7382. }
  7383. if (dotEdge.to instanceof Object) {
  7384. to = dotEdge.to.nodes;
  7385. }
  7386. else {
  7387. to = {
  7388. id: dotEdge.to
  7389. }
  7390. }
  7391. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7392. dotEdge.from.edges.forEach(function (subEdge) {
  7393. var graphEdge = convertEdge(subEdge);
  7394. graphData.edges.push(graphEdge);
  7395. });
  7396. }
  7397. forEach2(from, to, function (from, to) {
  7398. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7399. var graphEdge = convertEdge(subEdge);
  7400. graphData.edges.push(graphEdge);
  7401. });
  7402. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7403. dotEdge.to.edges.forEach(function (subEdge) {
  7404. var graphEdge = convertEdge(subEdge);
  7405. graphData.edges.push(graphEdge);
  7406. });
  7407. }
  7408. });
  7409. }
  7410. // copy the options
  7411. if (dotData.attr) {
  7412. graphData.options = dotData.attr;
  7413. }
  7414. return graphData;
  7415. }
  7416. // exports
  7417. exports.parseDOT = parseDOT;
  7418. exports.DOTToGraph = DOTToGraph;
  7419. })(typeof util !== 'undefined' ? util : exports);
  7420. /**
  7421. * Canvas shapes used by the Graph
  7422. */
  7423. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7424. /**
  7425. * Draw a circle shape
  7426. */
  7427. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7428. this.beginPath();
  7429. this.arc(x, y, r, 0, 2*Math.PI, false);
  7430. };
  7431. /**
  7432. * Draw a square shape
  7433. * @param {Number} x horizontal center
  7434. * @param {Number} y vertical center
  7435. * @param {Number} r size, width and height of the square
  7436. */
  7437. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7438. this.beginPath();
  7439. this.rect(x - r, y - r, r * 2, r * 2);
  7440. };
  7441. /**
  7442. * Draw a triangle shape
  7443. * @param {Number} x horizontal center
  7444. * @param {Number} y vertical center
  7445. * @param {Number} r radius, half the length of the sides of the triangle
  7446. */
  7447. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7448. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7449. this.beginPath();
  7450. var s = r * 2;
  7451. var s2 = s / 2;
  7452. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7453. var h = Math.sqrt(s * s - s2 * s2); // height
  7454. this.moveTo(x, y - (h - ir));
  7455. this.lineTo(x + s2, y + ir);
  7456. this.lineTo(x - s2, y + ir);
  7457. this.lineTo(x, y - (h - ir));
  7458. this.closePath();
  7459. };
  7460. /**
  7461. * Draw a triangle shape in downward orientation
  7462. * @param {Number} x horizontal center
  7463. * @param {Number} y vertical center
  7464. * @param {Number} r radius
  7465. */
  7466. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7467. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7468. this.beginPath();
  7469. var s = r * 2;
  7470. var s2 = s / 2;
  7471. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7472. var h = Math.sqrt(s * s - s2 * s2); // height
  7473. this.moveTo(x, y + (h - ir));
  7474. this.lineTo(x + s2, y - ir);
  7475. this.lineTo(x - s2, y - ir);
  7476. this.lineTo(x, y + (h - ir));
  7477. this.closePath();
  7478. };
  7479. /**
  7480. * Draw a star shape, a star with 5 points
  7481. * @param {Number} x horizontal center
  7482. * @param {Number} y vertical center
  7483. * @param {Number} r radius, half the length of the sides of the triangle
  7484. */
  7485. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7486. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7487. this.beginPath();
  7488. for (var n = 0; n < 10; n++) {
  7489. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7490. this.lineTo(
  7491. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7492. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7493. );
  7494. }
  7495. this.closePath();
  7496. };
  7497. /**
  7498. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7499. */
  7500. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7501. var r2d = Math.PI/180;
  7502. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7503. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7504. this.beginPath();
  7505. this.moveTo(x+r,y);
  7506. this.lineTo(x+w-r,y);
  7507. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7508. this.lineTo(x+w,y+h-r);
  7509. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7510. this.lineTo(x+r,y+h);
  7511. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7512. this.lineTo(x,y+r);
  7513. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7514. };
  7515. /**
  7516. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7517. */
  7518. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7519. var kappa = .5522848,
  7520. ox = (w / 2) * kappa, // control point offset horizontal
  7521. oy = (h / 2) * kappa, // control point offset vertical
  7522. xe = x + w, // x-end
  7523. ye = y + h, // y-end
  7524. xm = x + w / 2, // x-middle
  7525. ym = y + h / 2; // y-middle
  7526. this.beginPath();
  7527. this.moveTo(x, ym);
  7528. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7529. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7530. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7531. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7532. };
  7533. /**
  7534. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7535. */
  7536. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7537. var f = 1/3;
  7538. var wEllipse = w;
  7539. var hEllipse = h * f;
  7540. var kappa = .5522848,
  7541. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7542. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7543. xe = x + wEllipse, // x-end
  7544. ye = y + hEllipse, // y-end
  7545. xm = x + wEllipse / 2, // x-middle
  7546. ym = y + hEllipse / 2, // y-middle
  7547. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7548. yeb = y + h; // y-end, bottom ellipse
  7549. this.beginPath();
  7550. this.moveTo(xe, ym);
  7551. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7552. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7553. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7554. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7555. this.lineTo(xe, ymb);
  7556. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7557. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7558. this.lineTo(x, ym);
  7559. };
  7560. /**
  7561. * Draw an arrow point (no line)
  7562. */
  7563. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7564. // tail
  7565. var xt = x - length * Math.cos(angle);
  7566. var yt = y - length * Math.sin(angle);
  7567. // inner tail
  7568. // TODO: allow to customize different shapes
  7569. var xi = x - length * 0.9 * Math.cos(angle);
  7570. var yi = y - length * 0.9 * Math.sin(angle);
  7571. // left
  7572. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7573. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7574. // right
  7575. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7576. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7577. this.beginPath();
  7578. this.moveTo(x, y);
  7579. this.lineTo(xl, yl);
  7580. this.lineTo(xi, yi);
  7581. this.lineTo(xr, yr);
  7582. this.closePath();
  7583. };
  7584. /**
  7585. * Sets up the dashedLine functionality for drawing
  7586. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7587. * @author David Jordan
  7588. * @date 2012-08-08
  7589. */
  7590. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7591. if (!dashArray) dashArray=[10,5];
  7592. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7593. var dashCount = dashArray.length;
  7594. this.moveTo(x, y);
  7595. var dx = (x2-x), dy = (y2-y);
  7596. var slope = dy/dx;
  7597. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7598. var dashIndex=0, draw=true;
  7599. while (distRemaining>=0.1){
  7600. var dashLength = dashArray[dashIndex++%dashCount];
  7601. if (dashLength > distRemaining) dashLength = distRemaining;
  7602. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7603. if (dx<0) xStep = -xStep;
  7604. x += xStep;
  7605. y += slope*xStep;
  7606. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7607. distRemaining -= dashLength;
  7608. draw = !draw;
  7609. }
  7610. };
  7611. // TODO: add diamond shape
  7612. }
  7613. /**
  7614. * @class Node
  7615. * A node. A node can be connected to other nodes via one or multiple edges.
  7616. * @param {object} properties An object containing properties for the node. All
  7617. * properties are optional, except for the id.
  7618. * {number} id Id of the node. Required
  7619. * {string} label Text label for the node
  7620. * {number} x Horizontal position of the node
  7621. * {number} y Vertical position of the node
  7622. * {string} shape Node shape, available:
  7623. * "database", "circle", "ellipse",
  7624. * "box", "image", "text", "dot",
  7625. * "star", "triangle", "triangleDown",
  7626. * "square"
  7627. * {string} image An image url
  7628. * {string} title An title text, can be HTML
  7629. * {anytype} group A group name or number
  7630. * @param {Graph.Images} imagelist A list with images. Only needed
  7631. * when the node has an image
  7632. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7633. * retrieving group properties
  7634. * @param {Object} constants An object with default values for
  7635. * example for the color
  7636. */
  7637. function Node(properties, imagelist, grouplist, constants) {
  7638. this.selected = false;
  7639. this.edges = []; // all edges connected to this node
  7640. this.group = constants.nodes.group;
  7641. this.fontSize = constants.nodes.fontSize;
  7642. this.fontFace = constants.nodes.fontFace;
  7643. this.fontColor = constants.nodes.fontColor;
  7644. this.color = constants.nodes.color;
  7645. // set defaults for the properties
  7646. this.id = undefined;
  7647. this.shape = constants.nodes.shape;
  7648. this.image = constants.nodes.image;
  7649. this.x = 0;
  7650. this.y = 0;
  7651. this.xFixed = false;
  7652. this.yFixed = false;
  7653. this.radius = constants.nodes.radius;
  7654. this.radiusFixed = false;
  7655. this.radiusMin = constants.nodes.radiusMin;
  7656. this.radiusMax = constants.nodes.radiusMax;
  7657. this.imagelist = imagelist;
  7658. this.grouplist = grouplist;
  7659. this.setProperties(properties, constants);
  7660. // mass, force, velocity
  7661. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  7662. this.fx = 0.0; // external force x
  7663. this.fy = 0.0; // external force y
  7664. this.vx = 0.0; // velocity x
  7665. this.vy = 0.0; // velocity y
  7666. this.minForce = constants.minForce;
  7667. this.damping = 0.9; // damping factor
  7668. };
  7669. /**
  7670. * Attach a edge to the node
  7671. * @param {Edge} edge
  7672. */
  7673. Node.prototype.attachEdge = function(edge) {
  7674. if (this.edges.indexOf(edge) == -1) {
  7675. this.edges.push(edge);
  7676. }
  7677. this._updateMass();
  7678. };
  7679. /**
  7680. * Detach a edge from the node
  7681. * @param {Edge} edge
  7682. */
  7683. Node.prototype.detachEdge = function(edge) {
  7684. var index = this.edges.indexOf(edge);
  7685. if (index != -1) {
  7686. this.edges.splice(index, 1);
  7687. }
  7688. this._updateMass();
  7689. };
  7690. /**
  7691. * Update the nodes mass, which is determined by the number of edges connecting
  7692. * to it (more edges -> heavier node).
  7693. * @private
  7694. */
  7695. Node.prototype._updateMass = function() {
  7696. this.mass = 50 + 20 * this.edges.length; // kg
  7697. };
  7698. /**
  7699. * Set or overwrite properties for the node
  7700. * @param {Object} properties an object with properties
  7701. * @param {Object} constants and object with default, global properties
  7702. */
  7703. Node.prototype.setProperties = function(properties, constants) {
  7704. if (!properties) {
  7705. return;
  7706. }
  7707. // basic properties
  7708. if (properties.id != undefined) {this.id = properties.id;}
  7709. if (properties.label != undefined) {this.label = properties.label;}
  7710. if (properties.title != undefined) {this.title = properties.title;}
  7711. if (properties.group != undefined) {this.group = properties.group;}
  7712. if (properties.x != undefined) {this.x = properties.x;}
  7713. if (properties.y != undefined) {this.y = properties.y;}
  7714. if (properties.value != undefined) {this.value = properties.value;}
  7715. if (this.id === undefined) {
  7716. throw "Node must have an id";
  7717. }
  7718. // copy group properties
  7719. if (this.group) {
  7720. var groupObj = this.grouplist.get(this.group);
  7721. for (var prop in groupObj) {
  7722. if (groupObj.hasOwnProperty(prop)) {
  7723. this[prop] = groupObj[prop];
  7724. }
  7725. }
  7726. }
  7727. // individual shape properties
  7728. if (properties.shape != undefined) {this.shape = properties.shape;}
  7729. if (properties.image != undefined) {this.image = properties.image;}
  7730. if (properties.radius != undefined) {this.radius = properties.radius;}
  7731. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  7732. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7733. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7734. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7735. if (this.image != undefined) {
  7736. if (this.imagelist) {
  7737. this.imageObj = this.imagelist.load(this.image);
  7738. }
  7739. else {
  7740. throw "No imagelist provided";
  7741. }
  7742. }
  7743. this.xFixed = this.xFixed || (properties.x != undefined);
  7744. this.yFixed = this.yFixed || (properties.y != undefined);
  7745. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  7746. if (this.shape == 'image') {
  7747. this.radiusMin = constants.nodes.widthMin;
  7748. this.radiusMax = constants.nodes.widthMax;
  7749. }
  7750. // choose draw method depending on the shape
  7751. switch (this.shape) {
  7752. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7753. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7754. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7755. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7756. // TODO: add diamond shape
  7757. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7758. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7759. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7760. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7761. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7762. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7763. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7764. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7765. }
  7766. // reset the size of the node, this can be changed
  7767. this._reset();
  7768. };
  7769. /**
  7770. * Parse a color property into an object with border, background, and
  7771. * hightlight colors
  7772. * @param {Object | String} color
  7773. * @return {Object} colorObject
  7774. */
  7775. Node.parseColor = function(color) {
  7776. var c;
  7777. if (util.isString(color)) {
  7778. c = {
  7779. border: color,
  7780. background: color,
  7781. highlight: {
  7782. border: color,
  7783. background: color
  7784. }
  7785. };
  7786. // TODO: automatically generate a nice highlight color
  7787. }
  7788. else {
  7789. c = {};
  7790. c.background = color.background || 'white';
  7791. c.border = color.border || c.background;
  7792. if (util.isString(color.highlight)) {
  7793. c.highlight = {
  7794. border: color.highlight,
  7795. background: color.highlight
  7796. }
  7797. }
  7798. else {
  7799. c.highlight = {};
  7800. c.highlight.background = color.highlight && color.highlight.background || c.background;
  7801. c.highlight.border = color.highlight && color.highlight.border || c.border;
  7802. }
  7803. }
  7804. return c;
  7805. };
  7806. /**
  7807. * select this node
  7808. */
  7809. Node.prototype.select = function() {
  7810. this.selected = true;
  7811. this._reset();
  7812. };
  7813. /**
  7814. * unselect this node
  7815. */
  7816. Node.prototype.unselect = function() {
  7817. this.selected = false;
  7818. this._reset();
  7819. };
  7820. /**
  7821. * Reset the calculated size of the node, forces it to recalculate its size
  7822. * @private
  7823. */
  7824. Node.prototype._reset = function() {
  7825. this.width = undefined;
  7826. this.height = undefined;
  7827. };
  7828. /**
  7829. * get the title of this node.
  7830. * @return {string} title The title of the node, or undefined when no title
  7831. * has been set.
  7832. */
  7833. Node.prototype.getTitle = function() {
  7834. return this.title;
  7835. };
  7836. /**
  7837. * Calculate the distance to the border of the Node
  7838. * @param {CanvasRenderingContext2D} ctx
  7839. * @param {Number} angle Angle in radians
  7840. * @returns {number} distance Distance to the border in pixels
  7841. */
  7842. Node.prototype.distanceToBorder = function (ctx, angle) {
  7843. var borderWidth = 1;
  7844. if (!this.width) {
  7845. this.resize(ctx);
  7846. }
  7847. //noinspection FallthroughInSwitchStatementJS
  7848. switch (this.shape) {
  7849. case 'circle':
  7850. case 'dot':
  7851. return this.radius + borderWidth;
  7852. case 'ellipse':
  7853. var a = this.width / 2;
  7854. var b = this.height / 2;
  7855. var w = (Math.sin(angle) * a);
  7856. var h = (Math.cos(angle) * b);
  7857. return a * b / Math.sqrt(w * w + h * h);
  7858. // TODO: implement distanceToBorder for database
  7859. // TODO: implement distanceToBorder for triangle
  7860. // TODO: implement distanceToBorder for triangleDown
  7861. case 'box':
  7862. case 'image':
  7863. case 'text':
  7864. default:
  7865. if (this.width) {
  7866. return Math.min(
  7867. Math.abs(this.width / 2 / Math.cos(angle)),
  7868. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7869. // TODO: reckon with border radius too in case of box
  7870. }
  7871. else {
  7872. return 0;
  7873. }
  7874. }
  7875. // TODO: implement calculation of distance to border for all shapes
  7876. };
  7877. /**
  7878. * Set forces acting on the node
  7879. * @param {number} fx Force in horizontal direction
  7880. * @param {number} fy Force in vertical direction
  7881. */
  7882. Node.prototype._setForce = function(fx, fy) {
  7883. this.fx = fx;
  7884. this.fy = fy;
  7885. };
  7886. /**
  7887. * Add forces acting on the node
  7888. * @param {number} fx Force in horizontal direction
  7889. * @param {number} fy Force in vertical direction
  7890. * @private
  7891. */
  7892. Node.prototype._addForce = function(fx, fy) {
  7893. this.fx += fx;
  7894. this.fy += fy;
  7895. };
  7896. /**
  7897. * Perform one discrete step for the node
  7898. * @param {number} interval Time interval in seconds
  7899. */
  7900. Node.prototype.discreteStep = function(interval) {
  7901. if (!this.xFixed) {
  7902. var dx = -this.damping * this.vx; // damping force
  7903. var ax = (this.fx + dx) / this.mass; // acceleration
  7904. this.vx += ax / interval; // velocity
  7905. this.x += this.vx / interval; // position
  7906. }
  7907. if (!this.yFixed) {
  7908. var dy = -this.damping * this.vy; // damping force
  7909. var ay = (this.fy + dy) / this.mass; // acceleration
  7910. this.vy += ay / interval; // velocity
  7911. this.y += this.vy / interval; // position
  7912. }
  7913. };
  7914. /**
  7915. * Check if this node has a fixed x and y position
  7916. * @return {boolean} true if fixed, false if not
  7917. */
  7918. Node.prototype.isFixed = function() {
  7919. return (this.xFixed && this.yFixed);
  7920. };
  7921. /**
  7922. * Check if this node is moving
  7923. * @param {number} vmin the minimum velocity considered as "moving"
  7924. * @return {boolean} true if moving, false if it has no velocity
  7925. */
  7926. // TODO: replace this method with calculating the kinetic energy
  7927. Node.prototype.isMoving = function(vmin) {
  7928. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  7929. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  7930. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  7931. };
  7932. /**
  7933. * check if this node is selecte
  7934. * @return {boolean} selected True if node is selected, else false
  7935. */
  7936. Node.prototype.isSelected = function() {
  7937. return this.selected;
  7938. };
  7939. /**
  7940. * Retrieve the value of the node. Can be undefined
  7941. * @return {Number} value
  7942. */
  7943. Node.prototype.getValue = function() {
  7944. return this.value;
  7945. };
  7946. /**
  7947. * Calculate the distance from the nodes location to the given location (x,y)
  7948. * @param {Number} x
  7949. * @param {Number} y
  7950. * @return {Number} value
  7951. */
  7952. Node.prototype.getDistance = function(x, y) {
  7953. var dx = this.x - x,
  7954. dy = this.y - y;
  7955. return Math.sqrt(dx * dx + dy * dy);
  7956. };
  7957. /**
  7958. * Adjust the value range of the node. The node will adjust it's radius
  7959. * based on its value.
  7960. * @param {Number} min
  7961. * @param {Number} max
  7962. */
  7963. Node.prototype.setValueRange = function(min, max) {
  7964. if (!this.radiusFixed && this.value !== undefined) {
  7965. if (max == min) {
  7966. this.radius = (this.radiusMin + this.radiusMax) / 2;
  7967. }
  7968. else {
  7969. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  7970. this.radius = (this.value - min) * scale + this.radiusMin;
  7971. }
  7972. }
  7973. };
  7974. /**
  7975. * Draw this node in the given canvas
  7976. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7977. * @param {CanvasRenderingContext2D} ctx
  7978. */
  7979. Node.prototype.draw = function(ctx) {
  7980. throw "Draw method not initialized for node";
  7981. };
  7982. /**
  7983. * Recalculate the size of this node in the given canvas
  7984. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7985. * @param {CanvasRenderingContext2D} ctx
  7986. */
  7987. Node.prototype.resize = function(ctx) {
  7988. throw "Resize method not initialized for node";
  7989. };
  7990. /**
  7991. * Check if this object is overlapping with the provided object
  7992. * @param {Object} obj an object with parameters left, top, right, bottom
  7993. * @return {boolean} True if location is located on node
  7994. */
  7995. Node.prototype.isOverlappingWith = function(obj) {
  7996. return (this.left < obj.right &&
  7997. this.left + this.width > obj.left &&
  7998. this.top < obj.bottom &&
  7999. this.top + this.height > obj.top);
  8000. };
  8001. Node.prototype._resizeImage = function (ctx) {
  8002. // TODO: pre calculate the image size
  8003. if (!this.width) { // undefined or 0
  8004. var width, height;
  8005. if (this.value) {
  8006. var scale = this.imageObj.height / this.imageObj.width;
  8007. width = this.radius || this.imageObj.width;
  8008. height = this.radius * scale || this.imageObj.height;
  8009. }
  8010. else {
  8011. width = this.imageObj.width;
  8012. height = this.imageObj.height;
  8013. }
  8014. this.width = width;
  8015. this.height = height;
  8016. }
  8017. };
  8018. Node.prototype._drawImage = function (ctx) {
  8019. this._resizeImage(ctx);
  8020. this.left = this.x - this.width / 2;
  8021. this.top = this.y - this.height / 2;
  8022. var yLabel;
  8023. if (this.imageObj) {
  8024. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8025. yLabel = this.y + this.height / 2;
  8026. }
  8027. else {
  8028. // image still loading... just draw the label for now
  8029. yLabel = this.y;
  8030. }
  8031. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8032. };
  8033. Node.prototype._resizeBox = function (ctx) {
  8034. if (!this.width) {
  8035. var margin = 5;
  8036. var textSize = this.getTextSize(ctx);
  8037. this.width = textSize.width + 2 * margin;
  8038. this.height = textSize.height + 2 * margin;
  8039. }
  8040. };
  8041. Node.prototype._drawBox = function (ctx) {
  8042. this._resizeBox(ctx);
  8043. this.left = this.x - this.width / 2;
  8044. this.top = this.y - this.height / 2;
  8045. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8046. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8047. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8048. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8049. ctx.fill();
  8050. ctx.stroke();
  8051. this._label(ctx, this.label, this.x, this.y);
  8052. };
  8053. Node.prototype._resizeDatabase = function (ctx) {
  8054. if (!this.width) {
  8055. var margin = 5;
  8056. var textSize = this.getTextSize(ctx);
  8057. var size = textSize.width + 2 * margin;
  8058. this.width = size;
  8059. this.height = size;
  8060. }
  8061. };
  8062. Node.prototype._drawDatabase = function (ctx) {
  8063. this._resizeDatabase(ctx);
  8064. this.left = this.x - this.width / 2;
  8065. this.top = this.y - this.height / 2;
  8066. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8067. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8068. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8069. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8070. ctx.fill();
  8071. ctx.stroke();
  8072. this._label(ctx, this.label, this.x, this.y);
  8073. };
  8074. Node.prototype._resizeCircle = function (ctx) {
  8075. if (!this.width) {
  8076. var margin = 5;
  8077. var textSize = this.getTextSize(ctx);
  8078. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8079. this.radius = diameter / 2;
  8080. this.width = diameter;
  8081. this.height = diameter;
  8082. }
  8083. };
  8084. Node.prototype._drawCircle = function (ctx) {
  8085. this._resizeCircle(ctx);
  8086. this.left = this.x - this.width / 2;
  8087. this.top = this.y - this.height / 2;
  8088. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8089. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8090. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8091. ctx.circle(this.x, this.y, this.radius);
  8092. ctx.fill();
  8093. ctx.stroke();
  8094. this._label(ctx, this.label, this.x, this.y);
  8095. };
  8096. Node.prototype._resizeEllipse = function (ctx) {
  8097. if (!this.width) {
  8098. var textSize = this.getTextSize(ctx);
  8099. this.width = textSize.width * 1.5;
  8100. this.height = textSize.height * 2;
  8101. if (this.width < this.height) {
  8102. this.width = this.height;
  8103. }
  8104. }
  8105. };
  8106. Node.prototype._drawEllipse = function (ctx) {
  8107. this._resizeEllipse(ctx);
  8108. this.left = this.x - this.width / 2;
  8109. this.top = this.y - this.height / 2;
  8110. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8111. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8112. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8113. ctx.ellipse(this.left, this.top, this.width, this.height);
  8114. ctx.fill();
  8115. ctx.stroke();
  8116. this._label(ctx, this.label, this.x, this.y);
  8117. };
  8118. Node.prototype._drawDot = function (ctx) {
  8119. this._drawShape(ctx, 'circle');
  8120. };
  8121. Node.prototype._drawTriangle = function (ctx) {
  8122. this._drawShape(ctx, 'triangle');
  8123. };
  8124. Node.prototype._drawTriangleDown = function (ctx) {
  8125. this._drawShape(ctx, 'triangleDown');
  8126. };
  8127. Node.prototype._drawSquare = function (ctx) {
  8128. this._drawShape(ctx, 'square');
  8129. };
  8130. Node.prototype._drawStar = function (ctx) {
  8131. this._drawShape(ctx, 'star');
  8132. };
  8133. Node.prototype._resizeShape = function (ctx) {
  8134. if (!this.width) {
  8135. var size = 2 * this.radius;
  8136. this.width = size;
  8137. this.height = size;
  8138. }
  8139. };
  8140. Node.prototype._drawShape = function (ctx, shape) {
  8141. this._resizeShape(ctx);
  8142. this.left = this.x - this.width / 2;
  8143. this.top = this.y - this.height / 2;
  8144. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8145. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8146. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8147. ctx[shape](this.x, this.y, this.radius);
  8148. ctx.fill();
  8149. ctx.stroke();
  8150. if (this.label) {
  8151. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8152. }
  8153. };
  8154. Node.prototype._resizeText = function (ctx) {
  8155. if (!this.width) {
  8156. var margin = 5;
  8157. var textSize = this.getTextSize(ctx);
  8158. this.width = textSize.width + 2 * margin;
  8159. this.height = textSize.height + 2 * margin;
  8160. }
  8161. };
  8162. Node.prototype._drawText = function (ctx) {
  8163. this._resizeText(ctx);
  8164. this.left = this.x - this.width / 2;
  8165. this.top = this.y - this.height / 2;
  8166. this._label(ctx, this.label, this.x, this.y);
  8167. };
  8168. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8169. if (text) {
  8170. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8171. ctx.fillStyle = this.fontColor || "black";
  8172. ctx.textAlign = align || "center";
  8173. ctx.textBaseline = baseline || "middle";
  8174. var lines = text.split('\n'),
  8175. lineCount = lines.length,
  8176. fontSize = (this.fontSize + 4),
  8177. yLine = y + (1 - lineCount) / 2 * fontSize;
  8178. for (var i = 0; i < lineCount; i++) {
  8179. ctx.fillText(lines[i], x, yLine);
  8180. yLine += fontSize;
  8181. }
  8182. }
  8183. };
  8184. Node.prototype.getTextSize = function(ctx) {
  8185. if (this.label != undefined) {
  8186. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8187. var lines = this.label.split('\n'),
  8188. height = (this.fontSize + 4) * lines.length,
  8189. width = 0;
  8190. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8191. width = Math.max(width, ctx.measureText(lines[i]).width);
  8192. }
  8193. return {"width": width, "height": height};
  8194. }
  8195. else {
  8196. return {"width": 0, "height": 0};
  8197. }
  8198. };
  8199. /**
  8200. * @class Edge
  8201. *
  8202. * A edge connects two nodes
  8203. * @param {Object} properties Object with properties. Must contain
  8204. * At least properties from and to.
  8205. * Available properties: from (number),
  8206. * to (number), label (string, color (string),
  8207. * width (number), style (string),
  8208. * length (number), title (string)
  8209. * @param {Graph} graph A graph object, used to find and edge to
  8210. * nodes.
  8211. * @param {Object} constants An object with default values for
  8212. * example for the color
  8213. */
  8214. function Edge (properties, graph, constants) {
  8215. if (!graph) {
  8216. throw "No graph provided";
  8217. }
  8218. this.graph = graph;
  8219. // initialize constants
  8220. this.widthMin = constants.edges.widthMin;
  8221. this.widthMax = constants.edges.widthMax;
  8222. // initialize variables
  8223. this.id = undefined;
  8224. this.fromId = undefined;
  8225. this.toId = undefined;
  8226. this.style = constants.edges.style;
  8227. this.title = undefined;
  8228. this.width = constants.edges.width;
  8229. this.value = undefined;
  8230. this.length = constants.edges.length;
  8231. this.from = null; // a node
  8232. this.to = null; // a node
  8233. this.connected = false;
  8234. // Added to support dashed lines
  8235. // David Jordan
  8236. // 2012-08-08
  8237. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8238. this.stiffness = undefined; // depends on the length of the edge
  8239. this.color = constants.edges.color;
  8240. this.widthFixed = false;
  8241. this.lengthFixed = false;
  8242. this.setProperties(properties, constants);
  8243. }
  8244. /**
  8245. * Set or overwrite properties for the edge
  8246. * @param {Object} properties an object with properties
  8247. * @param {Object} constants and object with default, global properties
  8248. */
  8249. Edge.prototype.setProperties = function(properties, constants) {
  8250. if (!properties) {
  8251. return;
  8252. }
  8253. if (properties.from != undefined) {this.fromId = properties.from;}
  8254. if (properties.to != undefined) {this.toId = properties.to;}
  8255. if (properties.id != undefined) {this.id = properties.id;}
  8256. if (properties.style != undefined) {this.style = properties.style;}
  8257. if (properties.label != undefined) {this.label = properties.label;}
  8258. if (this.label) {
  8259. this.fontSize = constants.edges.fontSize;
  8260. this.fontFace = constants.edges.fontFace;
  8261. this.fontColor = constants.edges.fontColor;
  8262. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  8263. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  8264. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  8265. }
  8266. if (properties.title != undefined) {this.title = properties.title;}
  8267. if (properties.width != undefined) {this.width = properties.width;}
  8268. if (properties.value != undefined) {this.value = properties.value;}
  8269. if (properties.length != undefined) {this.length = properties.length;}
  8270. // Added to support dashed lines
  8271. // David Jordan
  8272. // 2012-08-08
  8273. if (properties.dash) {
  8274. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  8275. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  8276. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  8277. }
  8278. if (properties.color != undefined) {this.color = properties.color;}
  8279. // A node is connected when it has a from and to node.
  8280. this.connect();
  8281. this.widthFixed = this.widthFixed || (properties.width != undefined);
  8282. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  8283. this.stiffness = 1 / this.length;
  8284. // set draw method based on style
  8285. switch (this.style) {
  8286. case 'line': this.draw = this._drawLine; break;
  8287. case 'arrow': this.draw = this._drawArrow; break;
  8288. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8289. case 'dash-line': this.draw = this._drawDashLine; break;
  8290. default: this.draw = this._drawLine; break;
  8291. }
  8292. };
  8293. /**
  8294. * Connect an edge to its nodes
  8295. */
  8296. Edge.prototype.connect = function () {
  8297. this.disconnect();
  8298. this.from = this.graph.nodes[this.fromId] || null;
  8299. this.to = this.graph.nodes[this.toId] || null;
  8300. this.connected = (this.from && this.to);
  8301. if (this.connected) {
  8302. this.from.attachEdge(this);
  8303. this.to.attachEdge(this);
  8304. }
  8305. else {
  8306. if (this.from) {
  8307. this.from.detachEdge(this);
  8308. }
  8309. if (this.to) {
  8310. this.to.detachEdge(this);
  8311. }
  8312. }
  8313. };
  8314. /**
  8315. * Disconnect an edge from its nodes
  8316. */
  8317. Edge.prototype.disconnect = function () {
  8318. if (this.from) {
  8319. this.from.detachEdge(this);
  8320. this.from = null;
  8321. }
  8322. if (this.to) {
  8323. this.to.detachEdge(this);
  8324. this.to = null;
  8325. }
  8326. this.connected = false;
  8327. };
  8328. /**
  8329. * get the title of this edge.
  8330. * @return {string} title The title of the edge, or undefined when no title
  8331. * has been set.
  8332. */
  8333. Edge.prototype.getTitle = function() {
  8334. return this.title;
  8335. };
  8336. /**
  8337. * Retrieve the value of the edge. Can be undefined
  8338. * @return {Number} value
  8339. */
  8340. Edge.prototype.getValue = function() {
  8341. return this.value;
  8342. };
  8343. /**
  8344. * Adjust the value range of the edge. The edge will adjust it's width
  8345. * based on its value.
  8346. * @param {Number} min
  8347. * @param {Number} max
  8348. */
  8349. Edge.prototype.setValueRange = function(min, max) {
  8350. if (!this.widthFixed && this.value !== undefined) {
  8351. var scale = (this.widthMax - this.widthMin) / (max - min);
  8352. this.width = (this.value - min) * scale + this.widthMin;
  8353. }
  8354. };
  8355. /**
  8356. * Redraw a edge
  8357. * Draw this edge in the given canvas
  8358. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8359. * @param {CanvasRenderingContext2D} ctx
  8360. */
  8361. Edge.prototype.draw = function(ctx) {
  8362. throw "Method draw not initialized in edge";
  8363. };
  8364. /**
  8365. * Check if this object is overlapping with the provided object
  8366. * @param {Object} obj an object with parameters left, top
  8367. * @return {boolean} True if location is located on the edge
  8368. */
  8369. Edge.prototype.isOverlappingWith = function(obj) {
  8370. var distMax = 10;
  8371. var xFrom = this.from.x;
  8372. var yFrom = this.from.y;
  8373. var xTo = this.to.x;
  8374. var yTo = this.to.y;
  8375. var xObj = obj.left;
  8376. var yObj = obj.top;
  8377. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8378. return (dist < distMax);
  8379. };
  8380. /**
  8381. * Redraw a edge as a line
  8382. * Draw this edge in the given canvas
  8383. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8384. * @param {CanvasRenderingContext2D} ctx
  8385. * @private
  8386. */
  8387. Edge.prototype._drawLine = function(ctx) {
  8388. // set style
  8389. ctx.strokeStyle = this.color;
  8390. ctx.lineWidth = this._getLineWidth();
  8391. var point;
  8392. if (this.from != this.to) {
  8393. // draw line
  8394. this._line(ctx);
  8395. // draw label
  8396. if (this.label) {
  8397. point = this._pointOnLine(0.5);
  8398. this._label(ctx, this.label, point.x, point.y);
  8399. }
  8400. }
  8401. else {
  8402. var x, y;
  8403. var radius = this.length / 4;
  8404. var node = this.from;
  8405. if (!node.width) {
  8406. node.resize(ctx);
  8407. }
  8408. if (node.width > node.height) {
  8409. x = node.x + node.width / 2;
  8410. y = node.y - radius;
  8411. }
  8412. else {
  8413. x = node.x + radius;
  8414. y = node.y - node.height / 2;
  8415. }
  8416. this._circle(ctx, x, y, radius);
  8417. point = this._pointOnCircle(x, y, radius, 0.5);
  8418. this._label(ctx, this.label, point.x, point.y);
  8419. }
  8420. };
  8421. /**
  8422. * Get the line width of the edge. Depends on width and whether one of the
  8423. * connected nodes is selected.
  8424. * @return {Number} width
  8425. * @private
  8426. */
  8427. Edge.prototype._getLineWidth = function() {
  8428. if (this.from.selected || this.to.selected) {
  8429. return Math.min(this.width * 2, this.widthMax);
  8430. }
  8431. else {
  8432. return this.width;
  8433. }
  8434. };
  8435. /**
  8436. * Draw a line between two nodes
  8437. * @param {CanvasRenderingContext2D} ctx
  8438. * @private
  8439. */
  8440. Edge.prototype._line = function (ctx) {
  8441. // draw a straight line
  8442. ctx.beginPath();
  8443. ctx.moveTo(this.from.x, this.from.y);
  8444. ctx.lineTo(this.to.x, this.to.y);
  8445. ctx.stroke();
  8446. };
  8447. /**
  8448. * Draw a line from a node to itself, a circle
  8449. * @param {CanvasRenderingContext2D} ctx
  8450. * @param {Number} x
  8451. * @param {Number} y
  8452. * @param {Number} radius
  8453. * @private
  8454. */
  8455. Edge.prototype._circle = function (ctx, x, y, radius) {
  8456. // draw a circle
  8457. ctx.beginPath();
  8458. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8459. ctx.stroke();
  8460. };
  8461. /**
  8462. * Draw label with white background and with the middle at (x, y)
  8463. * @param {CanvasRenderingContext2D} ctx
  8464. * @param {String} text
  8465. * @param {Number} x
  8466. * @param {Number} y
  8467. * @private
  8468. */
  8469. Edge.prototype._label = function (ctx, text, x, y) {
  8470. if (text) {
  8471. // TODO: cache the calculated size
  8472. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8473. this.fontSize + "px " + this.fontFace;
  8474. ctx.fillStyle = 'white';
  8475. var width = ctx.measureText(text).width;
  8476. var height = this.fontSize;
  8477. var left = x - width / 2;
  8478. var top = y - height / 2;
  8479. ctx.fillRect(left, top, width, height);
  8480. // draw text
  8481. ctx.fillStyle = this.fontColor || "black";
  8482. ctx.textAlign = "left";
  8483. ctx.textBaseline = "top";
  8484. ctx.fillText(text, left, top);
  8485. }
  8486. };
  8487. /**
  8488. * Redraw a edge as a dashed line
  8489. * Draw this edge in the given canvas
  8490. * @author David Jordan
  8491. * @date 2012-08-08
  8492. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8493. * @param {CanvasRenderingContext2D} ctx
  8494. * @private
  8495. */
  8496. Edge.prototype._drawDashLine = function(ctx) {
  8497. // set style
  8498. ctx.strokeStyle = this.color;
  8499. ctx.lineWidth = this._getLineWidth();
  8500. // draw dashed line
  8501. ctx.beginPath();
  8502. ctx.lineCap = 'round';
  8503. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  8504. {
  8505. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8506. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8507. }
  8508. else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
  8509. {
  8510. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8511. [this.dash.length,this.dash.gap]);
  8512. }
  8513. else //If all else fails draw a line
  8514. {
  8515. ctx.moveTo(this.from.x, this.from.y);
  8516. ctx.lineTo(this.to.x, this.to.y);
  8517. }
  8518. ctx.stroke();
  8519. // draw label
  8520. if (this.label) {
  8521. var point = this._pointOnLine(0.5);
  8522. this._label(ctx, this.label, point.x, point.y);
  8523. }
  8524. };
  8525. /**
  8526. * Get a point on a line
  8527. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8528. * @return {Object} point
  8529. * @private
  8530. */
  8531. Edge.prototype._pointOnLine = function (percentage) {
  8532. return {
  8533. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  8534. y: (1 - percentage) * this.from.y + percentage * this.to.y
  8535. }
  8536. };
  8537. /**
  8538. * Get a point on a circle
  8539. * @param {Number} x
  8540. * @param {Number} y
  8541. * @param {Number} radius
  8542. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8543. * @return {Object} point
  8544. * @private
  8545. */
  8546. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  8547. var angle = (percentage - 3/8) * 2 * Math.PI;
  8548. return {
  8549. x: x + radius * Math.cos(angle),
  8550. y: y - radius * Math.sin(angle)
  8551. }
  8552. };
  8553. /**
  8554. * Redraw a edge as a line with an arrow halfway the line
  8555. * Draw this edge in the given canvas
  8556. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8557. * @param {CanvasRenderingContext2D} ctx
  8558. * @private
  8559. */
  8560. Edge.prototype._drawArrowCenter = function(ctx) {
  8561. var point;
  8562. // set style
  8563. ctx.strokeStyle = this.color;
  8564. ctx.fillStyle = this.color;
  8565. ctx.lineWidth = this._getLineWidth();
  8566. if (this.from != this.to) {
  8567. // draw line
  8568. this._line(ctx);
  8569. // draw an arrow halfway the line
  8570. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8571. var length = 10 + 5 * this.width; // TODO: make customizable?
  8572. point = this._pointOnLine(0.5);
  8573. ctx.arrow(point.x, point.y, angle, length);
  8574. ctx.fill();
  8575. ctx.stroke();
  8576. // draw label
  8577. if (this.label) {
  8578. point = this._pointOnLine(0.5);
  8579. this._label(ctx, this.label, point.x, point.y);
  8580. }
  8581. }
  8582. else {
  8583. // draw circle
  8584. var x, y;
  8585. var radius = this.length / 4;
  8586. var node = this.from;
  8587. if (!node.width) {
  8588. node.resize(ctx);
  8589. }
  8590. if (node.width > node.height) {
  8591. x = node.x + node.width / 2;
  8592. y = node.y - radius;
  8593. }
  8594. else {
  8595. x = node.x + radius;
  8596. y = node.y - node.height / 2;
  8597. }
  8598. this._circle(ctx, x, y, radius);
  8599. // draw all arrows
  8600. var angle = 0.2 * Math.PI;
  8601. var length = 10 + 5 * this.width; // TODO: make customizable?
  8602. point = this._pointOnCircle(x, y, radius, 0.5);
  8603. ctx.arrow(point.x, point.y, angle, length);
  8604. ctx.fill();
  8605. ctx.stroke();
  8606. // draw label
  8607. if (this.label) {
  8608. point = this._pointOnCircle(x, y, radius, 0.5);
  8609. this._label(ctx, this.label, point.x, point.y);
  8610. }
  8611. }
  8612. };
  8613. /**
  8614. * Redraw a edge as a line with an arrow
  8615. * Draw this edge in the given canvas
  8616. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8617. * @param {CanvasRenderingContext2D} ctx
  8618. * @private
  8619. */
  8620. Edge.prototype._drawArrow = function(ctx) {
  8621. // set style
  8622. ctx.strokeStyle = this.color;
  8623. ctx.fillStyle = this.color;
  8624. ctx.lineWidth = this._getLineWidth();
  8625. // draw line
  8626. var angle, length;
  8627. if (this.from != this.to) {
  8628. // calculate length and angle of the line
  8629. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8630. var dx = (this.to.x - this.from.x);
  8631. var dy = (this.to.y - this.from.y);
  8632. var lEdge = Math.sqrt(dx * dx + dy * dy);
  8633. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  8634. var pFrom = (lEdge - lFrom) / lEdge;
  8635. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  8636. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  8637. var lTo = this.to.distanceToBorder(ctx, angle);
  8638. var pTo = (lEdge - lTo) / lEdge;
  8639. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  8640. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  8641. ctx.beginPath();
  8642. ctx.moveTo(xFrom, yFrom);
  8643. ctx.lineTo(xTo, yTo);
  8644. ctx.stroke();
  8645. // draw arrow at the end of the line
  8646. length = 10 + 5 * this.width; // TODO: make customizable?
  8647. ctx.arrow(xTo, yTo, angle, length);
  8648. ctx.fill();
  8649. ctx.stroke();
  8650. // draw label
  8651. if (this.label) {
  8652. var point = this._pointOnLine(0.5);
  8653. this._label(ctx, this.label, point.x, point.y);
  8654. }
  8655. }
  8656. else {
  8657. // draw circle
  8658. var node = this.from;
  8659. var x, y, arrow;
  8660. var radius = this.length / 4;
  8661. if (!node.width) {
  8662. node.resize(ctx);
  8663. }
  8664. if (node.width > node.height) {
  8665. x = node.x + node.width / 2;
  8666. y = node.y - radius;
  8667. arrow = {
  8668. x: x,
  8669. y: node.y,
  8670. angle: 0.9 * Math.PI
  8671. };
  8672. }
  8673. else {
  8674. x = node.x + radius;
  8675. y = node.y - node.height / 2;
  8676. arrow = {
  8677. x: node.x,
  8678. y: y,
  8679. angle: 0.6 * Math.PI
  8680. };
  8681. }
  8682. ctx.beginPath();
  8683. // TODO: do not draw a circle, but an arc
  8684. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  8685. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8686. ctx.stroke();
  8687. // draw all arrows
  8688. length = 10 + 5 * this.width; // TODO: make customizable?
  8689. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  8690. ctx.fill();
  8691. ctx.stroke();
  8692. // draw label
  8693. if (this.label) {
  8694. point = this._pointOnCircle(x, y, radius, 0.5);
  8695. this._label(ctx, this.label, point.x, point.y);
  8696. }
  8697. }
  8698. };
  8699. /**
  8700. * Calculate the distance between a point (x3,y3) and a line segment from
  8701. * (x1,y1) to (x2,y2).
  8702. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  8703. * @param {number} x1
  8704. * @param {number} y1
  8705. * @param {number} x2
  8706. * @param {number} y2
  8707. * @param {number} x3
  8708. * @param {number} y3
  8709. * @private
  8710. */
  8711. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  8712. var px = x2-x1,
  8713. py = y2-y1,
  8714. something = px*px + py*py,
  8715. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  8716. if (u > 1) {
  8717. u = 1;
  8718. }
  8719. else if (u < 0) {
  8720. u = 0;
  8721. }
  8722. var x = x1 + u * px,
  8723. y = y1 + u * py,
  8724. dx = x - x3,
  8725. dy = y - y3;
  8726. //# Note: If the actual distance does not matter,
  8727. //# if you only want to compare what this function
  8728. //# returns to other results of this function, you
  8729. //# can just return the squared distance instead
  8730. //# (i.e. remove the sqrt) to gain a little performance
  8731. return Math.sqrt(dx*dx + dy*dy);
  8732. };
  8733. /**
  8734. * Popup is a class to create a popup window with some text
  8735. * @param {Element} container The container object.
  8736. * @param {Number} [x]
  8737. * @param {Number} [y]
  8738. * @param {String} [text]
  8739. */
  8740. function Popup(container, x, y, text) {
  8741. if (container) {
  8742. this.container = container;
  8743. }
  8744. else {
  8745. this.container = document.body;
  8746. }
  8747. this.x = 0;
  8748. this.y = 0;
  8749. this.padding = 5;
  8750. if (x !== undefined && y !== undefined ) {
  8751. this.setPosition(x, y);
  8752. }
  8753. if (text !== undefined) {
  8754. this.setText(text);
  8755. }
  8756. // create the frame
  8757. this.frame = document.createElement("div");
  8758. var style = this.frame.style;
  8759. style.position = "absolute";
  8760. style.visibility = "hidden";
  8761. style.border = "1px solid #666";
  8762. style.color = "black";
  8763. style.padding = this.padding + "px";
  8764. style.backgroundColor = "#FFFFC6";
  8765. style.borderRadius = "3px";
  8766. style.MozBorderRadius = "3px";
  8767. style.WebkitBorderRadius = "3px";
  8768. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  8769. style.whiteSpace = "nowrap";
  8770. this.container.appendChild(this.frame);
  8771. };
  8772. /**
  8773. * @param {number} x Horizontal position of the popup window
  8774. * @param {number} y Vertical position of the popup window
  8775. */
  8776. Popup.prototype.setPosition = function(x, y) {
  8777. this.x = parseInt(x);
  8778. this.y = parseInt(y);
  8779. };
  8780. /**
  8781. * Set the text for the popup window. This can be HTML code
  8782. * @param {string} text
  8783. */
  8784. Popup.prototype.setText = function(text) {
  8785. this.frame.innerHTML = text;
  8786. };
  8787. /**
  8788. * Show the popup window
  8789. * @param {boolean} show Optional. Show or hide the window
  8790. */
  8791. Popup.prototype.show = function (show) {
  8792. if (show === undefined) {
  8793. show = true;
  8794. }
  8795. if (show) {
  8796. var height = this.frame.clientHeight;
  8797. var width = this.frame.clientWidth;
  8798. var maxHeight = this.frame.parentNode.clientHeight;
  8799. var maxWidth = this.frame.parentNode.clientWidth;
  8800. var top = (this.y - height);
  8801. if (top + height + this.padding > maxHeight) {
  8802. top = maxHeight - height - this.padding;
  8803. }
  8804. if (top < this.padding) {
  8805. top = this.padding;
  8806. }
  8807. var left = this.x;
  8808. if (left + width + this.padding > maxWidth) {
  8809. left = maxWidth - width - this.padding;
  8810. }
  8811. if (left < this.padding) {
  8812. left = this.padding;
  8813. }
  8814. this.frame.style.left = left + "px";
  8815. this.frame.style.top = top + "px";
  8816. this.frame.style.visibility = "visible";
  8817. }
  8818. else {
  8819. this.hide();
  8820. }
  8821. };
  8822. /**
  8823. * Hide the popup window
  8824. */
  8825. Popup.prototype.hide = function () {
  8826. this.frame.style.visibility = "hidden";
  8827. };
  8828. /**
  8829. * @class Groups
  8830. * This class can store groups and properties specific for groups.
  8831. */
  8832. Groups = function () {
  8833. this.clear();
  8834. this.defaultIndex = 0;
  8835. };
  8836. /**
  8837. * default constants for group colors
  8838. */
  8839. Groups.DEFAULT = [
  8840. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  8841. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  8842. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  8843. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  8844. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  8845. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  8846. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  8847. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  8848. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  8849. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  8850. ];
  8851. /**
  8852. * Clear all groups
  8853. */
  8854. Groups.prototype.clear = function () {
  8855. this.groups = {};
  8856. this.groups.length = function()
  8857. {
  8858. var i = 0;
  8859. for ( var p in this ) {
  8860. if (this.hasOwnProperty(p)) {
  8861. i++;
  8862. }
  8863. }
  8864. return i;
  8865. }
  8866. };
  8867. /**
  8868. * get group properties of a groupname. If groupname is not found, a new group
  8869. * is added.
  8870. * @param {*} groupname Can be a number, string, Date, etc.
  8871. * @return {Object} group The created group, containing all group properties
  8872. */
  8873. Groups.prototype.get = function (groupname) {
  8874. var group = this.groups[groupname];
  8875. if (group == undefined) {
  8876. // create new group
  8877. var index = this.defaultIndex % Groups.DEFAULT.length;
  8878. this.defaultIndex++;
  8879. group = {};
  8880. group.color = Groups.DEFAULT[index];
  8881. this.groups[groupname] = group;
  8882. }
  8883. return group;
  8884. };
  8885. /**
  8886. * Add a custom group style
  8887. * @param {String} groupname
  8888. * @param {Object} style An object containing borderColor,
  8889. * backgroundColor, etc.
  8890. * @return {Object} group The created group object
  8891. */
  8892. Groups.prototype.add = function (groupname, style) {
  8893. this.groups[groupname] = style;
  8894. if (style.color) {
  8895. style.color = Node.parseColor(style.color);
  8896. }
  8897. return style;
  8898. };
  8899. /**
  8900. * @class Images
  8901. * This class loads images and keeps them stored.
  8902. */
  8903. Images = function () {
  8904. this.images = {};
  8905. this.callback = undefined;
  8906. };
  8907. /**
  8908. * Set an onload callback function. This will be called each time an image
  8909. * is loaded
  8910. * @param {function} callback
  8911. */
  8912. Images.prototype.setOnloadCallback = function(callback) {
  8913. this.callback = callback;
  8914. };
  8915. /**
  8916. *
  8917. * @param {string} url Url of the image
  8918. * @return {Image} img The image object
  8919. */
  8920. Images.prototype.load = function(url) {
  8921. var img = this.images[url];
  8922. if (img == undefined) {
  8923. // create the image
  8924. var images = this;
  8925. img = new Image();
  8926. this.images[url] = img;
  8927. img.onload = function() {
  8928. if (images.callback) {
  8929. images.callback(this);
  8930. }
  8931. };
  8932. img.src = url;
  8933. }
  8934. return img;
  8935. };
  8936. /**
  8937. * @constructor Graph
  8938. * Create a graph visualization, displaying nodes and edges.
  8939. *
  8940. * @param {Element} container The DOM element in which the Graph will
  8941. * be created. Normally a div element.
  8942. * @param {Object} data An object containing parameters
  8943. * {Array} nodes
  8944. * {Array} edges
  8945. * @param {Object} options Options
  8946. */
  8947. function Graph (container, data, options) {
  8948. // create variables and set default values
  8949. this.containerElement = container;
  8950. this.width = '100%';
  8951. this.height = '100%';
  8952. this.refreshRate = 50; // milliseconds
  8953. this.stabilize = true; // stabilize before displaying the graph
  8954. this.selectable = true;
  8955. // set constant values
  8956. this.constants = {
  8957. nodes: {
  8958. radiusMin: 5,
  8959. radiusMax: 20,
  8960. radius: 5,
  8961. distance: 100, // px
  8962. shape: 'ellipse',
  8963. image: undefined,
  8964. widthMin: 16, // px
  8965. widthMax: 64, // px
  8966. fontColor: 'black',
  8967. fontSize: 14, // px
  8968. //fontFace: verdana,
  8969. fontFace: 'arial',
  8970. color: {
  8971. border: '#2B7CE9',
  8972. background: '#97C2FC',
  8973. highlight: {
  8974. border: '#2B7CE9',
  8975. background: '#D2E5FF'
  8976. }
  8977. },
  8978. borderColor: '#2B7CE9',
  8979. backgroundColor: '#97C2FC',
  8980. highlightColor: '#D2E5FF',
  8981. group: undefined
  8982. },
  8983. edges: {
  8984. widthMin: 1,
  8985. widthMax: 15,
  8986. width: 1,
  8987. style: 'line',
  8988. color: '#343434',
  8989. fontColor: '#343434',
  8990. fontSize: 14, // px
  8991. fontFace: 'arial',
  8992. //distance: 100, //px
  8993. length: 100, // px
  8994. dash: {
  8995. length: 10,
  8996. gap: 5,
  8997. altLength: undefined
  8998. }
  8999. },
  9000. minForce: 0.05,
  9001. minVelocity: 0.02, // px/s
  9002. maxIterations: 1000 // maximum number of iteration to stabilize
  9003. };
  9004. var graph = this;
  9005. this.nodes = {}; // object with Node objects
  9006. this.edges = {}; // object with Edge objects
  9007. // TODO: create a counter to keep track on the number of nodes having values
  9008. // TODO: create a counter to keep track on the number of nodes currently moving
  9009. // TODO: create a counter to keep track on the number of edges having values
  9010. this.nodesData = null; // A DataSet or DataView
  9011. this.edgesData = null; // A DataSet or DataView
  9012. // create event listeners used to subscribe on the DataSets of the nodes and edges
  9013. var me = this;
  9014. this.nodesListeners = {
  9015. 'add': function (event, params) {
  9016. me._addNodes(params.items);
  9017. me.start();
  9018. },
  9019. 'update': function (event, params) {
  9020. me._updateNodes(params.items);
  9021. me.start();
  9022. },
  9023. 'remove': function (event, params) {
  9024. me._removeNodes(params.items);
  9025. me.start();
  9026. }
  9027. };
  9028. this.edgesListeners = {
  9029. 'add': function (event, params) {
  9030. me._addEdges(params.items);
  9031. me.start();
  9032. },
  9033. 'update': function (event, params) {
  9034. me._updateEdges(params.items);
  9035. me.start();
  9036. },
  9037. 'remove': function (event, params) {
  9038. me._removeEdges(params.items);
  9039. me.start();
  9040. }
  9041. };
  9042. this.groups = new Groups(); // object with groups
  9043. this.images = new Images(); // object with images
  9044. this.images.setOnloadCallback(function () {
  9045. graph._redraw();
  9046. });
  9047. // properties of the data
  9048. this.moving = false; // True if any of the nodes have an undefined position
  9049. this.selection = [];
  9050. this.timer = undefined;
  9051. // create a frame and canvas
  9052. this._create();
  9053. // apply options
  9054. this.setOptions(options);
  9055. // draw data
  9056. this.setData(data);
  9057. }
  9058. /**
  9059. * Set nodes and edges, and optionally options as well.
  9060. *
  9061. * @param {Object} data Object containing parameters:
  9062. * {Array | DataSet | DataView} [nodes] Array with nodes
  9063. * {Array | DataSet | DataView} [edges] Array with edges
  9064. * {String} [dot] String containing data in DOT format
  9065. * {Options} [options] Object with options
  9066. */
  9067. Graph.prototype.setData = function(data) {
  9068. if (data && data.dot && (data.nodes || data.edges)) {
  9069. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  9070. ' parameter pair "nodes" and "edges", but not both.');
  9071. }
  9072. // set options
  9073. this.setOptions(data && data.options);
  9074. // set all data
  9075. if (data && data.dot) {
  9076. // parse DOT file
  9077. if(data && data.dot) {
  9078. var dotData = vis.util.DOTToGraph(data.dot);
  9079. this.setData(dotData);
  9080. return;
  9081. }
  9082. }
  9083. else {
  9084. this._setNodes(data && data.nodes);
  9085. this._setEdges(data && data.edges);
  9086. }
  9087. // find a stable position or start animating to a stable position
  9088. if (this.stabilize) {
  9089. this._doStabilize();
  9090. }
  9091. this.start();
  9092. };
  9093. /**
  9094. * Set options
  9095. * @param {Object} options
  9096. */
  9097. Graph.prototype.setOptions = function (options) {
  9098. if (options) {
  9099. // retrieve parameter values
  9100. if (options.width != undefined) {this.width = options.width;}
  9101. if (options.height != undefined) {this.height = options.height;}
  9102. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  9103. if (options.selectable != undefined) {this.selectable = options.selectable;}
  9104. // TODO: work out these options and document them
  9105. if (options.edges) {
  9106. for (var prop in options.edges) {
  9107. if (options.edges.hasOwnProperty(prop)) {
  9108. this.constants.edges[prop] = options.edges[prop];
  9109. }
  9110. }
  9111. if (options.edges.length != undefined &&
  9112. options.nodes && options.nodes.distance == undefined) {
  9113. this.constants.edges.length = options.edges.length;
  9114. this.constants.nodes.distance = options.edges.length * 1.25;
  9115. }
  9116. if (!options.edges.fontColor) {
  9117. this.constants.edges.fontColor = options.edges.color;
  9118. }
  9119. // Added to support dashed lines
  9120. // David Jordan
  9121. // 2012-08-08
  9122. if (options.edges.dash) {
  9123. if (options.edges.dash.length != undefined) {
  9124. this.constants.edges.dash.length = options.edges.dash.length;
  9125. }
  9126. if (options.edges.dash.gap != undefined) {
  9127. this.constants.edges.dash.gap = options.edges.dash.gap;
  9128. }
  9129. if (options.edges.dash.altLength != undefined) {
  9130. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  9131. }
  9132. }
  9133. }
  9134. if (options.nodes) {
  9135. for (prop in options.nodes) {
  9136. if (options.nodes.hasOwnProperty(prop)) {
  9137. this.constants.nodes[prop] = options.nodes[prop];
  9138. }
  9139. }
  9140. if (options.nodes.color) {
  9141. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  9142. }
  9143. /*
  9144. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  9145. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  9146. */
  9147. }
  9148. if (options.groups) {
  9149. for (var groupname in options.groups) {
  9150. if (options.groups.hasOwnProperty(groupname)) {
  9151. var group = options.groups[groupname];
  9152. this.groups.add(groupname, group);
  9153. }
  9154. }
  9155. }
  9156. }
  9157. this.setSize(this.width, this.height);
  9158. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  9159. this._setScale(1);
  9160. };
  9161. /**
  9162. * fire an event
  9163. * @param {String} event The name of an event, for example 'select'
  9164. * @param {Object} params Optional object with event parameters
  9165. * @private
  9166. */
  9167. Graph.prototype._trigger = function (event, params) {
  9168. events.trigger(this, event, params);
  9169. };
  9170. /**
  9171. * Create the main frame for the Graph.
  9172. * This function is executed once when a Graph object is created. The frame
  9173. * contains a canvas, and this canvas contains all objects like the axis and
  9174. * nodes.
  9175. * @private
  9176. */
  9177. Graph.prototype._create = function () {
  9178. // remove all elements from the container element.
  9179. while (this.containerElement.hasChildNodes()) {
  9180. this.containerElement.removeChild(this.containerElement.firstChild);
  9181. }
  9182. this.frame = document.createElement('div');
  9183. this.frame.className = 'graph-frame';
  9184. this.frame.style.position = 'relative';
  9185. this.frame.style.overflow = 'hidden';
  9186. // create the graph canvas (HTML canvas element)
  9187. this.frame.canvas = document.createElement( 'canvas' );
  9188. this.frame.canvas.style.position = 'relative';
  9189. this.frame.appendChild(this.frame.canvas);
  9190. if (!this.frame.canvas.getContext) {
  9191. var noCanvas = document.createElement( 'DIV' );
  9192. noCanvas.style.color = 'red';
  9193. noCanvas.style.fontWeight = 'bold' ;
  9194. noCanvas.style.padding = '10px';
  9195. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  9196. this.frame.canvas.appendChild(noCanvas);
  9197. }
  9198. var me = this;
  9199. this.drag = {};
  9200. this.pinch = {};
  9201. this.hammer = Hammer(this.frame.canvas, {
  9202. prevent_default: true
  9203. });
  9204. this.hammer.on('tap', me._onTap.bind(me) );
  9205. this.hammer.on('hold', me._onHold.bind(me) );
  9206. this.hammer.on('pinch', me._onPinch.bind(me) );
  9207. this.hammer.on('touch', me._onTouch.bind(me) );
  9208. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  9209. this.hammer.on('drag', me._onDrag.bind(me) );
  9210. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  9211. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  9212. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  9213. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  9214. // add the frame to the container element
  9215. this.containerElement.appendChild(this.frame);
  9216. };
  9217. /**
  9218. *
  9219. * @param {{x: Number, y: Number}} pointer
  9220. * @return {Number | null} node
  9221. * @private
  9222. */
  9223. Graph.prototype._getNodeAt = function (pointer) {
  9224. var x = this._canvasToX(pointer.x);
  9225. var y = this._canvasToY(pointer.y);
  9226. var obj = {
  9227. left: x,
  9228. top: y,
  9229. right: x,
  9230. bottom: y
  9231. };
  9232. // if there are overlapping nodes, select the last one, this is the
  9233. // one which is drawn on top of the others
  9234. var overlappingNodes = this._getNodesOverlappingWith(obj);
  9235. return (overlappingNodes.length > 0) ?
  9236. overlappingNodes[overlappingNodes.length - 1] : null;
  9237. };
  9238. /**
  9239. * Get the pointer location from a touch location
  9240. * @param {{pageX: Number, pageY: Number}} touch
  9241. * @return {{x: Number, y: Number}} pointer
  9242. * @private
  9243. */
  9244. Graph.prototype._getPointer = function (touch) {
  9245. return {
  9246. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  9247. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  9248. };
  9249. };
  9250. /**
  9251. * On start of a touch gesture, store the pointer
  9252. * @param event
  9253. * @private
  9254. */
  9255. Graph.prototype._onTouch = function (event) {
  9256. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  9257. this.drag.pinched = false;
  9258. this.pinch.scale = this._getScale();
  9259. };
  9260. /**
  9261. * handle drag start event
  9262. * @private
  9263. */
  9264. Graph.prototype._onDragStart = function () {
  9265. var drag = this.drag;
  9266. drag.selection = [];
  9267. drag.translation = this._getTranslation();
  9268. drag.nodeId = this._getNodeAt(drag.pointer);
  9269. // note: drag.pointer is set in _onTouch to get the initial touch location
  9270. var node = this.nodes[drag.nodeId];
  9271. if (node) {
  9272. // select the clicked node if not yet selected
  9273. if (!node.isSelected()) {
  9274. this._selectNodes([drag.nodeId]);
  9275. }
  9276. // create an array with the selected nodes and their original location and status
  9277. var me = this;
  9278. this.selection.forEach(function (id) {
  9279. var node = me.nodes[id];
  9280. if (node) {
  9281. var s = {
  9282. id: id,
  9283. node: node,
  9284. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  9285. x: node.x,
  9286. y: node.y,
  9287. xFixed: node.xFixed,
  9288. yFixed: node.yFixed
  9289. };
  9290. node.xFixed = true;
  9291. node.yFixed = true;
  9292. drag.selection.push(s);
  9293. }
  9294. });
  9295. }
  9296. };
  9297. /**
  9298. * handle drag event
  9299. * @private
  9300. */
  9301. Graph.prototype._onDrag = function (event) {
  9302. if (this.drag.pinched) {
  9303. return;
  9304. }
  9305. var pointer = this._getPointer(event.gesture.touches[0]);
  9306. var me = this,
  9307. drag = this.drag,
  9308. selection = drag.selection;
  9309. if (selection && selection.length) {
  9310. // calculate delta's and new location
  9311. var deltaX = pointer.x - drag.pointer.x,
  9312. deltaY = pointer.y - drag.pointer.y;
  9313. // update position of all selected nodes
  9314. selection.forEach(function (s) {
  9315. var node = s.node;
  9316. if (!s.xFixed) {
  9317. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  9318. }
  9319. if (!s.yFixed) {
  9320. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  9321. }
  9322. });
  9323. // start animation if not yet running
  9324. if (!this.moving) {
  9325. this.moving = true;
  9326. this.start();
  9327. }
  9328. }
  9329. else {
  9330. // move the graph
  9331. var diffX = pointer.x - this.drag.pointer.x;
  9332. var diffY = pointer.y - this.drag.pointer.y;
  9333. this._setTranslation(
  9334. this.drag.translation.x + diffX,
  9335. this.drag.translation.y + diffY);
  9336. this._redraw();
  9337. this.moved = true;
  9338. }
  9339. };
  9340. /**
  9341. * handle drag start event
  9342. * @private
  9343. */
  9344. Graph.prototype._onDragEnd = function () {
  9345. var selection = this.drag.selection;
  9346. if (selection) {
  9347. selection.forEach(function (s) {
  9348. // restore original xFixed and yFixed
  9349. s.node.xFixed = s.xFixed;
  9350. s.node.yFixed = s.yFixed;
  9351. });
  9352. }
  9353. };
  9354. /**
  9355. * handle tap/click event: select/unselect a node
  9356. * @private
  9357. */
  9358. Graph.prototype._onTap = function (event) {
  9359. var pointer = this._getPointer(event.gesture.touches[0]);
  9360. var nodeId = this._getNodeAt(pointer);
  9361. var node = this.nodes[nodeId];
  9362. if (node) {
  9363. // select this node
  9364. this._selectNodes([nodeId]);
  9365. if (!this.moving) {
  9366. this._redraw();
  9367. }
  9368. }
  9369. else {
  9370. // remove selection
  9371. this._unselectNodes();
  9372. this._redraw();
  9373. }
  9374. };
  9375. /**
  9376. * handle long tap event: multi select nodes
  9377. * @private
  9378. */
  9379. Graph.prototype._onHold = function (event) {
  9380. var pointer = this._getPointer(event.gesture.touches[0]);
  9381. var nodeId = this._getNodeAt(pointer);
  9382. var node = this.nodes[nodeId];
  9383. if (node) {
  9384. if (!node.isSelected()) {
  9385. // select this node, keep previous selection
  9386. var append = true;
  9387. this._selectNodes([nodeId], append);
  9388. }
  9389. else {
  9390. this._unselectNodes([nodeId]);
  9391. }
  9392. if (!this.moving) {
  9393. this._redraw();
  9394. }
  9395. }
  9396. else {
  9397. // Do nothing
  9398. }
  9399. };
  9400. /**
  9401. * Handle pinch event
  9402. * @param event
  9403. * @private
  9404. */
  9405. Graph.prototype._onPinch = function (event) {
  9406. var pointer = this._getPointer(event.gesture.center);
  9407. this.drag.pinched = true;
  9408. if (!('scale' in this.pinch)) {
  9409. this.pinch.scale = 1;
  9410. }
  9411. // TODO: enable moving while pinching?
  9412. var scale = this.pinch.scale * event.gesture.scale;
  9413. this._zoom(scale, pointer)
  9414. };
  9415. /**
  9416. * Zoom the graph in or out
  9417. * @param {Number} scale a number around 1, and between 0.01 and 10
  9418. * @param {{x: Number, y: Number}} pointer
  9419. * @return {Number} appliedScale scale is limited within the boundaries
  9420. * @private
  9421. */
  9422. Graph.prototype._zoom = function(scale, pointer) {
  9423. var scaleOld = this._getScale();
  9424. if (scale < 0.01) {
  9425. scale = 0.01;
  9426. }
  9427. if (scale > 10) {
  9428. scale = 10;
  9429. }
  9430. var translation = this._getTranslation();
  9431. var scaleFrac = scale / scaleOld;
  9432. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  9433. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  9434. this._setScale(scale);
  9435. this._setTranslation(tx, ty);
  9436. this._redraw();
  9437. return scale;
  9438. };
  9439. /**
  9440. * Event handler for mouse wheel event, used to zoom the timeline
  9441. * See http://adomas.org/javascript-mouse-wheel/
  9442. * https://github.com/EightMedia/hammer.js/issues/256
  9443. * @param {MouseEvent} event
  9444. * @private
  9445. */
  9446. Graph.prototype._onMouseWheel = function(event) {
  9447. // retrieve delta
  9448. var delta = 0;
  9449. if (event.wheelDelta) { /* IE/Opera. */
  9450. delta = event.wheelDelta/120;
  9451. } else if (event.detail) { /* Mozilla case. */
  9452. // In Mozilla, sign of delta is different than in IE.
  9453. // Also, delta is multiple of 3.
  9454. delta = -event.detail/3;
  9455. }
  9456. // If delta is nonzero, handle it.
  9457. // Basically, delta is now positive if wheel was scrolled up,
  9458. // and negative, if wheel was scrolled down.
  9459. if (delta) {
  9460. if (!('mouswheelScale' in this.pinch)) {
  9461. this.pinch.mouswheelScale = 1;
  9462. }
  9463. // calculate the new scale
  9464. var scale = this.pinch.mouswheelScale;
  9465. var zoom = delta / 10;
  9466. if (delta < 0) {
  9467. zoom = zoom / (1 - zoom);
  9468. }
  9469. scale *= (1 + zoom);
  9470. // calculate the pointer location
  9471. var gesture = Hammer.event.collectEventData(this, 'scroll', event);
  9472. var pointer = this._getPointer(gesture.center);
  9473. // apply the new scale
  9474. scale = this._zoom(scale, pointer);
  9475. // store the new, applied scale
  9476. this.pinch.mouswheelScale = scale;
  9477. }
  9478. // Prevent default actions caused by mouse wheel.
  9479. event.preventDefault();
  9480. };
  9481. /**
  9482. * Mouse move handler for checking whether the title moves over a node with a title.
  9483. * @param {Event} event
  9484. * @private
  9485. */
  9486. Graph.prototype._onMouseMoveTitle = function (event) {
  9487. var gesture = Hammer.event.collectEventData(this, 'mousemove', event);
  9488. var pointer = this._getPointer(gesture.center);
  9489. // check if the previously selected node is still selected
  9490. if (this.popupNode) {
  9491. this._checkHidePopup(pointer);
  9492. }
  9493. // start a timeout that will check if the mouse is positioned above
  9494. // an element
  9495. var me = this;
  9496. var checkShow = function() {
  9497. me._checkShowPopup(pointer);
  9498. };
  9499. if (this.popupTimer) {
  9500. clearInterval(this.popupTimer); // stop any running timer
  9501. }
  9502. if (!this.leftButtonDown) {
  9503. this.popupTimer = setTimeout(checkShow, 300);
  9504. }
  9505. };
  9506. /**
  9507. * Check if there is an element on the given position in the graph
  9508. * (a node or edge). If so, and if this element has a title,
  9509. * show a popup window with its title.
  9510. *
  9511. * @param {{x:Number, y:Number}} pointer
  9512. * @private
  9513. */
  9514. Graph.prototype._checkShowPopup = function (pointer) {
  9515. var obj = {
  9516. left: this._canvasToX(pointer.x),
  9517. top: this._canvasToY(pointer.y),
  9518. right: this._canvasToX(pointer.x),
  9519. bottom: this._canvasToY(pointer.y)
  9520. };
  9521. var id;
  9522. var lastPopupNode = this.popupNode;
  9523. if (this.popupNode == undefined) {
  9524. // search the nodes for overlap, select the top one in case of multiple nodes
  9525. var nodes = this.nodes;
  9526. for (id in nodes) {
  9527. if (nodes.hasOwnProperty(id)) {
  9528. var node = nodes[id];
  9529. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  9530. this.popupNode = node;
  9531. break;
  9532. }
  9533. }
  9534. }
  9535. }
  9536. if (this.popupNode == undefined) {
  9537. // search the edges for overlap
  9538. var edges = this.edges;
  9539. for (id in edges) {
  9540. if (edges.hasOwnProperty(id)) {
  9541. var edge = edges[id];
  9542. if (edge.connected && (edge.getTitle() != undefined) &&
  9543. edge.isOverlappingWith(obj)) {
  9544. this.popupNode = edge;
  9545. break;
  9546. }
  9547. }
  9548. }
  9549. }
  9550. if (this.popupNode) {
  9551. // show popup message window
  9552. if (this.popupNode != lastPopupNode) {
  9553. var me = this;
  9554. if (!me.popup) {
  9555. me.popup = new Popup(me.frame);
  9556. }
  9557. // adjust a small offset such that the mouse cursor is located in the
  9558. // bottom left location of the popup, and you can easily move over the
  9559. // popup area
  9560. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  9561. me.popup.setText(me.popupNode.getTitle());
  9562. me.popup.show();
  9563. }
  9564. }
  9565. else {
  9566. if (this.popup) {
  9567. this.popup.hide();
  9568. }
  9569. }
  9570. };
  9571. /**
  9572. * Check if the popup must be hided, which is the case when the mouse is no
  9573. * longer hovering on the object
  9574. * @param {{x:Number, y:Number}} pointer
  9575. * @private
  9576. */
  9577. Graph.prototype._checkHidePopup = function (pointer) {
  9578. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  9579. this.popupNode = undefined;
  9580. if (this.popup) {
  9581. this.popup.hide();
  9582. }
  9583. }
  9584. };
  9585. /**
  9586. * Unselect selected nodes. If no selection array is provided, all nodes
  9587. * are unselected
  9588. * @param {Object[]} selection Array with selection objects, each selection
  9589. * object has a parameter row. Optional
  9590. * @param {Boolean} triggerSelect If true (default), the select event
  9591. * is triggered when nodes are unselected
  9592. * @return {Boolean} changed True if the selection is changed
  9593. * @private
  9594. */
  9595. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  9596. var changed = false;
  9597. var i, iMax, id;
  9598. if (selection) {
  9599. // remove provided selections
  9600. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9601. id = selection[i];
  9602. this.nodes[id].unselect();
  9603. var j = 0;
  9604. while (j < this.selection.length) {
  9605. if (this.selection[j] == id) {
  9606. this.selection.splice(j, 1);
  9607. changed = true;
  9608. }
  9609. else {
  9610. j++;
  9611. }
  9612. }
  9613. }
  9614. }
  9615. else if (this.selection && this.selection.length) {
  9616. // remove all selections
  9617. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9618. id = this.selection[i];
  9619. this.nodes[id].unselect();
  9620. changed = true;
  9621. }
  9622. this.selection = [];
  9623. }
  9624. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  9625. // fire the select event
  9626. this._trigger('select');
  9627. }
  9628. return changed;
  9629. };
  9630. /**
  9631. * select all nodes on given location x, y
  9632. * @param {Array} selection an array with node ids
  9633. * @param {boolean} append If true, the new selection will be appended to the
  9634. * current selection (except for duplicate entries)
  9635. * @return {Boolean} changed True if the selection is changed
  9636. * @private
  9637. */
  9638. Graph.prototype._selectNodes = function(selection, append) {
  9639. var changed = false;
  9640. var i, iMax;
  9641. // TODO: the selectNodes method is a little messy, rework this
  9642. // check if the current selection equals the desired selection
  9643. var selectionAlreadyThere = true;
  9644. if (selection.length != this.selection.length) {
  9645. selectionAlreadyThere = false;
  9646. }
  9647. else {
  9648. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  9649. if (selection[i] != this.selection[i]) {
  9650. selectionAlreadyThere = false;
  9651. break;
  9652. }
  9653. }
  9654. }
  9655. if (selectionAlreadyThere) {
  9656. return changed;
  9657. }
  9658. if (append == undefined || append == false) {
  9659. // first deselect any selected node
  9660. var triggerSelect = false;
  9661. changed = this._unselectNodes(undefined, triggerSelect);
  9662. }
  9663. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9664. // add each of the new selections, but only when they are not duplicate
  9665. var id = selection[i];
  9666. var isDuplicate = (this.selection.indexOf(id) != -1);
  9667. if (!isDuplicate) {
  9668. this.nodes[id].select();
  9669. this.selection.push(id);
  9670. changed = true;
  9671. }
  9672. }
  9673. if (changed) {
  9674. // fire the select event
  9675. this._trigger('select');
  9676. }
  9677. return changed;
  9678. };
  9679. /**
  9680. * retrieve all nodes overlapping with given object
  9681. * @param {Object} obj An object with parameters left, top, right, bottom
  9682. * @return {Number[]} An array with id's of the overlapping nodes
  9683. * @private
  9684. */
  9685. Graph.prototype._getNodesOverlappingWith = function (obj) {
  9686. var nodes = this.nodes,
  9687. overlappingNodes = [];
  9688. for (var id in nodes) {
  9689. if (nodes.hasOwnProperty(id)) {
  9690. if (nodes[id].isOverlappingWith(obj)) {
  9691. overlappingNodes.push(id);
  9692. }
  9693. }
  9694. }
  9695. return overlappingNodes;
  9696. };
  9697. /**
  9698. * retrieve the currently selected nodes
  9699. * @return {Number[] | String[]} selection An array with the ids of the
  9700. * selected nodes.
  9701. */
  9702. Graph.prototype.getSelection = function() {
  9703. return this.selection.concat([]);
  9704. };
  9705. /**
  9706. * select zero or more nodes
  9707. * @param {Number[] | String[]} selection An array with the ids of the
  9708. * selected nodes.
  9709. */
  9710. Graph.prototype.setSelection = function(selection) {
  9711. var i, iMax, id;
  9712. if (!selection || (selection.length == undefined))
  9713. throw 'Selection must be an array with ids';
  9714. // first unselect any selected node
  9715. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9716. id = this.selection[i];
  9717. this.nodes[id].unselect();
  9718. }
  9719. this.selection = [];
  9720. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9721. id = selection[i];
  9722. var node = this.nodes[id];
  9723. if (!node) {
  9724. throw new RangeError('Node with id "' + id + '" not found');
  9725. }
  9726. node.select();
  9727. this.selection.push(id);
  9728. }
  9729. this.redraw();
  9730. };
  9731. /**
  9732. * Validate the selection: remove ids of nodes which no longer exist
  9733. * @private
  9734. */
  9735. Graph.prototype._updateSelection = function () {
  9736. var i = 0;
  9737. while (i < this.selection.length) {
  9738. var id = this.selection[i];
  9739. if (!this.nodes[id]) {
  9740. this.selection.splice(i, 1);
  9741. }
  9742. else {
  9743. i++;
  9744. }
  9745. }
  9746. };
  9747. /**
  9748. * Temporary method to test calculating a hub value for the nodes
  9749. * @param {number} level Maximum number edges between two nodes in order
  9750. * to call them connected. Optional, 1 by default
  9751. * @return {Number[]} connectioncount array with the connection count
  9752. * for each node
  9753. * @private
  9754. */
  9755. Graph.prototype._getConnectionCount = function(level) {
  9756. if (level == undefined) {
  9757. level = 1;
  9758. }
  9759. // get the nodes connected to given nodes
  9760. function getConnectedNodes(nodes) {
  9761. var connectedNodes = [];
  9762. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  9763. var node = nodes[j];
  9764. // find all nodes connected to this node
  9765. var edges = node.edges;
  9766. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9767. var edge = edges[i];
  9768. var other = null;
  9769. // check if connected
  9770. if (edge.from == node)
  9771. other = edge.to;
  9772. else if (edge.to == node)
  9773. other = edge.from;
  9774. // check if the other node is not already in the list with nodes
  9775. var k, kMax;
  9776. if (other) {
  9777. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  9778. if (nodes[k] == other) {
  9779. other = null;
  9780. break;
  9781. }
  9782. }
  9783. }
  9784. if (other) {
  9785. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  9786. if (connectedNodes[k] == other) {
  9787. other = null;
  9788. break;
  9789. }
  9790. }
  9791. }
  9792. if (other)
  9793. connectedNodes.push(other);
  9794. }
  9795. }
  9796. return connectedNodes;
  9797. }
  9798. var connections = [];
  9799. var nodes = this.nodes;
  9800. for (var id in nodes) {
  9801. if (nodes.hasOwnProperty(id)) {
  9802. var c = [nodes[id]];
  9803. for (var l = 0; l < level; l++) {
  9804. c = c.concat(getConnectedNodes(c));
  9805. }
  9806. connections.push(c);
  9807. }
  9808. }
  9809. var hubs = [];
  9810. for (var i = 0, len = connections.length; i < len; i++) {
  9811. hubs.push(connections[i].length);
  9812. }
  9813. return hubs;
  9814. };
  9815. /**
  9816. * Set a new size for the graph
  9817. * @param {string} width Width in pixels or percentage (for example '800px'
  9818. * or '50%')
  9819. * @param {string} height Height in pixels or percentage (for example '400px'
  9820. * or '30%')
  9821. */
  9822. Graph.prototype.setSize = function(width, height) {
  9823. this.frame.style.width = width;
  9824. this.frame.style.height = height;
  9825. this.frame.canvas.style.width = '100%';
  9826. this.frame.canvas.style.height = '100%';
  9827. this.frame.canvas.width = this.frame.canvas.clientWidth;
  9828. this.frame.canvas.height = this.frame.canvas.clientHeight;
  9829. };
  9830. /**
  9831. * Set a data set with nodes for the graph
  9832. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  9833. * @private
  9834. */
  9835. Graph.prototype._setNodes = function(nodes) {
  9836. var oldNodesData = this.nodesData;
  9837. if (nodes instanceof DataSet || nodes instanceof DataView) {
  9838. this.nodesData = nodes;
  9839. }
  9840. else if (nodes instanceof Array) {
  9841. this.nodesData = new DataSet();
  9842. this.nodesData.add(nodes);
  9843. }
  9844. else if (!nodes) {
  9845. this.nodesData = new DataSet();
  9846. }
  9847. else {
  9848. throw new TypeError('Array or DataSet expected');
  9849. }
  9850. if (oldNodesData) {
  9851. // unsubscribe from old dataset
  9852. util.forEach(this.nodesListeners, function (callback, event) {
  9853. oldNodesData.unsubscribe(event, callback);
  9854. });
  9855. }
  9856. // remove drawn nodes
  9857. this.nodes = {};
  9858. if (this.nodesData) {
  9859. // subscribe to new dataset
  9860. var me = this;
  9861. util.forEach(this.nodesListeners, function (callback, event) {
  9862. me.nodesData.subscribe(event, callback);
  9863. });
  9864. // draw all new nodes
  9865. var ids = this.nodesData.getIds();
  9866. this._addNodes(ids);
  9867. }
  9868. this._updateSelection();
  9869. };
  9870. /**
  9871. * Add nodes
  9872. * @param {Number[] | String[]} ids
  9873. * @private
  9874. */
  9875. Graph.prototype._addNodes = function(ids) {
  9876. var id;
  9877. for (var i = 0, len = ids.length; i < len; i++) {
  9878. id = ids[i];
  9879. var data = this.nodesData.get(id);
  9880. var node = new Node(data, this.images, this.groups, this.constants);
  9881. this.nodes[id] = node; // note: this may replace an existing node
  9882. if (!node.isFixed()) {
  9883. // TODO: position new nodes in a smarter way!
  9884. var radius = this.constants.edges.length * 2;
  9885. var count = ids.length;
  9886. var angle = 2 * Math.PI * (i / count);
  9887. node.x = radius * Math.cos(angle);
  9888. node.y = radius * Math.sin(angle);
  9889. // note: no not use node.isMoving() here, as that gives the current
  9890. // velocity of the node, which is zero after creation of the node.
  9891. this.moving = true;
  9892. }
  9893. }
  9894. this._reconnectEdges();
  9895. this._updateValueRange(this.nodes);
  9896. };
  9897. /**
  9898. * Update existing nodes, or create them when not yet existing
  9899. * @param {Number[] | String[]} ids
  9900. * @private
  9901. */
  9902. Graph.prototype._updateNodes = function(ids) {
  9903. var nodes = this.nodes,
  9904. nodesData = this.nodesData;
  9905. for (var i = 0, len = ids.length; i < len; i++) {
  9906. var id = ids[i];
  9907. var node = nodes[id];
  9908. var data = nodesData.get(id);
  9909. if (node) {
  9910. // update node
  9911. node.setProperties(data, this.constants);
  9912. }
  9913. else {
  9914. // create node
  9915. node = new Node(properties, this.images, this.groups, this.constants);
  9916. nodes[id] = node;
  9917. if (!node.isFixed()) {
  9918. this.moving = true;
  9919. }
  9920. }
  9921. }
  9922. this._reconnectEdges();
  9923. this._updateValueRange(nodes);
  9924. };
  9925. /**
  9926. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  9927. * @param {Number[] | String[]} ids
  9928. * @private
  9929. */
  9930. Graph.prototype._removeNodes = function(ids) {
  9931. var nodes = this.nodes;
  9932. for (var i = 0, len = ids.length; i < len; i++) {
  9933. var id = ids[i];
  9934. delete nodes[id];
  9935. }
  9936. this._reconnectEdges();
  9937. this._updateSelection();
  9938. this._updateValueRange(nodes);
  9939. };
  9940. /**
  9941. * Load edges by reading the data table
  9942. * @param {Array | DataSet | DataView} edges The data containing the edges.
  9943. * @private
  9944. * @private
  9945. */
  9946. Graph.prototype._setEdges = function(edges) {
  9947. var oldEdgesData = this.edgesData;
  9948. if (edges instanceof DataSet || edges instanceof DataView) {
  9949. this.edgesData = edges;
  9950. }
  9951. else if (edges instanceof Array) {
  9952. this.edgesData = new DataSet();
  9953. this.edgesData.add(edges);
  9954. }
  9955. else if (!edges) {
  9956. this.edgesData = new DataSet();
  9957. }
  9958. else {
  9959. throw new TypeError('Array or DataSet expected');
  9960. }
  9961. if (oldEdgesData) {
  9962. // unsubscribe from old dataset
  9963. util.forEach(this.edgesListeners, function (callback, event) {
  9964. oldEdgesData.unsubscribe(event, callback);
  9965. });
  9966. }
  9967. // remove drawn edges
  9968. this.edges = {};
  9969. if (this.edgesData) {
  9970. // subscribe to new dataset
  9971. var me = this;
  9972. util.forEach(this.edgesListeners, function (callback, event) {
  9973. me.edgesData.subscribe(event, callback);
  9974. });
  9975. // draw all new nodes
  9976. var ids = this.edgesData.getIds();
  9977. this._addEdges(ids);
  9978. }
  9979. this._reconnectEdges();
  9980. };
  9981. /**
  9982. * Add edges
  9983. * @param {Number[] | String[]} ids
  9984. * @private
  9985. */
  9986. Graph.prototype._addEdges = function (ids) {
  9987. var edges = this.edges,
  9988. edgesData = this.edgesData;
  9989. for (var i = 0, len = ids.length; i < len; i++) {
  9990. var id = ids[i];
  9991. var oldEdge = edges[id];
  9992. if (oldEdge) {
  9993. oldEdge.disconnect();
  9994. }
  9995. var data = edgesData.get(id);
  9996. edges[id] = new Edge(data, this, this.constants);
  9997. }
  9998. this.moving = true;
  9999. this._updateValueRange(edges);
  10000. };
  10001. /**
  10002. * Update existing edges, or create them when not yet existing
  10003. * @param {Number[] | String[]} ids
  10004. * @private
  10005. */
  10006. Graph.prototype._updateEdges = function (ids) {
  10007. var edges = this.edges,
  10008. edgesData = this.edgesData;
  10009. for (var i = 0, len = ids.length; i < len; i++) {
  10010. var id = ids[i];
  10011. var data = edgesData.get(id);
  10012. var edge = edges[id];
  10013. if (edge) {
  10014. // update edge
  10015. edge.disconnect();
  10016. edge.setProperties(data, this.constants);
  10017. edge.connect();
  10018. }
  10019. else {
  10020. // create edge
  10021. edge = new Edge(data, this, this.constants);
  10022. this.edges[id] = edge;
  10023. }
  10024. }
  10025. this.moving = true;
  10026. this._updateValueRange(edges);
  10027. };
  10028. /**
  10029. * Remove existing edges. Non existing ids will be ignored
  10030. * @param {Number[] | String[]} ids
  10031. * @private
  10032. */
  10033. Graph.prototype._removeEdges = function (ids) {
  10034. var edges = this.edges;
  10035. for (var i = 0, len = ids.length; i < len; i++) {
  10036. var id = ids[i];
  10037. var edge = edges[id];
  10038. if (edge) {
  10039. edge.disconnect();
  10040. delete edges[id];
  10041. }
  10042. }
  10043. this.moving = true;
  10044. this._updateValueRange(edges);
  10045. };
  10046. /**
  10047. * Reconnect all edges
  10048. * @private
  10049. */
  10050. Graph.prototype._reconnectEdges = function() {
  10051. var id,
  10052. nodes = this.nodes,
  10053. edges = this.edges;
  10054. for (id in nodes) {
  10055. if (nodes.hasOwnProperty(id)) {
  10056. nodes[id].edges = [];
  10057. }
  10058. }
  10059. for (id in edges) {
  10060. if (edges.hasOwnProperty(id)) {
  10061. var edge = edges[id];
  10062. edge.from = null;
  10063. edge.to = null;
  10064. edge.connect();
  10065. }
  10066. }
  10067. };
  10068. /**
  10069. * Update the values of all object in the given array according to the current
  10070. * value range of the objects in the array.
  10071. * @param {Object} obj An object containing a set of Edges or Nodes
  10072. * The objects must have a method getValue() and
  10073. * setValueRange(min, max).
  10074. * @private
  10075. */
  10076. Graph.prototype._updateValueRange = function(obj) {
  10077. var id;
  10078. // determine the range of the objects
  10079. var valueMin = undefined;
  10080. var valueMax = undefined;
  10081. for (id in obj) {
  10082. if (obj.hasOwnProperty(id)) {
  10083. var value = obj[id].getValue();
  10084. if (value !== undefined) {
  10085. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  10086. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  10087. }
  10088. }
  10089. }
  10090. // adjust the range of all objects
  10091. if (valueMin !== undefined && valueMax !== undefined) {
  10092. for (id in obj) {
  10093. if (obj.hasOwnProperty(id)) {
  10094. obj[id].setValueRange(valueMin, valueMax);
  10095. }
  10096. }
  10097. }
  10098. };
  10099. /**
  10100. * Redraw the graph with the current data
  10101. * chart will be resized too.
  10102. */
  10103. Graph.prototype.redraw = function() {
  10104. this.setSize(this.width, this.height);
  10105. this._redraw();
  10106. };
  10107. /**
  10108. * Redraw the graph with the current data
  10109. * @private
  10110. */
  10111. Graph.prototype._redraw = function() {
  10112. var ctx = this.frame.canvas.getContext('2d');
  10113. // clear the canvas
  10114. var w = this.frame.canvas.width;
  10115. var h = this.frame.canvas.height;
  10116. ctx.clearRect(0, 0, w, h);
  10117. // set scaling and translation
  10118. ctx.save();
  10119. ctx.translate(this.translation.x, this.translation.y);
  10120. ctx.scale(this.scale, this.scale);
  10121. this._drawEdges(ctx);
  10122. this._drawNodes(ctx);
  10123. // restore original scaling and translation
  10124. ctx.restore();
  10125. };
  10126. /**
  10127. * Set the translation of the graph
  10128. * @param {Number} offsetX Horizontal offset
  10129. * @param {Number} offsetY Vertical offset
  10130. * @private
  10131. */
  10132. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  10133. if (this.translation === undefined) {
  10134. this.translation = {
  10135. x: 0,
  10136. y: 0
  10137. };
  10138. }
  10139. if (offsetX !== undefined) {
  10140. this.translation.x = offsetX;
  10141. }
  10142. if (offsetY !== undefined) {
  10143. this.translation.y = offsetY;
  10144. }
  10145. };
  10146. /**
  10147. * Get the translation of the graph
  10148. * @return {Object} translation An object with parameters x and y, both a number
  10149. * @private
  10150. */
  10151. Graph.prototype._getTranslation = function() {
  10152. return {
  10153. x: this.translation.x,
  10154. y: this.translation.y
  10155. };
  10156. };
  10157. /**
  10158. * Scale the graph
  10159. * @param {Number} scale Scaling factor 1.0 is unscaled
  10160. * @private
  10161. */
  10162. Graph.prototype._setScale = function(scale) {
  10163. this.scale = scale;
  10164. };
  10165. /**
  10166. * Get the current scale of the graph
  10167. * @return {Number} scale Scaling factor 1.0 is unscaled
  10168. * @private
  10169. */
  10170. Graph.prototype._getScale = function() {
  10171. return this.scale;
  10172. };
  10173. /**
  10174. * Convert a horizontal point on the HTML canvas to the x-value of the model
  10175. * @param {number} x
  10176. * @returns {number}
  10177. * @private
  10178. */
  10179. Graph.prototype._canvasToX = function(x) {
  10180. return (x - this.translation.x) / this.scale;
  10181. };
  10182. /**
  10183. * Convert an x-value in the model to a horizontal point on the HTML canvas
  10184. * @param {number} x
  10185. * @returns {number}
  10186. * @private
  10187. */
  10188. Graph.prototype._xToCanvas = function(x) {
  10189. return x * this.scale + this.translation.x;
  10190. };
  10191. /**
  10192. * Convert a vertical point on the HTML canvas to the y-value of the model
  10193. * @param {number} y
  10194. * @returns {number}
  10195. * @private
  10196. */
  10197. Graph.prototype._canvasToY = function(y) {
  10198. return (y - this.translation.y) / this.scale;
  10199. };
  10200. /**
  10201. * Convert an y-value in the model to a vertical point on the HTML canvas
  10202. * @param {number} y
  10203. * @returns {number}
  10204. * @private
  10205. */
  10206. Graph.prototype._yToCanvas = function(y) {
  10207. return y * this.scale + this.translation.y ;
  10208. };
  10209. /**
  10210. * Redraw all nodes
  10211. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  10212. * @param {CanvasRenderingContext2D} ctx
  10213. * @private
  10214. */
  10215. Graph.prototype._drawNodes = function(ctx) {
  10216. // first draw the unselected nodes
  10217. var nodes = this.nodes;
  10218. var selected = [];
  10219. for (var id in nodes) {
  10220. if (nodes.hasOwnProperty(id)) {
  10221. if (nodes[id].isSelected()) {
  10222. selected.push(id);
  10223. }
  10224. else {
  10225. nodes[id].draw(ctx);
  10226. }
  10227. }
  10228. }
  10229. // draw the selected nodes on top
  10230. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  10231. nodes[selected[s]].draw(ctx);
  10232. }
  10233. };
  10234. /**
  10235. * Redraw all edges
  10236. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  10237. * @param {CanvasRenderingContext2D} ctx
  10238. * @private
  10239. */
  10240. Graph.prototype._drawEdges = function(ctx) {
  10241. var edges = this.edges;
  10242. for (var id in edges) {
  10243. if (edges.hasOwnProperty(id)) {
  10244. var edge = edges[id];
  10245. if (edge.connected) {
  10246. edges[id].draw(ctx);
  10247. }
  10248. }
  10249. }
  10250. };
  10251. /**
  10252. * Find a stable position for all nodes
  10253. * @private
  10254. */
  10255. Graph.prototype._doStabilize = function() {
  10256. var start = new Date();
  10257. // find stable position
  10258. var count = 0;
  10259. var vmin = this.constants.minVelocity;
  10260. var stable = false;
  10261. while (!stable && count < this.constants.maxIterations) {
  10262. this._calculateForces();
  10263. this._discreteStepNodes();
  10264. stable = !this._isMoving(vmin);
  10265. count++;
  10266. }
  10267. var end = new Date();
  10268. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  10269. };
  10270. /**
  10271. * Calculate the external forces acting on the nodes
  10272. * Forces are caused by: edges, repulsing forces between nodes, gravity
  10273. * @private
  10274. */
  10275. Graph.prototype._calculateForces = function() {
  10276. // create a local edge to the nodes and edges, that is faster
  10277. var id, dx, dy, angle, distance, fx, fy,
  10278. repulsingForce, springForce, length, edgeLength,
  10279. nodes = this.nodes,
  10280. edges = this.edges;
  10281. // gravity, add a small constant force to pull the nodes towards the center of
  10282. // the graph
  10283. // Also, the forces are reset to zero in this loop by using _setForce instead
  10284. // of _addForce
  10285. var gravity = 0.01,
  10286. gx = this.frame.canvas.clientWidth / 2,
  10287. gy = this.frame.canvas.clientHeight / 2;
  10288. for (id in nodes) {
  10289. if (nodes.hasOwnProperty(id)) {
  10290. var node = nodes[id];
  10291. dx = gx - node.x;
  10292. dy = gy - node.y;
  10293. angle = Math.atan2(dy, dx);
  10294. fx = Math.cos(angle) * gravity;
  10295. fy = Math.sin(angle) * gravity;
  10296. node._setForce(fx, fy);
  10297. }
  10298. }
  10299. // repulsing forces between nodes
  10300. var minimumDistance = this.constants.nodes.distance,
  10301. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  10302. for (var id1 in nodes) {
  10303. if (nodes.hasOwnProperty(id1)) {
  10304. var node1 = nodes[id1];
  10305. for (var id2 in nodes) {
  10306. if (nodes.hasOwnProperty(id2)) {
  10307. var node2 = nodes[id2];
  10308. // calculate normally distributed force
  10309. dx = node2.x - node1.x;
  10310. dy = node2.y - node1.y;
  10311. distance = Math.sqrt(dx * dx + dy * dy);
  10312. angle = Math.atan2(dy, dx);
  10313. // TODO: correct factor for repulsing force
  10314. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10315. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10316. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  10317. fx = Math.cos(angle) * repulsingForce;
  10318. fy = Math.sin(angle) * repulsingForce;
  10319. node1._addForce(-fx, -fy);
  10320. node2._addForce(fx, fy);
  10321. }
  10322. }
  10323. }
  10324. }
  10325. /* TODO: re-implement repulsion of edges
  10326. for (var n = 0; n < nodes.length; n++) {
  10327. for (var l = 0; l < edges.length; l++) {
  10328. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  10329. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  10330. // calculate normally distributed force
  10331. dx = nodes[n].x - lx,
  10332. dy = nodes[n].y - ly,
  10333. distance = Math.sqrt(dx * dx + dy * dy),
  10334. angle = Math.atan2(dy, dx),
  10335. // TODO: correct factor for repulsing force
  10336. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10337. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  10338. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  10339. fx = Math.cos(angle) * repulsingforce,
  10340. fy = Math.sin(angle) * repulsingforce;
  10341. nodes[n]._addForce(fx, fy);
  10342. edges[l].from._addForce(-fx/2,-fy/2);
  10343. edges[l].to._addForce(-fx/2,-fy/2);
  10344. }
  10345. }
  10346. */
  10347. // forces caused by the edges, modelled as springs
  10348. for (id in edges) {
  10349. if (edges.hasOwnProperty(id)) {
  10350. var edge = edges[id];
  10351. if (edge.connected) {
  10352. dx = (edge.to.x - edge.from.x);
  10353. dy = (edge.to.y - edge.from.y);
  10354. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  10355. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  10356. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  10357. edgeLength = edge.length;
  10358. length = Math.sqrt(dx * dx + dy * dy);
  10359. angle = Math.atan2(dy, dx);
  10360. springForce = edge.stiffness * (edgeLength - length);
  10361. fx = Math.cos(angle) * springForce;
  10362. fy = Math.sin(angle) * springForce;
  10363. edge.from._addForce(-fx, -fy);
  10364. edge.to._addForce(fx, fy);
  10365. }
  10366. }
  10367. }
  10368. /* TODO: re-implement repulsion of edges
  10369. // repulsing forces between edges
  10370. var minimumDistance = this.constants.edges.distance,
  10371. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  10372. for (var l = 0; l < edges.length; l++) {
  10373. //Keep distance from other edge centers
  10374. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  10375. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  10376. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  10377. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  10378. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  10379. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  10380. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  10381. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  10382. // calculate normally distributed force
  10383. dx = l2x - lx,
  10384. dy = l2y - ly,
  10385. distance = Math.sqrt(dx * dx + dy * dy),
  10386. angle = Math.atan2(dy, dx),
  10387. // TODO: correct factor for repulsing force
  10388. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  10389. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  10390. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  10391. fx = Math.cos(angle) * repulsingforce,
  10392. fy = Math.sin(angle) * repulsingforce;
  10393. edges[l].from._addForce(-fx, -fy);
  10394. edges[l].to._addForce(-fx, -fy);
  10395. edges[l2].from._addForce(fx, fy);
  10396. edges[l2].to._addForce(fx, fy);
  10397. }
  10398. }
  10399. */
  10400. };
  10401. /**
  10402. * Check if any of the nodes is still moving
  10403. * @param {number} vmin the minimum velocity considered as 'moving'
  10404. * @return {boolean} true if moving, false if non of the nodes is moving
  10405. * @private
  10406. */
  10407. Graph.prototype._isMoving = function(vmin) {
  10408. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  10409. var nodes = this.nodes;
  10410. for (var id in nodes) {
  10411. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  10412. return true;
  10413. }
  10414. }
  10415. return false;
  10416. };
  10417. /**
  10418. * Perform one discrete step for all nodes
  10419. * @private
  10420. */
  10421. Graph.prototype._discreteStepNodes = function() {
  10422. var interval = this.refreshRate / 1000.0; // in seconds
  10423. var nodes = this.nodes;
  10424. for (var id in nodes) {
  10425. if (nodes.hasOwnProperty(id)) {
  10426. nodes[id].discreteStep(interval);
  10427. }
  10428. }
  10429. };
  10430. /**
  10431. * Start animating nodes and edges
  10432. */
  10433. Graph.prototype.start = function() {
  10434. if (this.moving) {
  10435. this._calculateForces();
  10436. this._discreteStepNodes();
  10437. var vmin = this.constants.minVelocity;
  10438. this.moving = this._isMoving(vmin);
  10439. }
  10440. if (this.moving) {
  10441. // start animation. only start timer if it is not already running
  10442. if (!this.timer) {
  10443. var graph = this;
  10444. this.timer = window.setTimeout(function () {
  10445. graph.timer = undefined;
  10446. graph.start();
  10447. graph._redraw();
  10448. }, this.refreshRate);
  10449. }
  10450. }
  10451. else {
  10452. this._redraw();
  10453. }
  10454. };
  10455. /**
  10456. * Stop animating nodes and edges.
  10457. */
  10458. Graph.prototype.stop = function () {
  10459. if (this.timer) {
  10460. window.clearInterval(this.timer);
  10461. this.timer = undefined;
  10462. }
  10463. };
  10464. /**
  10465. * vis.js module exports
  10466. */
  10467. var vis = {
  10468. util: util,
  10469. events: events,
  10470. Controller: Controller,
  10471. DataSet: DataSet,
  10472. DataView: DataView,
  10473. Range: Range,
  10474. Stack: Stack,
  10475. TimeStep: TimeStep,
  10476. EventBus: EventBus,
  10477. components: {
  10478. items: {
  10479. Item: Item,
  10480. ItemBox: ItemBox,
  10481. ItemPoint: ItemPoint,
  10482. ItemRange: ItemRange
  10483. },
  10484. Component: Component,
  10485. Panel: Panel,
  10486. RootPanel: RootPanel,
  10487. ItemSet: ItemSet,
  10488. TimeAxis: TimeAxis
  10489. },
  10490. graph: {
  10491. Node: Node,
  10492. Edge: Edge,
  10493. Popup: Popup,
  10494. Groups: Groups,
  10495. Images: Images
  10496. },
  10497. Timeline: Timeline,
  10498. Graph: Graph
  10499. };
  10500. /**
  10501. * CommonJS module exports
  10502. */
  10503. if (typeof exports !== 'undefined') {
  10504. exports = vis;
  10505. }
  10506. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  10507. module.exports = vis;
  10508. }
  10509. /**
  10510. * AMD module exports
  10511. */
  10512. if (typeof(define) === 'function') {
  10513. define(function () {
  10514. return vis;
  10515. });
  10516. }
  10517. /**
  10518. * Window exports
  10519. */
  10520. if (typeof window !== 'undefined') {
  10521. // attach the module to the window, load as a regular javascript file
  10522. window['vis'] = vis;
  10523. }
  10524. // inject css
  10525. util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\r\n}\r\n\n\r\n.vis.timeline.rootpanel {\r\n position: relative;\r\n overflow: hidden;\r\n\r\n border: 1px solid #bfbfbf;\r\n -moz-box-sizing: border-box;\r\n box-sizing: border-box;\r\n}\r\n\r\n.vis.timeline .panel {\r\n position: absolute;\r\n overflow: hidden;\r\n}\r\n\n\r\n.vis.timeline .groupset {\r\n position: absolute;\r\n padding: 0;\r\n margin: 0;\r\n}\r\n\r\n.vis.timeline .labels {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n\r\n padding: 0;\r\n margin: 0;\r\n\r\n border-right: 1px solid #bfbfbf;\r\n box-sizing: border-box;\r\n -moz-box-sizing: border-box;\r\n}\r\n\r\n.vis.timeline .labels .label-set {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n\r\n overflow: hidden;\r\n\r\n border-top: none;\r\n border-bottom: 1px solid #bfbfbf;\r\n}\r\n\r\n.vis.timeline .labels .label-set .label {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n width: 100%;\r\n color: #4d4d4d;\r\n}\r\n\r\n.vis.timeline.top .labels .label-set .label,\r\n.vis.timeline.top .groupset .itemset-axis {\r\n border-top: 1px solid #bfbfbf;\r\n border-bottom: none;\r\n}\r\n\r\n.vis.timeline.bottom .labels .label-set .label,\r\n.vis.timeline.bottom .groupset .itemset-axis {\r\n border-top: none;\r\n border-bottom: 1px solid #bfbfbf;\r\n}\r\n\r\n.vis.timeline .labels .label-set .label .inner {\r\n display: inline-block;\r\n padding: 5px;\r\n}\r\n\n\r\n.vis.timeline .itemset {\r\n position: absolute;\r\n padding: 0;\r\n margin: 0;\r\n overflow: hidden;\r\n}\r\n\r\n.vis.timeline .background {\r\n}\r\n\r\n.vis.timeline .foreground {\r\n}\r\n\r\n.vis.timeline .itemset-axis {\r\n position: absolute;\r\n}\r\n\n\r\n.vis.timeline .item {\r\n position: absolute;\r\n color: #1A1A1A;\r\n border-color: #97B0F8;\r\n background-color: #D5DDF6;\r\n display: inline-block;\r\n}\r\n\r\n.vis.timeline .item.selected {\r\n border-color: #FFC200;\r\n background-color: #FFF785;\r\n z-index: 999;\r\n}\r\n\r\n.vis.timeline .item.cluster {\r\n /* TODO: use another color or pattern? */\r\n background: #97B0F8 url('img/cluster_bg.png');\r\n color: white;\r\n}\r\n.vis.timeline .item.cluster.point {\r\n border-color: #D5DDF6;\r\n}\r\n\r\n.vis.timeline .item.box {\r\n text-align: center;\r\n border-style: solid;\r\n border-width: 1px;\r\n border-radius: 5px;\r\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\r\n}\r\n\r\n.vis.timeline .item.point {\r\n background: none;\r\n}\r\n\r\n.vis.timeline .dot {\r\n border: 5px solid #97B0F8;\r\n position: absolute;\r\n border-radius: 5px;\r\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\r\n}\r\n\r\n.vis.timeline .item.range {\r\n overflow: hidden;\r\n border-style: solid;\r\n border-width: 1px;\r\n border-radius: 2px;\r\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\r\n}\r\n\r\n.vis.timeline .item.rangeoverflow {\r\n border-style: solid;\r\n border-width: 1px;\r\n border-radius: 2px;\r\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\r\n}\r\n\r\n.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {\r\n cursor: w-resize;\r\n z-index: 1000;\r\n}\r\n\r\n.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {\r\n cursor: e-resize;\r\n z-index: 1000;\r\n}\r\n\r\n.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {\r\n position: relative;\r\n display: inline-block;\r\n}\r\n\r\n.vis.timeline .item.line {\r\n position: absolute;\r\n width: 0;\r\n border-left-width: 1px;\r\n border-left-style: solid;\r\n}\r\n\r\n.vis.timeline .item .content {\r\n margin: 5px;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n}\r\n\n.vis.timeline .axis {\r\n position: relative;\r\n}\r\n\r\n.vis.timeline .axis .text {\r\n position: absolute;\r\n color: #4d4d4d;\r\n padding: 3px;\r\n white-space: nowrap;\r\n}\r\n\r\n.vis.timeline .axis .text.measure {\r\n position: absolute;\r\n padding-left: 0;\r\n p