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.

14154 lines
424 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.0.8
  8. * @date 2013-06-03
  9. *
  10. * @license
  11. * Copyright (C) 2011-2013 Almende B.V, http://almende.com
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. (function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports;
  26. return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
  27. (function(){/**
  28. * vis.js module imports
  29. */
  30. var moment = require('moment');
  31. /**
  32. * utility functions
  33. */
  34. var util = {};
  35. /**
  36. * Test whether given object is a number
  37. * @param {*} object
  38. * @return {Boolean} isNumber
  39. */
  40. util.isNumber = function isNumber(object) {
  41. return (object instanceof Number || typeof object == 'number');
  42. };
  43. /**
  44. * Test whether given object is a string
  45. * @param {*} object
  46. * @return {Boolean} isString
  47. */
  48. util.isString = function isString(object) {
  49. return (object instanceof String || typeof object == 'string');
  50. };
  51. /**
  52. * Test whether given object is a Date, or a String containing a Date
  53. * @param {Date | String} object
  54. * @return {Boolean} isDate
  55. */
  56. util.isDate = function isDate(object) {
  57. if (object instanceof Date) {
  58. return true;
  59. }
  60. else if (util.isString(object)) {
  61. // test whether this string contains a date
  62. var match = ASPDateRegex.exec(object);
  63. if (match) {
  64. return true;
  65. }
  66. else if (!isNaN(Date.parse(object))) {
  67. return true;
  68. }
  69. }
  70. return false;
  71. };
  72. /**
  73. * Test whether given object is an instance of google.visualization.DataTable
  74. * @param {*} object
  75. * @return {Boolean} isDataTable
  76. */
  77. util.isDataTable = function isDataTable(object) {
  78. return (typeof (google) !== 'undefined') &&
  79. (google.visualization) &&
  80. (google.visualization.DataTable) &&
  81. (object instanceof google.visualization.DataTable);
  82. };
  83. /**
  84. * Create a semi UUID
  85. * source: http://stackoverflow.com/a/105074/1262753
  86. * @return {String} uuid
  87. */
  88. util.randomUUID = function randomUUID () {
  89. var S4 = function () {
  90. return Math.floor(
  91. Math.random() * 0x10000 /* 65536 */
  92. ).toString(16);
  93. };
  94. return (
  95. S4() + S4() + '-' +
  96. S4() + '-' +
  97. S4() + '-' +
  98. S4() + '-' +
  99. S4() + S4() + S4()
  100. );
  101. };
  102. /**
  103. * Extend object a with the properties of object b or a series of objects
  104. * Only properties with defined values are copied
  105. * @param {Object} a
  106. * @param {... Object} b
  107. * @return {Object} a
  108. */
  109. util.extend = function (a, b) {
  110. for (var i = 1, len = arguments.length; i < len; i++) {
  111. var other = arguments[i];
  112. for (var prop in other) {
  113. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  114. a[prop] = other[prop];
  115. }
  116. }
  117. }
  118. return a;
  119. };
  120. /**
  121. * Cast an object to another type
  122. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  123. * @param {String | undefined} type Name of the type. Available types:
  124. * 'Boolean', 'Number', 'String',
  125. * 'Date', 'Moment', ISODate', 'ASPDate'.
  126. * @return {*} object
  127. * @throws Error
  128. */
  129. util.cast = function cast(object, type) {
  130. var match;
  131. if (object === undefined) {
  132. return undefined;
  133. }
  134. if (object === null) {
  135. return null;
  136. }
  137. if (!type) {
  138. return object;
  139. }
  140. if (!(typeof type === 'string') && !(type instanceof String)) {
  141. throw new Error('Type must be a string');
  142. }
  143. //noinspection FallthroughInSwitchStatementJS
  144. switch (type) {
  145. case 'boolean':
  146. case 'Boolean':
  147. return Boolean(object);
  148. case 'number':
  149. case 'Number':
  150. return Number(object);
  151. case 'string':
  152. case 'String':
  153. return String(object);
  154. case 'Date':
  155. if (util.isNumber(object)) {
  156. return new Date(object);
  157. }
  158. if (object instanceof Date) {
  159. return new Date(object.valueOf());
  160. }
  161. else if (moment.isMoment(object)) {
  162. return new Date(object.valueOf());
  163. }
  164. if (util.isString(object)) {
  165. // parse ASP.Net Date pattern,
  166. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  167. // code from http://momentjs.com/
  168. match = ASPDateRegex.exec(object);
  169. if (match) {
  170. return new Date(Number(match[1])); // parse number
  171. }
  172. else {
  173. return moment(object).toDate(); // parse string
  174. }
  175. }
  176. else {
  177. throw new Error(
  178. 'Cannot cast object of type ' + util.getType(object) +
  179. ' to type Date');
  180. }
  181. case 'Moment':
  182. if (util.isNumber(object)) {
  183. return moment(object);
  184. }
  185. if (object instanceof Date) {
  186. return moment(object.valueOf());
  187. }
  188. else if (moment.isMoment(object)) {
  189. return moment.clone();
  190. }
  191. if (util.isString(object)) {
  192. // parse ASP.Net Date pattern,
  193. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  194. // code from http://momentjs.com/
  195. match = ASPDateRegex.exec(object);
  196. if (match) {
  197. return moment(Number(match[1])); // parse number
  198. }
  199. else {
  200. return moment(object); // parse string
  201. }
  202. }
  203. else {
  204. throw new Error(
  205. 'Cannot cast object of type ' + util.getType(object) +
  206. ' to type Date');
  207. }
  208. case 'ISODate':
  209. if (object instanceof Date) {
  210. return object.toISOString();
  211. }
  212. else if (moment.isMoment(object)) {
  213. return object.toDate().toISOString();
  214. }
  215. else if (util.isNumber(object) || util.isString(object)) {
  216. return moment(object).toDate().toISOString();
  217. }
  218. else {
  219. throw new Error(
  220. 'Cannot cast object of type ' + util.getType(object) +
  221. ' to type ISODate');
  222. }
  223. case 'ASPDate':
  224. if (object instanceof Date) {
  225. return '/Date(' + object.valueOf() + ')/';
  226. }
  227. else if (util.isNumber(object) || util.isString(object)) {
  228. return '/Date(' + moment(object).valueOf() + ')/';
  229. }
  230. else {
  231. throw new Error(
  232. 'Cannot cast object of type ' + util.getType(object) +
  233. ' to type ASPDate');
  234. }
  235. default:
  236. throw new Error('Cannot cast object of type ' + util.getType(object) +
  237. ' to type "' + type + '"');
  238. }
  239. };
  240. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  241. /**
  242. * Get the type of an object, for example util.getType([]) returns 'Array'
  243. * @param {*} object
  244. * @return {String} type
  245. */
  246. util.getType = function getType(object) {
  247. var type = typeof object;
  248. if (type == 'object') {
  249. if (object == null) {
  250. return 'null';
  251. }
  252. if (object instanceof Boolean) {
  253. return 'Boolean';
  254. }
  255. if (object instanceof Number) {
  256. return 'Number';
  257. }
  258. if (object instanceof String) {
  259. return 'String';
  260. }
  261. if (object instanceof Array) {
  262. return 'Array';
  263. }
  264. if (object instanceof Date) {
  265. return 'Date';
  266. }
  267. return 'Object';
  268. }
  269. else if (type == 'number') {
  270. return 'Number';
  271. }
  272. else if (type == 'boolean') {
  273. return 'Boolean';
  274. }
  275. else if (type == 'string') {
  276. return 'String';
  277. }
  278. return type;
  279. };
  280. /**
  281. * Retrieve the absolute left value of a DOM element
  282. * @param {Element} elem A dom element, for example a div
  283. * @return {number} left The absolute left position of this element
  284. * in the browser page.
  285. */
  286. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  287. var doc = document.documentElement;
  288. var body = document.body;
  289. var left = elem.offsetLeft;
  290. var e = elem.offsetParent;
  291. while (e != null && e != body && e != doc) {
  292. left += e.offsetLeft;
  293. left -= e.scrollLeft;
  294. e = e.offsetParent;
  295. }
  296. return left;
  297. };
  298. /**
  299. * Retrieve the absolute top value of a DOM element
  300. * @param {Element} elem A dom element, for example a div
  301. * @return {number} top The absolute top position of this element
  302. * in the browser page.
  303. */
  304. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  305. var doc = document.documentElement;
  306. var body = document.body;
  307. var top = elem.offsetTop;
  308. var e = elem.offsetParent;
  309. while (e != null && e != body && e != doc) {
  310. top += e.offsetTop;
  311. top -= e.scrollTop;
  312. e = e.offsetParent;
  313. }
  314. return top;
  315. };
  316. /**
  317. * Get the absolute, vertical mouse position from an event.
  318. * @param {Event} event
  319. * @return {Number} pageY
  320. */
  321. util.getPageY = function getPageY (event) {
  322. if ('pageY' in event) {
  323. return event.pageY;
  324. }
  325. else {
  326. var clientY;
  327. if (('targetTouches' in event) && event.targetTouches.length) {
  328. clientY = event.targetTouches[0].clientY;
  329. }
  330. else {
  331. clientY = event.clientY;
  332. }
  333. var doc = document.documentElement;
  334. var body = document.body;
  335. return clientY +
  336. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  337. ( doc && doc.clientTop || body && body.clientTop || 0 );
  338. }
  339. };
  340. /**
  341. * Get the absolute, horizontal mouse position from an event.
  342. * @param {Event} event
  343. * @return {Number} pageX
  344. */
  345. util.getPageX = function getPageX (event) {
  346. if ('pageY' in event) {
  347. return event.pageX;
  348. }
  349. else {
  350. var clientX;
  351. if (('targetTouches' in event) && event.targetTouches.length) {
  352. clientX = event.targetTouches[0].clientX;
  353. }
  354. else {
  355. clientX = event.clientX;
  356. }
  357. var doc = document.documentElement;
  358. var body = document.body;
  359. return clientX +
  360. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  361. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  362. }
  363. };
  364. /**
  365. * add a className to the given elements style
  366. * @param {Element} elem
  367. * @param {String} className
  368. */
  369. util.addClassName = function addClassName(elem, className) {
  370. var classes = elem.className.split(' ');
  371. if (classes.indexOf(className) == -1) {
  372. classes.push(className); // add the class to the array
  373. elem.className = classes.join(' ');
  374. }
  375. };
  376. /**
  377. * add a className to the given elements style
  378. * @param {Element} elem
  379. * @param {String} className
  380. */
  381. util.removeClassName = function removeClassname(elem, className) {
  382. var classes = elem.className.split(' ');
  383. var index = classes.indexOf(className);
  384. if (index != -1) {
  385. classes.splice(index, 1); // remove the class from the array
  386. elem.className = classes.join(' ');
  387. }
  388. };
  389. /**
  390. * For each method for both arrays and objects.
  391. * In case of an array, the built-in Array.forEach() is applied.
  392. * In case of an Object, the method loops over all properties of the object.
  393. * @param {Object | Array} object An Object or Array
  394. * @param {function} callback Callback method, called for each item in
  395. * the object or array with three parameters:
  396. * callback(value, index, object)
  397. */
  398. util.forEach = function forEach (object, callback) {
  399. var i,
  400. len;
  401. if (object instanceof Array) {
  402. // array
  403. for (i = 0, len = object.length; i < len; i++) {
  404. callback(object[i], i, object);
  405. }
  406. }
  407. else {
  408. // object
  409. for (i in object) {
  410. if (object.hasOwnProperty(i)) {
  411. callback(object[i], i, object);
  412. }
  413. }
  414. }
  415. };
  416. /**
  417. * Update a property in an object
  418. * @param {Object} object
  419. * @param {String} key
  420. * @param {*} value
  421. * @return {Boolean} changed
  422. */
  423. util.updateProperty = function updateProp (object, key, value) {
  424. if (object[key] !== value) {
  425. object[key] = value;
  426. return true;
  427. }
  428. else {
  429. return false;
  430. }
  431. };
  432. /**
  433. * Add and event listener. Works for all browsers
  434. * @param {Element} element An html element
  435. * @param {string} action The action, for example "click",
  436. * without the prefix "on"
  437. * @param {function} listener The callback function to be executed
  438. * @param {boolean} [useCapture]
  439. */
  440. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  441. if (element.addEventListener) {
  442. if (useCapture === undefined)
  443. useCapture = false;
  444. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  445. action = "DOMMouseScroll"; // For Firefox
  446. }
  447. element.addEventListener(action, listener, useCapture);
  448. } else {
  449. element.attachEvent("on" + action, listener); // IE browsers
  450. }
  451. };
  452. /**
  453. * Remove an event listener from an element
  454. * @param {Element} element An html dom element
  455. * @param {string} action The name of the event, for example "mousedown"
  456. * @param {function} listener The listener function
  457. * @param {boolean} [useCapture]
  458. */
  459. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  460. if (element.removeEventListener) {
  461. // non-IE browsers
  462. if (useCapture === undefined)
  463. useCapture = false;
  464. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  465. action = "DOMMouseScroll"; // For Firefox
  466. }
  467. element.removeEventListener(action, listener, useCapture);
  468. } else {
  469. // IE browsers
  470. element.detachEvent("on" + action, listener);
  471. }
  472. };
  473. /**
  474. * Get HTML element which is the target of the event
  475. * @param {Event} event
  476. * @return {Element} target element
  477. */
  478. util.getTarget = function getTarget(event) {
  479. // code from http://www.quirksmode.org/js/events_properties.html
  480. if (!event) {
  481. event = window.event;
  482. }
  483. var target;
  484. if (event.target) {
  485. target = event.target;
  486. }
  487. else if (event.srcElement) {
  488. target = event.srcElement;
  489. }
  490. if (target.nodeType != undefined && target.nodeType == 3) {
  491. // defeat Safari bug
  492. target = target.parentNode;
  493. }
  494. return target;
  495. };
  496. /**
  497. * Stop event propagation
  498. */
  499. util.stopPropagation = function stopPropagation(event) {
  500. if (!event)
  501. event = window.event;
  502. if (event.stopPropagation) {
  503. event.stopPropagation(); // non-IE browsers
  504. }
  505. else {
  506. event.cancelBubble = true; // IE browsers
  507. }
  508. };
  509. /**
  510. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  511. */
  512. util.preventDefault = function preventDefault (event) {
  513. if (!event)
  514. event = window.event;
  515. if (event.preventDefault) {
  516. event.preventDefault(); // non-IE browsers
  517. }
  518. else {
  519. event.returnValue = false; // IE browsers
  520. }
  521. };
  522. util.option = {};
  523. /**
  524. * Cast a value as boolean
  525. * @param {Boolean | function | undefined} value
  526. * @param {Boolean} [defaultValue]
  527. * @returns {Boolean} bool
  528. */
  529. util.option.asBoolean = function (value, defaultValue) {
  530. if (typeof value == 'function') {
  531. value = value();
  532. }
  533. if (value != null) {
  534. return (value != false);
  535. }
  536. return defaultValue || null;
  537. };
  538. /**
  539. * Cast a value as number
  540. * @param {Boolean | function | undefined} value
  541. * @param {Number} [defaultValue]
  542. * @returns {Number} number
  543. */
  544. util.option.asNumber = function (value, defaultValue) {
  545. if (typeof value == 'function') {
  546. value = value();
  547. }
  548. if (value != null) {
  549. return Number(value) || defaultValue || null;
  550. }
  551. return defaultValue || null;
  552. };
  553. /**
  554. * Cast a value as string
  555. * @param {String | function | undefined} value
  556. * @param {String} [defaultValue]
  557. * @returns {String} str
  558. */
  559. util.option.asString = function (value, defaultValue) {
  560. if (typeof value == 'function') {
  561. value = value();
  562. }
  563. if (value != null) {
  564. return String(value);
  565. }
  566. return defaultValue || null;
  567. };
  568. /**
  569. * Cast a size or location in pixels or a percentage
  570. * @param {String | Number | function | undefined} value
  571. * @param {String} [defaultValue]
  572. * @returns {String} size
  573. */
  574. util.option.asSize = function (value, defaultValue) {
  575. if (typeof value == 'function') {
  576. value = value();
  577. }
  578. if (util.isString(value)) {
  579. return value;
  580. }
  581. else if (util.isNumber(value)) {
  582. return value + 'px';
  583. }
  584. else {
  585. return defaultValue || null;
  586. }
  587. };
  588. /**
  589. * Cast a value as DOM element
  590. * @param {HTMLElement | function | undefined} value
  591. * @param {HTMLElement} [defaultValue]
  592. * @returns {HTMLElement | null} dom
  593. */
  594. util.option.asElement = function (value, defaultValue) {
  595. if (typeof value == 'function') {
  596. value = value();
  597. }
  598. return value || defaultValue || null;
  599. };
  600. /**
  601. * load css from text
  602. * @param {String} css Text containing css
  603. */
  604. util.loadCss = function (css) {
  605. if (typeof document === 'undefined') {
  606. return;
  607. }
  608. // get the script location, and built the css file name from the js file name
  609. // http://stackoverflow.com/a/2161748/1262753
  610. // var scripts = document.getElementsByTagName('script');
  611. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  612. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  613. // inject css
  614. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  615. var style = document.createElement('style');
  616. style.type = 'text/css';
  617. if (style.styleSheet){
  618. style.styleSheet.cssText = css;
  619. } else {
  620. style.appendChild(document.createTextNode(css));
  621. }
  622. document.getElementsByTagName('head')[0].appendChild(style);
  623. };
  624. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  625. // it here in that case.
  626. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  627. if(!Array.prototype.indexOf) {
  628. Array.prototype.indexOf = function(obj){
  629. for(var i = 0; i < this.length; i++){
  630. if(this[i] == obj){
  631. return i;
  632. }
  633. }
  634. return -1;
  635. };
  636. try {
  637. console.log("Warning: Ancient browser detected. Please update your browser");
  638. }
  639. catch (err) {
  640. }
  641. }
  642. // Internet Explorer 8 and older does not support Array.forEach, so we define
  643. // it here in that case.
  644. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  645. if (!Array.prototype.forEach) {
  646. Array.prototype.forEach = function(fn, scope) {
  647. for(var i = 0, len = this.length; i < len; ++i) {
  648. fn.call(scope || this, this[i], i, this);
  649. }
  650. }
  651. }
  652. // Internet Explorer 8 and older does not support Array.map, so we define it
  653. // here in that case.
  654. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  655. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  656. // Reference: http://es5.github.com/#x15.4.4.19
  657. if (!Array.prototype.map) {
  658. Array.prototype.map = function(callback, thisArg) {
  659. var T, A, k;
  660. if (this == null) {
  661. throw new TypeError(" this is null or not defined");
  662. }
  663. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  664. var O = Object(this);
  665. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  666. // 3. Let len be ToUint32(lenValue).
  667. var len = O.length >>> 0;
  668. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  669. // See: http://es5.github.com/#x9.11
  670. if (typeof callback !== "function") {
  671. throw new TypeError(callback + " is not a function");
  672. }
  673. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  674. if (thisArg) {
  675. T = thisArg;
  676. }
  677. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  678. // the standard built-in constructor with that name and len is the value of len.
  679. A = new Array(len);
  680. // 7. Let k be 0
  681. k = 0;
  682. // 8. Repeat, while k < len
  683. while(k < len) {
  684. var kValue, mappedValue;
  685. // a. Let Pk be ToString(k).
  686. // This is implicit for LHS operands of the in operator
  687. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  688. // This step can be combined with c
  689. // c. If kPresent is true, then
  690. if (k in O) {
  691. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  692. kValue = O[ k ];
  693. // ii. Let mappedValue be the result of calling the Call internal method of callback
  694. // with T as the this value and argument list containing kValue, k, and O.
  695. mappedValue = callback.call(T, kValue, k, O);
  696. // iii. Call the DefineOwnProperty internal method of A with arguments
  697. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  698. // and false.
  699. // In browsers that support Object.defineProperty, use the following:
  700. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  701. // For best browser support, use the following:
  702. A[ k ] = mappedValue;
  703. }
  704. // d. Increase k by 1.
  705. k++;
  706. }
  707. // 9. return A
  708. return A;
  709. };
  710. }
  711. // Internet Explorer 8 and older does not support Array.filter, so we define it
  712. // here in that case.
  713. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  714. if (!Array.prototype.filter) {
  715. Array.prototype.filter = function(fun /*, thisp */) {
  716. "use strict";
  717. if (this == null) {
  718. throw new TypeError();
  719. }
  720. var t = Object(this);
  721. var len = t.length >>> 0;
  722. if (typeof fun != "function") {
  723. throw new TypeError();
  724. }
  725. var res = [];
  726. var thisp = arguments[1];
  727. for (var i = 0; i < len; i++) {
  728. if (i in t) {
  729. var val = t[i]; // in case fun mutates this
  730. if (fun.call(thisp, val, i, t))
  731. res.push(val);
  732. }
  733. }
  734. return res;
  735. };
  736. }
  737. // Internet Explorer 8 and older does not support Object.keys, so we define it
  738. // here in that case.
  739. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  740. if (!Object.keys) {
  741. Object.keys = (function () {
  742. var hasOwnProperty = Object.prototype.hasOwnProperty,
  743. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  744. dontEnums = [
  745. 'toString',
  746. 'toLocaleString',
  747. 'valueOf',
  748. 'hasOwnProperty',
  749. 'isPrototypeOf',
  750. 'propertyIsEnumerable',
  751. 'constructor'
  752. ],
  753. dontEnumsLength = dontEnums.length;
  754. return function (obj) {
  755. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  756. throw new TypeError('Object.keys called on non-object');
  757. }
  758. var result = [];
  759. for (var prop in obj) {
  760. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  761. }
  762. if (hasDontEnumBug) {
  763. for (var i=0; i < dontEnumsLength; i++) {
  764. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  765. }
  766. }
  767. return result;
  768. }
  769. })()
  770. }
  771. // Internet Explorer 8 and older does not support Array.isArray,
  772. // so we define it here in that case.
  773. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  774. if(!Array.isArray) {
  775. Array.isArray = function (vArg) {
  776. return Object.prototype.toString.call(vArg) === "[object Array]";
  777. };
  778. }
  779. // Internet Explorer 8 and older does not support Function.bind,
  780. // so we define it here in that case.
  781. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  782. if (!Function.prototype.bind) {
  783. Function.prototype.bind = function (oThis) {
  784. if (typeof this !== "function") {
  785. // closest thing possible to the ECMAScript 5 internal IsCallable function
  786. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  787. }
  788. var aArgs = Array.prototype.slice.call(arguments, 1),
  789. fToBind = this,
  790. fNOP = function () {},
  791. fBound = function () {
  792. return fToBind.apply(this instanceof fNOP && oThis
  793. ? this
  794. : oThis,
  795. aArgs.concat(Array.prototype.slice.call(arguments)));
  796. };
  797. fNOP.prototype = this.prototype;
  798. fBound.prototype = new fNOP();
  799. return fBound;
  800. };
  801. }
  802. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  803. if (!Object.create) {
  804. Object.create = function (o) {
  805. if (arguments.length > 1) {
  806. throw new Error('Object.create implementation only accepts the first parameter.');
  807. }
  808. function F() {}
  809. F.prototype = o;
  810. return new F();
  811. };
  812. }
  813. /**
  814. * Event listener (singleton)
  815. */
  816. // TODO: replace usage of the event listener for the EventBus
  817. var events = {
  818. 'listeners': [],
  819. /**
  820. * Find a single listener by its object
  821. * @param {Object} object
  822. * @return {Number} index -1 when not found
  823. */
  824. 'indexOf': function (object) {
  825. var listeners = this.listeners;
  826. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  827. var listener = listeners[i];
  828. if (listener && listener.object == object) {
  829. return i;
  830. }
  831. }
  832. return -1;
  833. },
  834. /**
  835. * Add an event listener
  836. * @param {Object} object
  837. * @param {String} event The name of an event, for example 'select'
  838. * @param {function} callback The callback method, called when the
  839. * event takes place
  840. */
  841. 'addListener': function (object, event, callback) {
  842. var index = this.indexOf(object);
  843. var listener = this.listeners[index];
  844. if (!listener) {
  845. listener = {
  846. 'object': object,
  847. 'events': {}
  848. };
  849. this.listeners.push(listener);
  850. }
  851. var callbacks = listener.events[event];
  852. if (!callbacks) {
  853. callbacks = [];
  854. listener.events[event] = callbacks;
  855. }
  856. // add the callback if it does not yet exist
  857. if (callbacks.indexOf(callback) == -1) {
  858. callbacks.push(callback);
  859. }
  860. },
  861. /**
  862. * Remove an event listener
  863. * @param {Object} object
  864. * @param {String} event The name of an event, for example 'select'
  865. * @param {function} callback The registered callback method
  866. */
  867. 'removeListener': function (object, event, callback) {
  868. var index = this.indexOf(object);
  869. var listener = this.listeners[index];
  870. if (listener) {
  871. var callbacks = listener.events[event];
  872. if (callbacks) {
  873. index = callbacks.indexOf(callback);
  874. if (index != -1) {
  875. callbacks.splice(index, 1);
  876. }
  877. // remove the array when empty
  878. if (callbacks.length == 0) {
  879. delete listener.events[event];
  880. }
  881. }
  882. // count the number of registered events. remove listener when empty
  883. var count = 0;
  884. var events = listener.events;
  885. for (var e in events) {
  886. if (events.hasOwnProperty(e)) {
  887. count++;
  888. }
  889. }
  890. if (count == 0) {
  891. delete this.listeners[index];
  892. }
  893. }
  894. },
  895. /**
  896. * Remove all registered event listeners
  897. */
  898. 'removeAllListeners': function () {
  899. this.listeners = [];
  900. },
  901. /**
  902. * Trigger an event. All registered event handlers will be called
  903. * @param {Object} object
  904. * @param {String} event
  905. * @param {Object} properties (optional)
  906. */
  907. 'trigger': function (object, event, properties) {
  908. var index = this.indexOf(object);
  909. var listener = this.listeners[index];
  910. if (listener) {
  911. var callbacks = listener.events[event];
  912. if (callbacks) {
  913. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  914. callbacks[i](properties);
  915. }
  916. }
  917. }
  918. }
  919. };
  920. /**
  921. * @constructor TimeStep
  922. * The class TimeStep is an iterator for dates. You provide a start date and an
  923. * end date. The class itself determines the best scale (step size) based on the
  924. * provided start Date, end Date, and minimumStep.
  925. *
  926. * If minimumStep is provided, the step size is chosen as close as possible
  927. * to the minimumStep but larger than minimumStep. If minimumStep is not
  928. * provided, the scale is set to 1 DAY.
  929. * The minimumStep should correspond with the onscreen size of about 6 characters
  930. *
  931. * Alternatively, you can set a scale by hand.
  932. * After creation, you can initialize the class by executing first(). Then you
  933. * can iterate from the start date to the end date via next(). You can check if
  934. * the end date is reached with the function hasNext(). After each step, you can
  935. * retrieve the current date via getCurrent().
  936. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  937. * days, to years.
  938. *
  939. * Version: 1.2
  940. *
  941. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  942. * or new Date(2010, 9, 21, 23, 45, 00)
  943. * @param {Date} [end] The end date
  944. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  945. */
  946. TimeStep = function(start, end, minimumStep) {
  947. // variables
  948. this.current = new Date();
  949. this._start = new Date();
  950. this._end = new Date();
  951. this.autoScale = true;
  952. this.scale = TimeStep.SCALE.DAY;
  953. this.step = 1;
  954. // initialize the range
  955. this.setRange(start, end, minimumStep);
  956. };
  957. /// enum scale
  958. TimeStep.SCALE = {
  959. MILLISECOND: 1,
  960. SECOND: 2,
  961. MINUTE: 3,
  962. HOUR: 4,
  963. DAY: 5,
  964. WEEKDAY: 6,
  965. MONTH: 7,
  966. YEAR: 8
  967. };
  968. /**
  969. * Set a new range
  970. * If minimumStep is provided, the step size is chosen as close as possible
  971. * to the minimumStep but larger than minimumStep. If minimumStep is not
  972. * provided, the scale is set to 1 DAY.
  973. * The minimumStep should correspond with the onscreen size of about 6 characters
  974. * @param {Date} [start] The start date and time.
  975. * @param {Date} [end] The end date and time.
  976. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  977. */
  978. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  979. if (!(start instanceof Date) || !(end instanceof Date)) {
  980. //throw "No legal start or end date in method setRange";
  981. return;
  982. }
  983. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  984. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  985. if (this.autoScale) {
  986. this.setMinimumStep(minimumStep);
  987. }
  988. };
  989. /**
  990. * Set the range iterator to the start date.
  991. */
  992. TimeStep.prototype.first = function() {
  993. this.current = new Date(this._start.valueOf());
  994. this.roundToMinor();
  995. };
  996. /**
  997. * Round the current date to the first minor date value
  998. * This must be executed once when the current date is set to start Date
  999. */
  1000. TimeStep.prototype.roundToMinor = function() {
  1001. // round to floor
  1002. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  1003. //noinspection FallthroughInSwitchStatementJS
  1004. switch (this.scale) {
  1005. case TimeStep.SCALE.YEAR:
  1006. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  1007. this.current.setMonth(0);
  1008. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  1009. case TimeStep.SCALE.DAY: // intentional fall through
  1010. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  1011. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  1012. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  1013. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  1014. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  1015. }
  1016. if (this.step != 1) {
  1017. // round down to the first minor value that is a multiple of the current step size
  1018. switch (this.scale) {
  1019. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  1020. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  1021. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  1022. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  1023. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1024. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  1025. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  1026. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  1027. default: break;
  1028. }
  1029. }
  1030. };
  1031. /**
  1032. * Check if the there is a next step
  1033. * @return {boolean} true if the current date has not passed the end date
  1034. */
  1035. TimeStep.prototype.hasNext = function () {
  1036. return (this.current.valueOf() <= this._end.valueOf());
  1037. };
  1038. /**
  1039. * Do the next step
  1040. */
  1041. TimeStep.prototype.next = function() {
  1042. var prev = this.current.valueOf();
  1043. // Two cases, needed to prevent issues with switching daylight savings
  1044. // (end of March and end of October)
  1045. if (this.current.getMonth() < 6) {
  1046. switch (this.scale) {
  1047. case TimeStep.SCALE.MILLISECOND:
  1048. this.current = new Date(this.current.valueOf() + this.step); break;
  1049. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  1050. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  1051. case TimeStep.SCALE.HOUR:
  1052. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  1053. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  1054. var h = this.current.getHours();
  1055. this.current.setHours(h - (h % this.step));
  1056. break;
  1057. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1058. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  1059. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  1060. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  1061. default: break;
  1062. }
  1063. }
  1064. else {
  1065. switch (this.scale) {
  1066. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  1067. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  1068. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  1069. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  1070. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1071. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  1072. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  1073. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  1074. default: break;
  1075. }
  1076. }
  1077. if (this.step != 1) {
  1078. // round down to the correct major value
  1079. switch (this.scale) {
  1080. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  1081. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  1082. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  1083. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  1084. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1085. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  1086. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  1087. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  1088. default: break;
  1089. }
  1090. }
  1091. // safety mechanism: if current time is still unchanged, move to the end
  1092. if (this.current.valueOf() == prev) {
  1093. this.current = new Date(this._end.valueOf());
  1094. }
  1095. };
  1096. /**
  1097. * Get the current datetime
  1098. * @return {Date} current The current date
  1099. */
  1100. TimeStep.prototype.getCurrent = function() {
  1101. return this.current;
  1102. };
  1103. /**
  1104. * Set a custom scale. Autoscaling will be disabled.
  1105. * For example setScale(SCALE.MINUTES, 5) will result
  1106. * in minor steps of 5 minutes, and major steps of an hour.
  1107. *
  1108. * @param {TimeStep.SCALE} newScale
  1109. * A scale. Choose from SCALE.MILLISECOND,
  1110. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  1111. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  1112. * SCALE.YEAR.
  1113. * @param {Number} newStep A step size, by default 1. Choose for
  1114. * example 1, 2, 5, or 10.
  1115. */
  1116. TimeStep.prototype.setScale = function(newScale, newStep) {
  1117. this.scale = newScale;
  1118. if (newStep > 0) {
  1119. this.step = newStep;
  1120. }
  1121. this.autoScale = false;
  1122. };
  1123. /**
  1124. * Enable or disable autoscaling
  1125. * @param {boolean} enable If true, autoascaling is set true
  1126. */
  1127. TimeStep.prototype.setAutoScale = function (enable) {
  1128. this.autoScale = enable;
  1129. };
  1130. /**
  1131. * Automatically determine the scale that bests fits the provided minimum step
  1132. * @param {Number} [minimumStep] The minimum step size in milliseconds
  1133. */
  1134. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  1135. if (minimumStep == undefined) {
  1136. return;
  1137. }
  1138. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  1139. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  1140. var stepDay = (1000 * 60 * 60 * 24);
  1141. var stepHour = (1000 * 60 * 60);
  1142. var stepMinute = (1000 * 60);
  1143. var stepSecond = (1000);
  1144. var stepMillisecond= (1);
  1145. // find the smallest step that is larger than the provided minimumStep
  1146. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  1147. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  1148. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  1149. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  1150. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  1151. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  1152. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  1153. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  1154. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  1155. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  1156. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  1157. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  1158. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  1159. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  1160. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  1161. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  1162. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  1163. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  1164. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  1165. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  1166. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  1167. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  1168. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  1169. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  1170. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  1171. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  1172. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  1173. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  1174. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  1175. };
  1176. /**
  1177. * Snap a date to a rounded value. The snap intervals are dependent on the
  1178. * current scale and step.
  1179. * @param {Date} date the date to be snapped
  1180. */
  1181. TimeStep.prototype.snap = function(date) {
  1182. if (this.scale == TimeStep.SCALE.YEAR) {
  1183. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  1184. date.setFullYear(Math.round(year / this.step) * this.step);
  1185. date.setMonth(0);
  1186. date.setDate(0);
  1187. date.setHours(0);
  1188. date.setMinutes(0);
  1189. date.setSeconds(0);
  1190. date.setMilliseconds(0);
  1191. }
  1192. else if (this.scale == TimeStep.SCALE.MONTH) {
  1193. if (date.getDate() > 15) {
  1194. date.setDate(1);
  1195. date.setMonth(date.getMonth() + 1);
  1196. // important: first set Date to 1, after that change the month.
  1197. }
  1198. else {
  1199. date.setDate(1);
  1200. }
  1201. date.setHours(0);
  1202. date.setMinutes(0);
  1203. date.setSeconds(0);
  1204. date.setMilliseconds(0);
  1205. }
  1206. else if (this.scale == TimeStep.SCALE.DAY ||
  1207. this.scale == TimeStep.SCALE.WEEKDAY) {
  1208. //noinspection FallthroughInSwitchStatementJS
  1209. switch (this.step) {
  1210. case 5:
  1211. case 2:
  1212. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  1213. default:
  1214. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  1215. }
  1216. date.setMinutes(0);
  1217. date.setSeconds(0);
  1218. date.setMilliseconds(0);
  1219. }
  1220. else if (this.scale == TimeStep.SCALE.HOUR) {
  1221. switch (this.step) {
  1222. case 4:
  1223. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  1224. default:
  1225. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  1226. }
  1227. date.setSeconds(0);
  1228. date.setMilliseconds(0);
  1229. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  1230. //noinspection FallthroughInSwitchStatementJS
  1231. switch (this.step) {
  1232. case 15:
  1233. case 10:
  1234. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  1235. date.setSeconds(0);
  1236. break;
  1237. case 5:
  1238. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  1239. default:
  1240. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  1241. }
  1242. date.setMilliseconds(0);
  1243. }
  1244. else if (this.scale == TimeStep.SCALE.SECOND) {
  1245. //noinspection FallthroughInSwitchStatementJS
  1246. switch (this.step) {
  1247. case 15:
  1248. case 10:
  1249. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  1250. date.setMilliseconds(0);
  1251. break;
  1252. case 5:
  1253. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  1254. default:
  1255. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  1256. }
  1257. }
  1258. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  1259. var step = this.step > 5 ? this.step / 2 : 1;
  1260. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  1261. }
  1262. };
  1263. /**
  1264. * Check if the current value is a major value (for example when the step
  1265. * is DAY, a major value is each first day of the MONTH)
  1266. * @return {boolean} true if current date is major, else false.
  1267. */
  1268. TimeStep.prototype.isMajor = function() {
  1269. switch (this.scale) {
  1270. case TimeStep.SCALE.MILLISECOND:
  1271. return (this.current.getMilliseconds() == 0);
  1272. case TimeStep.SCALE.SECOND:
  1273. return (this.current.getSeconds() == 0);
  1274. case TimeStep.SCALE.MINUTE:
  1275. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  1276. // Note: this is no bug. Major label is equal for both minute and hour scale
  1277. case TimeStep.SCALE.HOUR:
  1278. return (this.current.getHours() == 0);
  1279. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  1280. case TimeStep.SCALE.DAY:
  1281. return (this.current.getDate() == 1);
  1282. case TimeStep.SCALE.MONTH:
  1283. return (this.current.getMonth() == 0);
  1284. case TimeStep.SCALE.YEAR:
  1285. return false;
  1286. default:
  1287. return false;
  1288. }
  1289. };
  1290. /**
  1291. * Returns formatted text for the minor axislabel, depending on the current
  1292. * date and the scale. For example when scale is MINUTE, the current time is
  1293. * formatted as "hh:mm".
  1294. * @param {Date} [date] custom date. if not provided, current date is taken
  1295. */
  1296. TimeStep.prototype.getLabelMinor = function(date) {
  1297. if (date == undefined) {
  1298. date = this.current;
  1299. }
  1300. switch (this.scale) {
  1301. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  1302. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  1303. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  1304. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  1305. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  1306. case TimeStep.SCALE.DAY: return moment(date).format('D');
  1307. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  1308. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  1309. default: return '';
  1310. }
  1311. };
  1312. /**
  1313. * Returns formatted text for the major axis label, depending on the current
  1314. * date and the scale. For example when scale is MINUTE, the major scale is
  1315. * hours, and the hour will be formatted as "hh".
  1316. * @param {Date} [date] custom date. if not provided, current date is taken
  1317. */
  1318. TimeStep.prototype.getLabelMajor = function(date) {
  1319. if (date == undefined) {
  1320. date = this.current;
  1321. }
  1322. //noinspection FallthroughInSwitchStatementJS
  1323. switch (this.scale) {
  1324. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  1325. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  1326. case TimeStep.SCALE.MINUTE:
  1327. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  1328. case TimeStep.SCALE.WEEKDAY:
  1329. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  1330. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  1331. case TimeStep.SCALE.YEAR: return '';
  1332. default: return '';
  1333. }
  1334. };
  1335. /**
  1336. * DataSet
  1337. *
  1338. * Usage:
  1339. * var dataSet = new DataSet({
  1340. * fieldId: '_id',
  1341. * fieldTypes: {
  1342. * // ...
  1343. * }
  1344. * });
  1345. *
  1346. * dataSet.add(item);
  1347. * dataSet.add(data);
  1348. * dataSet.update(item);
  1349. * dataSet.update(data);
  1350. * dataSet.remove(id);
  1351. * dataSet.remove(ids);
  1352. * var data = dataSet.get();
  1353. * var data = dataSet.get(id);
  1354. * var data = dataSet.get(ids);
  1355. * var data = dataSet.get(ids, options, data);
  1356. * dataSet.clear();
  1357. *
  1358. * A data set can:
  1359. * - add/remove/update data
  1360. * - gives triggers upon changes in the data
  1361. * - can import/export data in various data formats
  1362. *
  1363. * @param {Object} [options] Available options:
  1364. * {String} fieldId Field name of the id in the
  1365. * items, 'id' by default.
  1366. * {Object.<String, String} fieldTypes
  1367. * A map with field names as key,
  1368. * and the field type as value.
  1369. * @constructor DataSet
  1370. */
  1371. function DataSet (options) {
  1372. this.id = util.randomUUID();
  1373. this.options = options || {};
  1374. this.data = {}; // map with data indexed by id
  1375. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1376. this.fieldTypes = {}; // field types by field name
  1377. if (this.options.fieldTypes) {
  1378. for (var field in this.options.fieldTypes) {
  1379. if (this.options.fieldTypes.hasOwnProperty(field)) {
  1380. var value = this.options.fieldTypes[field];
  1381. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1382. this.fieldTypes[field] = 'Date';
  1383. }
  1384. else {
  1385. this.fieldTypes[field] = value;
  1386. }
  1387. }
  1388. }
  1389. }
  1390. // event subscribers
  1391. this.subscribers = {};
  1392. this.internalIds = {}; // internally generated id's
  1393. }
  1394. /**
  1395. * Subscribe to an event, add an event listener
  1396. * @param {String} event Event name. Available events: 'put', 'update',
  1397. * 'remove'
  1398. * @param {function} callback Callback method. Called with three parameters:
  1399. * {String} event
  1400. * {Object | null} params
  1401. * {String} senderId
  1402. * @param {String} [id] Optional id for the sender, used to filter
  1403. * events triggered by the sender itself.
  1404. */
  1405. DataSet.prototype.subscribe = function (event, callback, id) {
  1406. var subscribers = this.subscribers[event];
  1407. if (!subscribers) {
  1408. subscribers = [];
  1409. this.subscribers[event] = subscribers;
  1410. }
  1411. subscribers.push({
  1412. id: id ? String(id) : null,
  1413. callback: callback
  1414. });
  1415. };
  1416. /**
  1417. * Unsubscribe from an event, remove an event listener
  1418. * @param {String} event
  1419. * @param {function} callback
  1420. */
  1421. DataSet.prototype.unsubscribe = function (event, callback) {
  1422. var subscribers = this.subscribers[event];
  1423. if (subscribers) {
  1424. this.subscribers[event] = subscribers.filter(function (listener) {
  1425. return (listener.callback != callback);
  1426. });
  1427. }
  1428. };
  1429. /**
  1430. * Trigger an event
  1431. * @param {String} event
  1432. * @param {Object | null} params
  1433. * @param {String} [senderId] Optional id of the sender.
  1434. * @private
  1435. */
  1436. DataSet.prototype._trigger = function (event, params, senderId) {
  1437. if (event == '*') {
  1438. throw new Error('Cannot trigger event *');
  1439. }
  1440. var subscribers = [];
  1441. if (event in this.subscribers) {
  1442. subscribers = subscribers.concat(this.subscribers[event]);
  1443. }
  1444. if ('*' in this.subscribers) {
  1445. subscribers = subscribers.concat(this.subscribers['*']);
  1446. }
  1447. for (var i = 0; i < subscribers.length; i++) {
  1448. var subscriber = subscribers[i];
  1449. if (subscriber.callback) {
  1450. subscriber.callback(event, params, senderId || null);
  1451. }
  1452. }
  1453. };
  1454. /**
  1455. * Add data.
  1456. * Adding an item will fail when there already is an item with the same id.
  1457. * @param {Object | Array | DataTable} data
  1458. * @param {String} [senderId] Optional sender id
  1459. */
  1460. DataSet.prototype.add = function (data, senderId) {
  1461. var addedItems = [],
  1462. id,
  1463. me = this;
  1464. if (data instanceof Array) {
  1465. // Array
  1466. for (var i = 0, len = data.length; i < len; i++) {
  1467. id = me._addItem(data[i]);
  1468. addedItems.push(id);
  1469. }
  1470. }
  1471. else if (util.isDataTable(data)) {
  1472. // Google DataTable
  1473. var columns = this._getColumnNames(data);
  1474. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1475. var item = {};
  1476. for (var col = 0, cols = columns.length; col < cols; col++) {
  1477. var field = columns[col];
  1478. item[field] = data.getValue(row, col);
  1479. }
  1480. id = me._addItem(item);
  1481. addedItems.push(id);
  1482. }
  1483. }
  1484. else if (data instanceof Object) {
  1485. // Single item
  1486. id = me._addItem(data);
  1487. addedItems.push(id);
  1488. }
  1489. else {
  1490. throw new Error('Unknown dataType');
  1491. }
  1492. if (addedItems.length) {
  1493. this._trigger('add', {items: addedItems}, senderId);
  1494. }
  1495. };
  1496. /**
  1497. * Update existing items. When an item does not exist, it will be created
  1498. * @param {Object | Array | DataTable} data
  1499. * @param {String} [senderId] Optional sender id
  1500. */
  1501. DataSet.prototype.update = function (data, senderId) {
  1502. var addedItems = [],
  1503. updatedItems = [],
  1504. me = this,
  1505. fieldId = me.fieldId;
  1506. var addOrUpdate = function (item) {
  1507. var id = item[fieldId];
  1508. if (me.data[id]) {
  1509. // update item
  1510. id = me._updateItem(item);
  1511. updatedItems.push(id);
  1512. }
  1513. else {
  1514. // add new item
  1515. id = me._addItem(item);
  1516. addedItems.push(id);
  1517. }
  1518. };
  1519. if (data instanceof Array) {
  1520. // Array
  1521. for (var i = 0, len = data.length; i < len; i++) {
  1522. addOrUpdate(data[i]);
  1523. }
  1524. }
  1525. else if (util.isDataTable(data)) {
  1526. // Google DataTable
  1527. var columns = this._getColumnNames(data);
  1528. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1529. var item = {};
  1530. for (var col = 0, cols = columns.length; col < cols; col++) {
  1531. var field = columns[col];
  1532. item[field] = data.getValue(row, col);
  1533. }
  1534. addOrUpdate(item);
  1535. }
  1536. }
  1537. else if (data instanceof Object) {
  1538. // Single item
  1539. addOrUpdate(data);
  1540. }
  1541. else {
  1542. throw new Error('Unknown dataType');
  1543. }
  1544. if (addedItems.length) {
  1545. this._trigger('add', {items: addedItems}, senderId);
  1546. }
  1547. if (updatedItems.length) {
  1548. this._trigger('update', {items: updatedItems}, senderId);
  1549. }
  1550. };
  1551. /**
  1552. * Get a data item or multiple items.
  1553. *
  1554. * Usage:
  1555. *
  1556. * get()
  1557. * get(options: Object)
  1558. * get(options: Object, data: Array | DataTable)
  1559. *
  1560. * get(id: Number | String)
  1561. * get(id: Number | String, options: Object)
  1562. * get(id: Number | String, options: Object, data: Array | DataTable)
  1563. *
  1564. * get(ids: Number[] | String[])
  1565. * get(ids: Number[] | String[], options: Object)
  1566. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1567. *
  1568. * Where:
  1569. *
  1570. * {Number | String} id The id of an item
  1571. * {Number[] | String{}} ids An array with ids of items
  1572. * {Object} options An Object with options. Available options:
  1573. * {String} [type] Type of data to be returned. Can
  1574. * be 'DataTable' or 'Array' (default)
  1575. * {Object.<String, String>} [fieldTypes]
  1576. * {String[]} [fields] field names to be returned
  1577. * {function} [filter] filter items
  1578. * {String | function} [order] Order the items by
  1579. * a field name or custom sort function.
  1580. * {Array | DataTable} [data] If provided, items will be appended to this
  1581. * array or table. Required in case of Google
  1582. * DataTable.
  1583. *
  1584. * @throws Error
  1585. */
  1586. DataSet.prototype.get = function (args) {
  1587. var me = this;
  1588. // parse the arguments
  1589. var id, ids, options, data;
  1590. var firstType = util.getType(arguments[0]);
  1591. if (firstType == 'String' || firstType == 'Number') {
  1592. // get(id [, options] [, data])
  1593. id = arguments[0];
  1594. options = arguments[1];
  1595. data = arguments[2];
  1596. }
  1597. else if (firstType == 'Array') {
  1598. // get(ids [, options] [, data])
  1599. ids = arguments[0];
  1600. options = arguments[1];
  1601. data = arguments[2];
  1602. }
  1603. else {
  1604. // get([, options] [, data])
  1605. options = arguments[0];
  1606. data = arguments[1];
  1607. }
  1608. // determine the return type
  1609. var type;
  1610. if (options && options.type) {
  1611. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1612. if (data && (type != util.getType(data))) {
  1613. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1614. 'does not correspond with specified options.type (' + options.type + ')');
  1615. }
  1616. if (type == 'DataTable' && !util.isDataTable(data)) {
  1617. throw new Error('Parameter "data" must be a DataTable ' +
  1618. 'when options.type is "DataTable"');
  1619. }
  1620. }
  1621. else if (data) {
  1622. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1623. }
  1624. else {
  1625. type = 'Array';
  1626. }
  1627. // build options
  1628. var fieldTypes = options && options.fieldTypes || this.options.fieldTypes;
  1629. var filter = options && options.filter;
  1630. var items = [], item, itemId, i, len;
  1631. // cast items
  1632. if (id != undefined) {
  1633. // return a single item
  1634. item = me._getItem(id, fieldTypes);
  1635. if (filter && !filter(item)) {
  1636. item = null;
  1637. }
  1638. }
  1639. else if (ids != undefined) {
  1640. // return a subset of items
  1641. for (i = 0, len = ids.length; i < len; i++) {
  1642. item = me._getItem(ids[i], fieldTypes);
  1643. if (!filter || filter(item)) {
  1644. items.push(item);
  1645. }
  1646. }
  1647. }
  1648. else {
  1649. // return all items
  1650. for (itemId in this.data) {
  1651. if (this.data.hasOwnProperty(itemId)) {
  1652. item = me._getItem(itemId, fieldTypes);
  1653. if (!filter || filter(item)) {
  1654. items.push(item);
  1655. }
  1656. }
  1657. }
  1658. }
  1659. // order the results
  1660. if (options && options.order && id == undefined) {
  1661. this._sort(items, options.order);
  1662. }
  1663. // filter fields of the items
  1664. if (options && options.fields) {
  1665. var fields = options.fields;
  1666. if (id != undefined) {
  1667. item = this._filterFields(item, fields);
  1668. }
  1669. else {
  1670. for (i = 0, len = items.length; i < len; i++) {
  1671. items[i] = this._filterFields(items[i], fields);
  1672. }
  1673. }
  1674. }
  1675. // return the results
  1676. if (type == 'DataTable') {
  1677. var columns = this._getColumnNames(data);
  1678. if (id != undefined) {
  1679. // append a single item to the data table
  1680. me._appendRow(data, columns, item);
  1681. }
  1682. else {
  1683. // copy the items to the provided data table
  1684. for (i = 0, len = items.length; i < len; i++) {
  1685. me._appendRow(data, columns, items[i]);
  1686. }
  1687. }
  1688. return data;
  1689. }
  1690. else {
  1691. // return an array
  1692. if (id != undefined) {
  1693. // a single item
  1694. return item;
  1695. }
  1696. else {
  1697. // multiple items
  1698. if (data) {
  1699. // copy the items to the provided array
  1700. for (i = 0, len = items.length; i < len; i++) {
  1701. data.push(items[i]);
  1702. }
  1703. return data;
  1704. }
  1705. else {
  1706. // just return our array
  1707. return items;
  1708. }
  1709. }
  1710. }
  1711. };
  1712. /**
  1713. * Get ids of all items or from a filtered set of items.
  1714. * @param {Object} [options] An Object with options. Available options:
  1715. * {function} [filter] filter items
  1716. * {String | function} [order] Order the items by
  1717. * a field name or custom sort function.
  1718. * @return {Array} ids
  1719. */
  1720. DataSet.prototype.getIds = function (options) {
  1721. var data = this.data,
  1722. filter = options && options.filter,
  1723. order = options && options.order,
  1724. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1725. i,
  1726. len,
  1727. id,
  1728. item,
  1729. items,
  1730. ids = [];
  1731. if (filter) {
  1732. // get filtered items
  1733. if (order) {
  1734. // create ordered list
  1735. items = [];
  1736. for (id in data) {
  1737. if (data.hasOwnProperty(id)) {
  1738. item = this._getItem(id, fieldTypes);
  1739. if (filter(item)) {
  1740. items.push(item);
  1741. }
  1742. }
  1743. }
  1744. this._sort(items, order);
  1745. for (i = 0, len = items.length; i < len; i++) {
  1746. ids[i] = items[i][this.fieldId];
  1747. }
  1748. }
  1749. else {
  1750. // create unordered list
  1751. for (id in data) {
  1752. if (data.hasOwnProperty(id)) {
  1753. item = this._getItem(id, fieldTypes);
  1754. if (filter(item)) {
  1755. ids.push(item[this.fieldId]);
  1756. }
  1757. }
  1758. }
  1759. }
  1760. }
  1761. else {
  1762. // get all items
  1763. if (order) {
  1764. // create an ordered list
  1765. items = [];
  1766. for (id in data) {
  1767. if (data.hasOwnProperty(id)) {
  1768. items.push(data[id]);
  1769. }
  1770. }
  1771. this._sort(items, order);
  1772. for (i = 0, len = items.length; i < len; i++) {
  1773. ids[i] = items[i][this.fieldId];
  1774. }
  1775. }
  1776. else {
  1777. // create unordered list
  1778. for (id in data) {
  1779. if (data.hasOwnProperty(id)) {
  1780. item = data[id];
  1781. ids.push(item[this.fieldId]);
  1782. }
  1783. }
  1784. }
  1785. }
  1786. return ids;
  1787. };
  1788. /**
  1789. * Execute a callback function for every item in the dataset.
  1790. * The order of the items is not determined.
  1791. * @param {function} callback
  1792. * @param {Object} [options] Available options:
  1793. * {Object.<String, String>} [fieldTypes]
  1794. * {String[]} [fields] filter fields
  1795. * {function} [filter] filter items
  1796. * {String | function} [order] Order the items by
  1797. * a field name or custom sort function.
  1798. */
  1799. DataSet.prototype.forEach = function (callback, options) {
  1800. var filter = options && options.filter,
  1801. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1802. data = this.data,
  1803. item,
  1804. id;
  1805. if (options && options.order) {
  1806. // execute forEach on ordered list
  1807. var items = this.get(options);
  1808. for (var i = 0, len = items.length; i < len; i++) {
  1809. item = items[i];
  1810. id = item[this.fieldId];
  1811. callback(item, id);
  1812. }
  1813. }
  1814. else {
  1815. // unordered
  1816. for (id in data) {
  1817. if (data.hasOwnProperty(id)) {
  1818. item = this._getItem(id, fieldTypes);
  1819. if (!filter || filter(item)) {
  1820. callback(item, id);
  1821. }
  1822. }
  1823. }
  1824. }
  1825. };
  1826. /**
  1827. * Map every item in the dataset.
  1828. * @param {function} callback
  1829. * @param {Object} [options] Available options:
  1830. * {Object.<String, String>} [fieldTypes]
  1831. * {String[]} [fields] filter fields
  1832. * {function} [filter] filter items
  1833. * {String | function} [order] Order the items by
  1834. * a field name or custom sort function.
  1835. * @return {Object[]} mappedItems
  1836. */
  1837. DataSet.prototype.map = function (callback, options) {
  1838. var filter = options && options.filter,
  1839. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1840. mappedItems = [],
  1841. data = this.data,
  1842. item;
  1843. // cast and filter items
  1844. for (var id in data) {
  1845. if (data.hasOwnProperty(id)) {
  1846. item = this._getItem(id, fieldTypes);
  1847. if (!filter || filter(item)) {
  1848. mappedItems.push(callback(item, id));
  1849. }
  1850. }
  1851. }
  1852. // order items
  1853. if (options && options.order) {
  1854. this._sort(mappedItems, options.order);
  1855. }
  1856. return mappedItems;
  1857. };
  1858. /**
  1859. * Filter the fields of an item
  1860. * @param {Object} item
  1861. * @param {String[]} fields Field names
  1862. * @return {Object} filteredItem
  1863. * @private
  1864. */
  1865. DataSet.prototype._filterFields = function (item, fields) {
  1866. var filteredItem = {};
  1867. for (var field in item) {
  1868. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1869. filteredItem[field] = item[field];
  1870. }
  1871. }
  1872. return filteredItem;
  1873. };
  1874. /**
  1875. * Sort the provided array with items
  1876. * @param {Object[]} items
  1877. * @param {String | function} order A field name or custom sort function.
  1878. * @private
  1879. */
  1880. DataSet.prototype._sort = function (items, order) {
  1881. if (util.isString(order)) {
  1882. // order by provided field name
  1883. var name = order; // field name
  1884. items.sort(function (a, b) {
  1885. var av = a[name];
  1886. var bv = b[name];
  1887. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1888. });
  1889. }
  1890. else if (typeof order === 'function') {
  1891. // order by sort function
  1892. items.sort(order);
  1893. }
  1894. // TODO: extend order by an Object {field:String, direction:String}
  1895. // where direction can be 'asc' or 'desc'
  1896. else {
  1897. throw new TypeError('Order must be a function or a string');
  1898. }
  1899. };
  1900. /**
  1901. * Remove an object by pointer or by id
  1902. * @param {String | Number | Object | Array} id Object or id, or an array with
  1903. * objects or ids to be removed
  1904. * @param {String} [senderId] Optional sender id
  1905. */
  1906. DataSet.prototype.remove = function (id, senderId) {
  1907. var removedItems = [],
  1908. i, len;
  1909. if (util.isNumber(id) || util.isString(id)) {
  1910. delete this.data[id];
  1911. delete this.internalIds[id];
  1912. removedItems.push(id);
  1913. }
  1914. else if (id instanceof Array) {
  1915. for (i = 0, len = id.length; i < len; i++) {
  1916. this.remove(id[i]);
  1917. }
  1918. removedItems = items.concat(id);
  1919. }
  1920. else if (id instanceof Object) {
  1921. // search for the object
  1922. for (i in this.data) {
  1923. if (this.data.hasOwnProperty(i)) {
  1924. if (this.data[i] == id) {
  1925. delete this.data[i];
  1926. delete this.internalIds[i];
  1927. removedItems.push(i);
  1928. }
  1929. }
  1930. }
  1931. }
  1932. if (removedItems.length) {
  1933. this._trigger('remove', {items: removedItems}, senderId);
  1934. }
  1935. };
  1936. /**
  1937. * Clear the data
  1938. * @param {String} [senderId] Optional sender id
  1939. */
  1940. DataSet.prototype.clear = function (senderId) {
  1941. var ids = Object.keys(this.data);
  1942. this.data = {};
  1943. this.internalIds = {};
  1944. this._trigger('remove', {items: ids}, senderId);
  1945. };
  1946. /**
  1947. * Find the item with maximum value of a specified field
  1948. * @param {String} field
  1949. * @return {Object | null} item Item containing max value, or null if no items
  1950. */
  1951. DataSet.prototype.max = function (field) {
  1952. var data = this.data,
  1953. max = null,
  1954. maxField = null;
  1955. for (var id in data) {
  1956. if (data.hasOwnProperty(id)) {
  1957. var item = data[id];
  1958. var itemField = item[field];
  1959. if (itemField != null && (!max || itemField > maxField)) {
  1960. max = item;
  1961. maxField = itemField;
  1962. }
  1963. }
  1964. }
  1965. return max;
  1966. };
  1967. /**
  1968. * Find the item with minimum value of a specified field
  1969. * @param {String} field
  1970. * @return {Object | null} item Item containing max value, or null if no items
  1971. */
  1972. DataSet.prototype.min = function (field) {
  1973. var data = this.data,
  1974. min = null,
  1975. minField = null;
  1976. for (var id in data) {
  1977. if (data.hasOwnProperty(id)) {
  1978. var item = data[id];
  1979. var itemField = item[field];
  1980. if (itemField != null && (!min || itemField < minField)) {
  1981. min = item;
  1982. minField = itemField;
  1983. }
  1984. }
  1985. }
  1986. return min;
  1987. };
  1988. /**
  1989. * Find all distinct values of a specified field
  1990. * @param {String} field
  1991. * @return {Array} values Array containing all distinct values. If the data
  1992. * items do not contain the specified field, an array
  1993. * containing a single value undefined is returned.
  1994. * The returned array is unordered.
  1995. */
  1996. DataSet.prototype.distinct = function (field) {
  1997. var data = this.data,
  1998. values = [],
  1999. fieldType = this.options.fieldTypes[field],
  2000. count = 0;
  2001. for (var prop in data) {
  2002. if (data.hasOwnProperty(prop)) {
  2003. var item = data[prop];
  2004. var value = util.cast(item[field], fieldType);
  2005. var exists = false;
  2006. for (var i = 0; i < count; i++) {
  2007. if (values[i] == value) {
  2008. exists = true;
  2009. break;
  2010. }
  2011. }
  2012. if (!exists) {
  2013. values[count] = value;
  2014. count++;
  2015. }
  2016. }
  2017. }
  2018. return values;
  2019. };
  2020. /**
  2021. * Add a single item. Will fail when an item with the same id already exists.
  2022. * @param {Object} item
  2023. * @return {String} id
  2024. * @private
  2025. */
  2026. DataSet.prototype._addItem = function (item) {
  2027. var id = item[this.fieldId];
  2028. if (id != undefined) {
  2029. // check whether this id is already taken
  2030. if (this.data[id]) {
  2031. // item already exists
  2032. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  2033. }
  2034. }
  2035. else {
  2036. // generate an id
  2037. id = util.randomUUID();
  2038. item[this.fieldId] = id;
  2039. this.internalIds[id] = item;
  2040. }
  2041. var d = {};
  2042. for (var field in item) {
  2043. if (item.hasOwnProperty(field)) {
  2044. var type = this.fieldTypes[field]; // type may be undefined
  2045. d[field] = util.cast(item[field], type);
  2046. }
  2047. }
  2048. this.data[id] = d;
  2049. return id;
  2050. };
  2051. /**
  2052. * Get an item. Fields can be casted to a specific type
  2053. * @param {String} id
  2054. * @param {Object.<String, String>} [fieldTypes] Cast field types
  2055. * @return {Object | null} item
  2056. * @private
  2057. */
  2058. DataSet.prototype._getItem = function (id, fieldTypes) {
  2059. var field, value;
  2060. // get the item from the dataset
  2061. var raw = this.data[id];
  2062. if (!raw) {
  2063. return null;
  2064. }
  2065. // cast the items field types
  2066. var casted = {},
  2067. fieldId = this.fieldId,
  2068. internalIds = this.internalIds;
  2069. if (fieldTypes) {
  2070. for (field in raw) {
  2071. if (raw.hasOwnProperty(field)) {
  2072. value = raw[field];
  2073. // output all fields, except internal ids
  2074. if ((field != fieldId) || !(value in internalIds)) {
  2075. casted[field] = util.cast(value, fieldTypes[field]);
  2076. }
  2077. }
  2078. }
  2079. }
  2080. else {
  2081. // no field types specified, no casting needed
  2082. for (field in raw) {
  2083. if (raw.hasOwnProperty(field)) {
  2084. value = raw[field];
  2085. // output all fields, except internal ids
  2086. if ((field != fieldId) || !(value in internalIds)) {
  2087. casted[field] = value;
  2088. }
  2089. }
  2090. }
  2091. }
  2092. return casted;
  2093. };
  2094. /**
  2095. * Update a single item: merge with existing item.
  2096. * Will fail when the item has no id, or when there does not exist an item
  2097. * with the same id.
  2098. * @param {Object} item
  2099. * @return {String} id
  2100. * @private
  2101. */
  2102. DataSet.prototype._updateItem = function (item) {
  2103. var id = item[this.fieldId];
  2104. if (id == undefined) {
  2105. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  2106. }
  2107. var d = this.data[id];
  2108. if (!d) {
  2109. // item doesn't exist
  2110. throw new Error('Cannot update item: no item with id ' + id + ' found');
  2111. }
  2112. // merge with current item
  2113. for (var field in item) {
  2114. if (item.hasOwnProperty(field)) {
  2115. var type = this.fieldTypes[field]; // type may be undefined
  2116. d[field] = util.cast(item[field], type);
  2117. }
  2118. }
  2119. return id;
  2120. };
  2121. /**
  2122. * Get an array with the column names of a Google DataTable
  2123. * @param {DataTable} dataTable
  2124. * @return {String[]} columnNames
  2125. * @private
  2126. */
  2127. DataSet.prototype._getColumnNames = function (dataTable) {
  2128. var columns = [];
  2129. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  2130. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  2131. }
  2132. return columns;
  2133. };
  2134. /**
  2135. * Append an item as a row to the dataTable
  2136. * @param dataTable
  2137. * @param columns
  2138. * @param item
  2139. * @private
  2140. */
  2141. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  2142. var row = dataTable.addRow();
  2143. for (var col = 0, cols = columns.length; col < cols; col++) {
  2144. var field = columns[col];
  2145. dataTable.setValue(row, col, item[field]);
  2146. }
  2147. };
  2148. /**
  2149. * DataView
  2150. *
  2151. * a dataview offers a filtered view on a dataset or an other dataview.
  2152. *
  2153. * @param {DataSet | DataView} data
  2154. * @param {Object} [options] Available options: see method get
  2155. *
  2156. * @constructor DataView
  2157. */
  2158. function DataView (data, options) {
  2159. this.id = util.randomUUID();
  2160. this.data = null;
  2161. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  2162. this.options = options || {};
  2163. this.fieldId = 'id'; // name of the field containing id
  2164. this.subscribers = {}; // event subscribers
  2165. var me = this;
  2166. this.listener = function () {
  2167. me._onEvent.apply(me, arguments);
  2168. };
  2169. this.setData(data);
  2170. }
  2171. /**
  2172. * Set a data source for the view
  2173. * @param {DataSet | DataView} data
  2174. */
  2175. DataView.prototype.setData = function (data) {
  2176. var ids, dataItems, i, len;
  2177. if (this.data) {
  2178. // unsubscribe from current dataset
  2179. if (this.data.unsubscribe) {
  2180. this.data.unsubscribe('*', this.listener);
  2181. }
  2182. // trigger a remove of all items in memory
  2183. ids = [];
  2184. for (var id in this.ids) {
  2185. if (this.ids.hasOwnProperty(id)) {
  2186. ids.push(id);
  2187. }
  2188. }
  2189. this.ids = {};
  2190. this._trigger('remove', {items: ids});
  2191. }
  2192. this.data = data;
  2193. if (this.data) {
  2194. // update fieldId
  2195. this.fieldId = this.options.fieldId ||
  2196. (this.data && this.data.options && this.data.options.fieldId) ||
  2197. 'id';
  2198. // trigger an add of all added items
  2199. ids = this.data.getIds({filter: this.options && this.options.filter});
  2200. for (i = 0, len = ids.length; i < len; i++) {
  2201. id = ids[i];
  2202. this.ids[id] = true;
  2203. }
  2204. this._trigger('add', {items: ids});
  2205. // subscribe to new dataset
  2206. if (this.data.subscribe) {
  2207. this.data.subscribe('*', this.listener);
  2208. }
  2209. }
  2210. };
  2211. /**
  2212. * Get data from the data view
  2213. *
  2214. * Usage:
  2215. *
  2216. * get()
  2217. * get(options: Object)
  2218. * get(options: Object, data: Array | DataTable)
  2219. *
  2220. * get(id: Number)
  2221. * get(id: Number, options: Object)
  2222. * get(id: Number, options: Object, data: Array | DataTable)
  2223. *
  2224. * get(ids: Number[])
  2225. * get(ids: Number[], options: Object)
  2226. * get(ids: Number[], options: Object, data: Array | DataTable)
  2227. *
  2228. * Where:
  2229. *
  2230. * {Number | String} id The id of an item
  2231. * {Number[] | String{}} ids An array with ids of items
  2232. * {Object} options An Object with options. Available options:
  2233. * {String} [type] Type of data to be returned. Can
  2234. * be 'DataTable' or 'Array' (default)
  2235. * {Object.<String, String>} [fieldTypes]
  2236. * {String[]} [fields] field names to be returned
  2237. * {function} [filter] filter items
  2238. * {String | function} [order] Order the items by
  2239. * a field name or custom sort function.
  2240. * {Array | DataTable} [data] If provided, items will be appended to this
  2241. * array or table. Required in case of Google
  2242. * DataTable.
  2243. * @param args
  2244. */
  2245. DataView.prototype.get = function (args) {
  2246. var me = this;
  2247. // parse the arguments
  2248. var ids, options, data;
  2249. var firstType = util.getType(arguments[0]);
  2250. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2251. // get(id(s) [, options] [, data])
  2252. ids = arguments[0]; // can be a single id or an array with ids
  2253. options = arguments[1];
  2254. data = arguments[2];
  2255. }
  2256. else {
  2257. // get([, options] [, data])
  2258. options = arguments[0];
  2259. data = arguments[1];
  2260. }
  2261. // extend the options with the default options and provided options
  2262. var viewOptions = util.extend({}, this.options, options);
  2263. // create a combined filter method when needed
  2264. if (this.options.filter && options && options.filter) {
  2265. viewOptions.filter = function (item) {
  2266. return me.options.filter(item) && options.filter(item);
  2267. }
  2268. }
  2269. // build up the call to the linked data set
  2270. var getArguments = [];
  2271. if (ids != undefined) {
  2272. getArguments.push(ids);
  2273. }
  2274. getArguments.push(viewOptions);
  2275. getArguments.push(data);
  2276. return this.data && this.data.get.apply(this.data, getArguments);
  2277. };
  2278. /**
  2279. * Get ids of all items or from a filtered set of items.
  2280. * @param {Object} [options] An Object with options. Available options:
  2281. * {function} [filter] filter items
  2282. * {String | function} [order] Order the items by
  2283. * a field name or custom sort function.
  2284. * @return {Array} ids
  2285. */
  2286. DataView.prototype.getIds = function (options) {
  2287. var ids;
  2288. if (this.data) {
  2289. var defaultFilter = this.options.filter;
  2290. var filter;
  2291. if (options && options.filter) {
  2292. if (defaultFilter) {
  2293. filter = function (item) {
  2294. return defaultFilter(item) && options.filter(item);
  2295. }
  2296. }
  2297. else {
  2298. filter = options.filter;
  2299. }
  2300. }
  2301. else {
  2302. filter = defaultFilter;
  2303. }
  2304. ids = this.data.getIds({
  2305. filter: filter,
  2306. order: options && options.order
  2307. });
  2308. }
  2309. else {
  2310. ids = [];
  2311. }
  2312. return ids;
  2313. };
  2314. /**
  2315. * Event listener. Will propagate all events from the connected data set to
  2316. * the subscribers of the DataView, but will filter the items and only trigger
  2317. * when there are changes in the filtered data set.
  2318. * @param {String} event
  2319. * @param {Object | null} params
  2320. * @param {String} senderId
  2321. * @private
  2322. */
  2323. DataView.prototype._onEvent = function (event, params, senderId) {
  2324. var i, len, id, item,
  2325. ids = params && params.items,
  2326. data = this.data,
  2327. added = [],
  2328. updated = [],
  2329. removed = [];
  2330. if (ids && data) {
  2331. switch (event) {
  2332. case 'add':
  2333. // filter the ids of the added items
  2334. for (i = 0, len = ids.length; i < len; i++) {
  2335. id = ids[i];
  2336. item = this.get(id);
  2337. if (item) {
  2338. this.ids[id] = true;
  2339. added.push(id);
  2340. }
  2341. }
  2342. break;
  2343. case 'update':
  2344. // determine the event from the views viewpoint: an updated
  2345. // item can be added, updated, or removed from this view.
  2346. for (i = 0, len = ids.length; i < len; i++) {
  2347. id = ids[i];
  2348. item = this.get(id);
  2349. if (item) {
  2350. if (this.ids[id]) {
  2351. updated.push(id);
  2352. }
  2353. else {
  2354. this.ids[id] = true;
  2355. added.push(id);
  2356. }
  2357. }
  2358. else {
  2359. if (this.ids[id]) {
  2360. delete this.ids[id];
  2361. removed.push(id);
  2362. }
  2363. else {
  2364. // nothing interesting for me :-(
  2365. }
  2366. }
  2367. }
  2368. break;
  2369. case 'remove':
  2370. // filter the ids of the removed items
  2371. for (i = 0, len = ids.length; i < len; i++) {
  2372. id = ids[i];
  2373. if (this.ids[id]) {
  2374. delete this.ids[id];
  2375. removed.push(id);
  2376. }
  2377. }
  2378. break;
  2379. }
  2380. if (added.length) {
  2381. this._trigger('add', {items: added}, senderId);
  2382. }
  2383. if (updated.length) {
  2384. this._trigger('update', {items: updated}, senderId);
  2385. }
  2386. if (removed.length) {
  2387. this._trigger('remove', {items: removed}, senderId);
  2388. }
  2389. }
  2390. };
  2391. // copy subscription functionality from DataSet
  2392. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2393. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2394. DataView.prototype._trigger = DataSet.prototype._trigger;
  2395. /**
  2396. * @constructor Stack
  2397. * Stacks items on top of each other.
  2398. * @param {ItemSet} parent
  2399. * @param {Object} [options]
  2400. */
  2401. function Stack (parent, options) {
  2402. this.parent = parent;
  2403. this.options = options || {};
  2404. this.defaultOptions = {
  2405. order: function (a, b) {
  2406. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2407. // Order: ranges over non-ranges, ranged ordered by width, and
  2408. // lastly ordered by start.
  2409. if (a instanceof ItemRange) {
  2410. if (b instanceof ItemRange) {
  2411. var aInt = (a.data.end - a.data.start);
  2412. var bInt = (b.data.end - b.data.start);
  2413. return (aInt - bInt) || (a.data.start - b.data.start);
  2414. }
  2415. else {
  2416. return -1;
  2417. }
  2418. }
  2419. else {
  2420. if (b instanceof ItemRange) {
  2421. return 1;
  2422. }
  2423. else {
  2424. return (a.data.start - b.data.start);
  2425. }
  2426. }
  2427. },
  2428. margin: {
  2429. item: 10
  2430. }
  2431. };
  2432. this.ordered = []; // ordered items
  2433. }
  2434. /**
  2435. * Set options for the stack
  2436. * @param {Object} options Available options:
  2437. * {ItemSet} parent
  2438. * {Number} margin
  2439. * {function} order Stacking order
  2440. */
  2441. Stack.prototype.setOptions = function setOptions (options) {
  2442. util.extend(this.options, options);
  2443. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2444. };
  2445. /**
  2446. * Stack the items such that they don't overlap. The items will have a minimal
  2447. * distance equal to options.margin.item.
  2448. */
  2449. Stack.prototype.update = function update() {
  2450. this._order();
  2451. this._stack();
  2452. };
  2453. /**
  2454. * Order the items. The items are ordered by width first, and by left position
  2455. * second.
  2456. * If a custom order function has been provided via the options, then this will
  2457. * be used.
  2458. * @private
  2459. */
  2460. Stack.prototype._order = function _order () {
  2461. var items = this.parent.items;
  2462. if (!items) {
  2463. throw new Error('Cannot stack items: parent does not contain items');
  2464. }
  2465. // TODO: store the sorted items, to have less work later on
  2466. var ordered = [];
  2467. var index = 0;
  2468. // items is a map (no array)
  2469. util.forEach(items, function (item) {
  2470. if (item.visible) {
  2471. ordered[index] = item;
  2472. index++;
  2473. }
  2474. });
  2475. //if a customer stack order function exists, use it.
  2476. var order = this.options.order || this.defaultOptions.order;
  2477. if (!(typeof order === 'function')) {
  2478. throw new Error('Option order must be a function');
  2479. }
  2480. ordered.sort(order);
  2481. this.ordered = ordered;
  2482. };
  2483. /**
  2484. * Adjust vertical positions of the events such that they don't overlap each
  2485. * other.
  2486. * @private
  2487. */
  2488. Stack.prototype._stack = function _stack () {
  2489. var i,
  2490. iMax,
  2491. ordered = this.ordered,
  2492. options = this.options,
  2493. orientation = options.orientation || this.defaultOptions.orientation,
  2494. axisOnTop = (orientation == 'top'),
  2495. margin;
  2496. if (options.margin && options.margin.item !== undefined) {
  2497. margin = options.margin.item;
  2498. }
  2499. else {
  2500. margin = this.defaultOptions.margin.item
  2501. }
  2502. // calculate new, non-overlapping positions
  2503. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2504. var item = ordered[i];
  2505. var collidingItem = null;
  2506. do {
  2507. // TODO: optimize checking for overlap. when there is a gap without items,
  2508. // you only need to check for items from the next item on, not from zero
  2509. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2510. if (collidingItem != null) {
  2511. // There is a collision. Reposition the event above the colliding element
  2512. if (axisOnTop) {
  2513. item.top = collidingItem.top + collidingItem.height + margin;
  2514. }
  2515. else {
  2516. item.top = collidingItem.top - item.height - margin;
  2517. }
  2518. }
  2519. } while (collidingItem);
  2520. }
  2521. };
  2522. /**
  2523. * Check if the destiny position of given item overlaps with any
  2524. * of the other items from index itemStart to itemEnd.
  2525. * @param {Array} items Array with items
  2526. * @param {int} itemIndex Number of the item to be checked for overlap
  2527. * @param {int} itemStart First item to be checked.
  2528. * @param {int} itemEnd Last item to be checked.
  2529. * @return {Object | null} colliding item, or undefined when no collisions
  2530. * @param {Number} margin A minimum required margin.
  2531. * If margin is provided, the two items will be
  2532. * marked colliding when they overlap or
  2533. * when the margin between the two is smaller than
  2534. * the requested margin.
  2535. */
  2536. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2537. itemStart, itemEnd, margin) {
  2538. var collision = this.collision;
  2539. // we loop from end to start, as we suppose that the chance of a
  2540. // collision is larger for items at the end, so check these first.
  2541. var a = items[itemIndex];
  2542. for (var i = itemEnd; i >= itemStart; i--) {
  2543. var b = items[i];
  2544. if (collision(a, b, margin)) {
  2545. if (i != itemIndex) {
  2546. return b;
  2547. }
  2548. }
  2549. }
  2550. return null;
  2551. };
  2552. /**
  2553. * Test if the two provided items collide
  2554. * The items must have parameters left, width, top, and height.
  2555. * @param {Component} a The first item
  2556. * @param {Component} b The second item
  2557. * @param {Number} margin A minimum required margin.
  2558. * If margin is provided, the two items will be
  2559. * marked colliding when they overlap or
  2560. * when the margin between the two is smaller than
  2561. * the requested margin.
  2562. * @return {boolean} true if a and b collide, else false
  2563. */
  2564. Stack.prototype.collision = function collision (a, b, margin) {
  2565. return ((a.left - margin) < (b.left + b.width) &&
  2566. (a.left + a.width + margin) > b.left &&
  2567. (a.top - margin) < (b.top + b.height) &&
  2568. (a.top + a.height + margin) > b.top);
  2569. };
  2570. /**
  2571. * @constructor Range
  2572. * A Range controls a numeric range with a start and end value.
  2573. * The Range adjusts the range based on mouse events or programmatic changes,
  2574. * and triggers events when the range is changing or has been changed.
  2575. * @param {Object} [options] See description at Range.setOptions
  2576. * @extends Controller
  2577. */
  2578. function Range(options) {
  2579. this.id = util.randomUUID();
  2580. this.start = 0; // Number
  2581. this.end = 0; // Number
  2582. this.options = {
  2583. min: null,
  2584. max: null,
  2585. zoomMin: null,
  2586. zoomMax: null
  2587. };
  2588. this.listeners = [];
  2589. this.setOptions(options);
  2590. }
  2591. /**
  2592. * Set options for the range controller
  2593. * @param {Object} options Available options:
  2594. * {Number} start Set start value of the range
  2595. * {Number} end Set end value of the range
  2596. * {Number} min Minimum value for start
  2597. * {Number} max Maximum value for end
  2598. * {Number} zoomMin Set a minimum value for
  2599. * (end - start).
  2600. * {Number} zoomMax Set a maximum value for
  2601. * (end - start).
  2602. */
  2603. Range.prototype.setOptions = function (options) {
  2604. util.extend(this.options, options);
  2605. if (options.start != null || options.end != null) {
  2606. this.setRange(options.start, options.end);
  2607. }
  2608. };
  2609. /**
  2610. * Add listeners for mouse and touch events to the component
  2611. * @param {Component} component
  2612. * @param {String} event Available events: 'move', 'zoom'
  2613. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2614. */
  2615. Range.prototype.subscribe = function (component, event, direction) {
  2616. var me = this;
  2617. var listener;
  2618. if (direction != 'horizontal' && direction != 'vertical') {
  2619. throw new TypeError('Unknown direction "' + direction + '". ' +
  2620. 'Choose "horizontal" or "vertical".');
  2621. }
  2622. //noinspection FallthroughInSwitchStatementJS
  2623. if (event == 'move') {
  2624. listener = {
  2625. component: component,
  2626. event: event,
  2627. direction: direction,
  2628. callback: function (event) {
  2629. me._onMouseDown(event, listener);
  2630. },
  2631. params: {}
  2632. };
  2633. component.on('mousedown', listener.callback);
  2634. me.listeners.push(listener);
  2635. }
  2636. else if (event == 'zoom') {
  2637. listener = {
  2638. component: component,
  2639. event: event,
  2640. direction: direction,
  2641. callback: function (event) {
  2642. me._onMouseWheel(event, listener);
  2643. },
  2644. params: {}
  2645. };
  2646. component.on('mousewheel', listener.callback);
  2647. me.listeners.push(listener);
  2648. }
  2649. else {
  2650. throw new TypeError('Unknown event "' + event + '". ' +
  2651. 'Choose "move" or "zoom".');
  2652. }
  2653. };
  2654. /**
  2655. * Event handler
  2656. * @param {String} event name of the event, for example 'click', 'mousemove'
  2657. * @param {function} callback callback handler, invoked with the raw HTML Event
  2658. * as parameter.
  2659. */
  2660. Range.prototype.on = function (event, callback) {
  2661. events.addListener(this, event, callback);
  2662. };
  2663. /**
  2664. * Trigger an event
  2665. * @param {String} event name of the event, available events: 'rangechange',
  2666. * 'rangechanged'
  2667. * @private
  2668. */
  2669. Range.prototype._trigger = function (event) {
  2670. events.trigger(this, event, {
  2671. start: this.start,
  2672. end: this.end
  2673. });
  2674. };
  2675. /**
  2676. * Set a new start and end range
  2677. * @param {Number} start
  2678. * @param {Number} end
  2679. */
  2680. Range.prototype.setRange = function(start, end) {
  2681. var changed = this._applyRange(start, end);
  2682. if (changed) {
  2683. this._trigger('rangechange');
  2684. this._trigger('rangechanged');
  2685. }
  2686. };
  2687. /**
  2688. * Set a new start and end range. This method is the same as setRange, but
  2689. * does not trigger a range change and range changed event, and it returns
  2690. * true when the range is changed
  2691. * @param {Number} start
  2692. * @param {Number} end
  2693. * @return {Boolean} changed
  2694. * @private
  2695. */
  2696. Range.prototype._applyRange = function(start, end) {
  2697. var newStart = (start != null) ? util.cast(start, 'Number') : this.start;
  2698. var newEnd = (end != null) ? util.cast(end, 'Number') : this.end;
  2699. var diff;
  2700. // check for valid number
  2701. if (isNaN(newStart)) {
  2702. throw new Error('Invalid start "' + start + '"');
  2703. }
  2704. if (isNaN(newEnd)) {
  2705. throw new Error('Invalid end "' + end + '"');
  2706. }
  2707. // prevent start < end
  2708. if (newEnd < newStart) {
  2709. newEnd = newStart;
  2710. }
  2711. // prevent start < min
  2712. if (this.options.min != null) {
  2713. var min = this.options.min.valueOf();
  2714. if (newStart < min) {
  2715. diff = (min - newStart);
  2716. newStart += diff;
  2717. newEnd += diff;
  2718. }
  2719. }
  2720. // prevent end > max
  2721. if (this.options.max != null) {
  2722. var max = this.options.max.valueOf();
  2723. if (newEnd > max) {
  2724. diff = (newEnd - max);
  2725. newStart -= diff;
  2726. newEnd -= diff;
  2727. }
  2728. }
  2729. // prevent (end-start) > zoomMin
  2730. if (this.options.zoomMin != null) {
  2731. var zoomMin = this.options.zoomMin.valueOf();
  2732. if (zoomMin < 0) {
  2733. zoomMin = 0;
  2734. }
  2735. if ((newEnd - newStart) < zoomMin) {
  2736. if ((this.end - this.start) > zoomMin) {
  2737. // zoom to the minimum
  2738. diff = (zoomMin - (newEnd - newStart));
  2739. newStart -= diff / 2;
  2740. newEnd += diff / 2;
  2741. }
  2742. else {
  2743. // ingore this action, we are already zoomed to the minimum
  2744. newStart = this.start;
  2745. newEnd = this.end;
  2746. }
  2747. }
  2748. }
  2749. // prevent (end-start) > zoomMin
  2750. if (this.options.zoomMax != null) {
  2751. var zoomMax = this.options.zoomMax.valueOf();
  2752. if (zoomMax < 0) {
  2753. zoomMax = 0;
  2754. }
  2755. if ((newEnd - newStart) > zoomMax) {
  2756. if ((this.end - this.start) < zoomMax) {
  2757. // zoom to the maximum
  2758. diff = ((newEnd - newStart) - zoomMax);
  2759. newStart += diff / 2;
  2760. newEnd -= diff / 2;
  2761. }
  2762. else {
  2763. // ingore this action, we are already zoomed to the maximum
  2764. newStart = this.start;
  2765. newEnd = this.end;
  2766. }
  2767. }
  2768. }
  2769. var changed = (this.start != newStart || this.end != newEnd);
  2770. this.start = newStart;
  2771. this.end = newEnd;
  2772. return changed;
  2773. };
  2774. /**
  2775. * Retrieve the current range.
  2776. * @return {Object} An object with start and end properties
  2777. */
  2778. Range.prototype.getRange = function() {
  2779. return {
  2780. start: this.start,
  2781. end: this.end
  2782. };
  2783. };
  2784. /**
  2785. * Calculate the conversion offset and factor for current range, based on
  2786. * the provided width
  2787. * @param {Number} width
  2788. * @returns {{offset: number, factor: number}} conversion
  2789. */
  2790. Range.prototype.conversion = function (width) {
  2791. var start = this.start;
  2792. var end = this.end;
  2793. return Range.conversion(this.start, this.end, width);
  2794. };
  2795. /**
  2796. * Static method to calculate the conversion offset and factor for a range,
  2797. * based on the provided start, end, and width
  2798. * @param {Number} start
  2799. * @param {Number} end
  2800. * @param {Number} width
  2801. * @returns {{offset: number, factor: number}} conversion
  2802. */
  2803. Range.conversion = function (start, end, width) {
  2804. if (width != 0 && (end - start != 0)) {
  2805. return {
  2806. offset: start,
  2807. factor: width / (end - start)
  2808. }
  2809. }
  2810. else {
  2811. return {
  2812. offset: 0,
  2813. factor: 1
  2814. };
  2815. }
  2816. };
  2817. /**
  2818. * Start moving horizontally or vertically
  2819. * @param {Event} event
  2820. * @param {Object} listener Listener containing the component and params
  2821. * @private
  2822. */
  2823. Range.prototype._onMouseDown = function(event, listener) {
  2824. event = event || window.event;
  2825. var params = listener.params;
  2826. // only react on left mouse button down
  2827. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2828. if (!leftButtonDown) {
  2829. return;
  2830. }
  2831. // get mouse position
  2832. params.mouseX = util.getPageX(event);
  2833. params.mouseY = util.getPageY(event);
  2834. params.previousLeft = 0;
  2835. params.previousOffset = 0;
  2836. params.moved = false;
  2837. params.start = this.start;
  2838. params.end = this.end;
  2839. var frame = listener.component.frame;
  2840. if (frame) {
  2841. frame.style.cursor = 'move';
  2842. }
  2843. // add event listeners to handle moving the contents
  2844. // we store the function onmousemove and onmouseup in the timeaxis,
  2845. // so we can remove the eventlisteners lateron in the function onmouseup
  2846. var me = this;
  2847. if (!params.onMouseMove) {
  2848. params.onMouseMove = function (event) {
  2849. me._onMouseMove(event, listener);
  2850. };
  2851. util.addEventListener(document, "mousemove", params.onMouseMove);
  2852. }
  2853. if (!params.onMouseUp) {
  2854. params.onMouseUp = function (event) {
  2855. me._onMouseUp(event, listener);
  2856. };
  2857. util.addEventListener(document, "mouseup", params.onMouseUp);
  2858. }
  2859. util.preventDefault(event);
  2860. };
  2861. /**
  2862. * Perform moving operating.
  2863. * This function activated from within the funcion TimeAxis._onMouseDown().
  2864. * @param {Event} event
  2865. * @param {Object} listener
  2866. * @private
  2867. */
  2868. Range.prototype._onMouseMove = function (event, listener) {
  2869. event = event || window.event;
  2870. var params = listener.params;
  2871. // calculate change in mouse position
  2872. var mouseX = util.getPageX(event);
  2873. var mouseY = util.getPageY(event);
  2874. if (params.mouseX == undefined) {
  2875. params.mouseX = mouseX;
  2876. }
  2877. if (params.mouseY == undefined) {
  2878. params.mouseY = mouseY;
  2879. }
  2880. var diffX = mouseX - params.mouseX;
  2881. var diffY = mouseY - params.mouseY;
  2882. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  2883. // if mouse movement is big enough, register it as a "moved" event
  2884. if (Math.abs(diff) >= 1) {
  2885. params.moved = true;
  2886. }
  2887. var interval = (params.end - params.start);
  2888. var width = (listener.direction == 'horizontal') ?
  2889. listener.component.width : listener.component.height;
  2890. var diffRange = -diff / width * interval;
  2891. this._applyRange(params.start + diffRange, params.end + diffRange);
  2892. // fire a rangechange event
  2893. this._trigger('rangechange');
  2894. util.preventDefault(event);
  2895. };
  2896. /**
  2897. * Stop moving operating.
  2898. * This function activated from within the function Range._onMouseDown().
  2899. * @param {event} event
  2900. * @param {Object} listener
  2901. * @private
  2902. */
  2903. Range.prototype._onMouseUp = function (event, listener) {
  2904. event = event || window.event;
  2905. var params = listener.params;
  2906. if (listener.component.frame) {
  2907. listener.component.frame.style.cursor = 'auto';
  2908. }
  2909. // remove event listeners here, important for Safari
  2910. if (params.onMouseMove) {
  2911. util.removeEventListener(document, "mousemove", params.onMouseMove);
  2912. params.onMouseMove = null;
  2913. }
  2914. if (params.onMouseUp) {
  2915. util.removeEventListener(document, "mouseup", params.onMouseUp);
  2916. params.onMouseUp = null;
  2917. }
  2918. //util.preventDefault(event);
  2919. if (params.moved) {
  2920. // fire a rangechanged event
  2921. this._trigger('rangechanged');
  2922. }
  2923. };
  2924. /**
  2925. * Event handler for mouse wheel event, used to zoom
  2926. * Code from http://adomas.org/javascript-mouse-wheel/
  2927. * @param {Event} event
  2928. * @param {Object} listener
  2929. * @private
  2930. */
  2931. Range.prototype._onMouseWheel = function(event, listener) {
  2932. event = event || window.event;
  2933. // retrieve delta
  2934. var delta = 0;
  2935. if (event.wheelDelta) { /* IE/Opera. */
  2936. delta = event.wheelDelta / 120;
  2937. } else if (event.detail) { /* Mozilla case. */
  2938. // In Mozilla, sign of delta is different than in IE.
  2939. // Also, delta is multiple of 3.
  2940. delta = -event.detail / 3;
  2941. }
  2942. // If delta is nonzero, handle it.
  2943. // Basically, delta is now positive if wheel was scrolled up,
  2944. // and negative, if wheel was scrolled down.
  2945. if (delta) {
  2946. var me = this;
  2947. var zoom = function () {
  2948. // perform the zoom action. Delta is normally 1 or -1
  2949. var zoomFactor = delta / 5.0;
  2950. var zoomAround = null;
  2951. var frame = listener.component.frame;
  2952. if (frame) {
  2953. var size, conversion;
  2954. if (listener.direction == 'horizontal') {
  2955. size = listener.component.width;
  2956. conversion = me.conversion(size);
  2957. var frameLeft = util.getAbsoluteLeft(frame);
  2958. var mouseX = util.getPageX(event);
  2959. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  2960. }
  2961. else {
  2962. size = listener.component.height;
  2963. conversion = me.conversion(size);
  2964. var frameTop = util.getAbsoluteTop(frame);
  2965. var mouseY = util.getPageY(event);
  2966. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  2967. }
  2968. }
  2969. me.zoom(zoomFactor, zoomAround);
  2970. };
  2971. zoom();
  2972. }
  2973. // Prevent default actions caused by mouse wheel.
  2974. // That might be ugly, but we handle scrolls somehow
  2975. // anyway, so don't bother here...
  2976. util.preventDefault(event);
  2977. };
  2978. /**
  2979. * Zoom the range the given zoomfactor in or out. Start and end date will
  2980. * be adjusted, and the timeline will be redrawn. You can optionally give a
  2981. * date around which to zoom.
  2982. * For example, try zoomfactor = 0.1 or -0.1
  2983. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  2984. * negative value will zoom out
  2985. * @param {Number} zoomAround Value around which will be zoomed. Optional
  2986. */
  2987. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  2988. // if zoomAroundDate is not provided, take it half between start Date and end Date
  2989. if (zoomAround == null) {
  2990. zoomAround = (this.start + this.end) / 2;
  2991. }
  2992. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  2993. // result in a start>=end )
  2994. if (zoomFactor >= 1) {
  2995. zoomFactor = 0.9;
  2996. }
  2997. if (zoomFactor <= -1) {
  2998. zoomFactor = -0.9;
  2999. }
  3000. // adjust a negative factor such that zooming in with 0.1 equals zooming
  3001. // out with a factor -0.1
  3002. if (zoomFactor < 0) {
  3003. zoomFactor = zoomFactor / (1 + zoomFactor);
  3004. }
  3005. // zoom start and end relative to the zoomAround value
  3006. var startDiff = (this.start - zoomAround);
  3007. var endDiff = (this.end - zoomAround);
  3008. // calculate new start and end
  3009. var newStart = this.start - startDiff * zoomFactor;
  3010. var newEnd = this.end - endDiff * zoomFactor;
  3011. this.setRange(newStart, newEnd);
  3012. };
  3013. /**
  3014. * Move the range with a given factor to the left or right. Start and end
  3015. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  3016. * @param {Number} moveFactor Moving amount. Positive value will move right,
  3017. * negative value will move left
  3018. */
  3019. Range.prototype.move = function(moveFactor) {
  3020. // zoom start Date and end Date relative to the zoomAroundDate
  3021. var diff = (this.end - this.start);
  3022. // apply new values
  3023. var newStart = this.start + diff * moveFactor;
  3024. var newEnd = this.end + diff * moveFactor;
  3025. // TODO: reckon with min and max range
  3026. this.start = newStart;
  3027. this.end = newEnd;
  3028. };
  3029. /**
  3030. * An event bus can be used to emit events, and to subscribe to events
  3031. * @constructor EventBus
  3032. */
  3033. function EventBus() {
  3034. this.subscriptions = [];
  3035. }
  3036. /**
  3037. * Subscribe to an event
  3038. * @param {String | RegExp} event The event can be a regular expression, or
  3039. * a string with wildcards, like 'server.*'.
  3040. * @param {function} callback. Callback are called with three parameters:
  3041. * {String} event, {*} [data], {*} [source]
  3042. * @param {*} [target]
  3043. * @returns {String} id A subscription id
  3044. */
  3045. EventBus.prototype.on = function (event, callback, target) {
  3046. var regexp = (event instanceof RegExp) ?
  3047. event :
  3048. new RegExp(event.replace('*', '\\w+'));
  3049. var subscription = {
  3050. id: util.randomUUID(),
  3051. event: event,
  3052. regexp: regexp,
  3053. callback: (typeof callback === 'function') ? callback : null,
  3054. target: target
  3055. };
  3056. this.subscriptions.push(subscription);
  3057. return subscription.id;
  3058. };
  3059. /**
  3060. * Unsubscribe from an event
  3061. * @param {String | Object} filter Filter for subscriptions to be removed
  3062. * Filter can be a string containing a
  3063. * subscription id, or an object containing
  3064. * one or more of the fields id, event,
  3065. * callback, and target.
  3066. */
  3067. EventBus.prototype.off = function (filter) {
  3068. var i = 0;
  3069. while (i < this.subscriptions.length) {
  3070. var subscription = this.subscriptions[i];
  3071. var match = true;
  3072. if (filter instanceof Object) {
  3073. // filter is an object. All fields must match
  3074. for (var prop in filter) {
  3075. if (filter.hasOwnProperty(prop)) {
  3076. if (filter[prop] !== subscription[prop]) {
  3077. match = false;
  3078. }
  3079. }
  3080. }
  3081. }
  3082. else {
  3083. // filter is a string, filter on id
  3084. match = (subscription.id == filter);
  3085. }
  3086. if (match) {
  3087. this.subscriptions.splice(i, 1);
  3088. }
  3089. else {
  3090. i++;
  3091. }
  3092. }
  3093. };
  3094. /**
  3095. * Emit an event
  3096. * @param {String} event
  3097. * @param {*} [data]
  3098. * @param {*} [source]
  3099. */
  3100. EventBus.prototype.emit = function (event, data, source) {
  3101. for (var i =0; i < this.subscriptions.length; i++) {
  3102. var subscription = this.subscriptions[i];
  3103. if (subscription.regexp.test(event)) {
  3104. if (subscription.callback) {
  3105. subscription.callback(event, data, source);
  3106. }
  3107. }
  3108. }
  3109. };
  3110. /**
  3111. * @constructor Controller
  3112. *
  3113. * A Controller controls the reflows and repaints of all visual components
  3114. */
  3115. function Controller () {
  3116. this.id = util.randomUUID();
  3117. this.components = {};
  3118. this.repaintTimer = undefined;
  3119. this.reflowTimer = undefined;
  3120. }
  3121. /**
  3122. * Add a component to the controller
  3123. * @param {Component} component
  3124. */
  3125. Controller.prototype.add = function add(component) {
  3126. // validate the component
  3127. if (component.id == undefined) {
  3128. throw new Error('Component has no field id');
  3129. }
  3130. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3131. throw new TypeError('Component must be an instance of ' +
  3132. 'prototype Component or Controller');
  3133. }
  3134. // add the component
  3135. component.controller = this;
  3136. this.components[component.id] = component;
  3137. };
  3138. /**
  3139. * Remove a component from the controller
  3140. * @param {Component | String} component
  3141. */
  3142. Controller.prototype.remove = function remove(component) {
  3143. var id;
  3144. for (id in this.components) {
  3145. if (this.components.hasOwnProperty(id)) {
  3146. if (id == component || this.components[id] == component) {
  3147. break;
  3148. }
  3149. }
  3150. }
  3151. if (id) {
  3152. delete this.components[id];
  3153. }
  3154. };
  3155. /**
  3156. * Request a reflow. The controller will schedule a reflow
  3157. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3158. * is false.
  3159. */
  3160. Controller.prototype.requestReflow = function requestReflow(force) {
  3161. if (force) {
  3162. this.reflow();
  3163. }
  3164. else {
  3165. if (!this.reflowTimer) {
  3166. var me = this;
  3167. this.reflowTimer = setTimeout(function () {
  3168. me.reflowTimer = undefined;
  3169. me.reflow();
  3170. }, 0);
  3171. }
  3172. }
  3173. };
  3174. /**
  3175. * Request a repaint. The controller will schedule a repaint
  3176. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3177. * is false.
  3178. */
  3179. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3180. if (force) {
  3181. this.repaint();
  3182. }
  3183. else {
  3184. if (!this.repaintTimer) {
  3185. var me = this;
  3186. this.repaintTimer = setTimeout(function () {
  3187. me.repaintTimer = undefined;
  3188. me.repaint();
  3189. }, 0);
  3190. }
  3191. }
  3192. };
  3193. /**
  3194. * Repaint all components
  3195. */
  3196. Controller.prototype.repaint = function repaint() {
  3197. var changed = false;
  3198. // cancel any running repaint request
  3199. if (this.repaintTimer) {
  3200. clearTimeout(this.repaintTimer);
  3201. this.repaintTimer = undefined;
  3202. }
  3203. var done = {};
  3204. function repaint(component, id) {
  3205. if (!(id in done)) {
  3206. // first repaint the components on which this component is dependent
  3207. if (component.depends) {
  3208. component.depends.forEach(function (dep) {
  3209. repaint(dep, dep.id);
  3210. });
  3211. }
  3212. if (component.parent) {
  3213. repaint(component.parent, component.parent.id);
  3214. }
  3215. // repaint the component itself and mark as done
  3216. changed = component.repaint() || changed;
  3217. done[id] = true;
  3218. }
  3219. }
  3220. util.forEach(this.components, repaint);
  3221. // immediately reflow when needed
  3222. if (changed) {
  3223. this.reflow();
  3224. }
  3225. // TODO: limit the number of nested reflows/repaints, prevent loop
  3226. };
  3227. /**
  3228. * Reflow all components
  3229. */
  3230. Controller.prototype.reflow = function reflow() {
  3231. var resized = false;
  3232. // cancel any running repaint request
  3233. if (this.reflowTimer) {
  3234. clearTimeout(this.reflowTimer);
  3235. this.reflowTimer = undefined;
  3236. }
  3237. var done = {};
  3238. function reflow(component, id) {
  3239. if (!(id in done)) {
  3240. // first reflow the components on which this component is dependent
  3241. if (component.depends) {
  3242. component.depends.forEach(function (dep) {
  3243. reflow(dep, dep.id);
  3244. });
  3245. }
  3246. if (component.parent) {
  3247. reflow(component.parent, component.parent.id);
  3248. }
  3249. // reflow the component itself and mark as done
  3250. resized = component.reflow() || resized;
  3251. done[id] = true;
  3252. }
  3253. }
  3254. util.forEach(this.components, reflow);
  3255. // immediately repaint when needed
  3256. if (resized) {
  3257. this.repaint();
  3258. }
  3259. // TODO: limit the number of nested reflows/repaints, prevent loop
  3260. };
  3261. /**
  3262. * Prototype for visual components
  3263. */
  3264. function Component () {
  3265. this.id = null;
  3266. this.parent = null;
  3267. this.depends = null;
  3268. this.controller = null;
  3269. this.options = null;
  3270. this.frame = null; // main DOM element
  3271. this.top = 0;
  3272. this.left = 0;
  3273. this.width = 0;
  3274. this.height = 0;
  3275. }
  3276. /**
  3277. * Set parameters for the frame. Parameters will be merged in current parameter
  3278. * set.
  3279. * @param {Object} options Available parameters:
  3280. * {String | function} [className]
  3281. * {EventBus} [eventBus]
  3282. * {String | Number | function} [left]
  3283. * {String | Number | function} [top]
  3284. * {String | Number | function} [width]
  3285. * {String | Number | function} [height]
  3286. */
  3287. Component.prototype.setOptions = function setOptions(options) {
  3288. if (options) {
  3289. util.extend(this.options, options);
  3290. if (this.controller) {
  3291. this.requestRepaint();
  3292. this.requestReflow();
  3293. }
  3294. }
  3295. };
  3296. /**
  3297. * Get an option value by name
  3298. * The function will first check this.options object, and else will check
  3299. * this.defaultOptions.
  3300. * @param {String} name
  3301. * @return {*} value
  3302. */
  3303. Component.prototype.getOption = function getOption(name) {
  3304. var value;
  3305. if (this.options) {
  3306. value = this.options[name];
  3307. }
  3308. if (value === undefined && this.defaultOptions) {
  3309. value = this.defaultOptions[name];
  3310. }
  3311. return value;
  3312. };
  3313. /**
  3314. * Get the container element of the component, which can be used by a child to
  3315. * add its own widgets. Not all components do have a container for childs, in
  3316. * that case null is returned.
  3317. * @returns {HTMLElement | null} container
  3318. */
  3319. Component.prototype.getContainer = function getContainer() {
  3320. // should be implemented by the component
  3321. return null;
  3322. };
  3323. /**
  3324. * Get the frame element of the component, the outer HTML DOM element.
  3325. * @returns {HTMLElement | null} frame
  3326. */
  3327. Component.prototype.getFrame = function getFrame() {
  3328. return this.frame;
  3329. };
  3330. /**
  3331. * Repaint the component
  3332. * @return {Boolean} changed
  3333. */
  3334. Component.prototype.repaint = function repaint() {
  3335. // should be implemented by the component
  3336. return false;
  3337. };
  3338. /**
  3339. * Reflow the component
  3340. * @return {Boolean} resized
  3341. */
  3342. Component.prototype.reflow = function reflow() {
  3343. // should be implemented by the component
  3344. return false;
  3345. };
  3346. /**
  3347. * Hide the component from the DOM
  3348. * @return {Boolean} changed
  3349. */
  3350. Component.prototype.hide = function hide() {
  3351. if (this.frame && this.frame.parentNode) {
  3352. this.frame.parentNode.removeChild(this.frame);
  3353. return true;
  3354. }
  3355. else {
  3356. return false;
  3357. }
  3358. };
  3359. /**
  3360. * Show the component in the DOM (when not already visible).
  3361. * A repaint will be executed when the component is not visible
  3362. * @return {Boolean} changed
  3363. */
  3364. Component.prototype.show = function show() {
  3365. if (!this.frame || !this.frame.parentNode) {
  3366. return this.repaint();
  3367. }
  3368. else {
  3369. return false;
  3370. }
  3371. };
  3372. /**
  3373. * Request a repaint. The controller will schedule a repaint
  3374. */
  3375. Component.prototype.requestRepaint = function requestRepaint() {
  3376. if (this.controller) {
  3377. this.controller.requestRepaint();
  3378. }
  3379. else {
  3380. throw new Error('Cannot request a repaint: no controller configured');
  3381. // TODO: just do a repaint when no parent is configured?
  3382. }
  3383. };
  3384. /**
  3385. * Request a reflow. The controller will schedule a reflow
  3386. */
  3387. Component.prototype.requestReflow = function requestReflow() {
  3388. if (this.controller) {
  3389. this.controller.requestReflow();
  3390. }
  3391. else {
  3392. throw new Error('Cannot request a reflow: no controller configured');
  3393. // TODO: just do a reflow when no parent is configured?
  3394. }
  3395. };
  3396. /**
  3397. * A panel can contain components
  3398. * @param {Component} [parent]
  3399. * @param {Component[]} [depends] Components on which this components depends
  3400. * (except for the parent)
  3401. * @param {Object} [options] Available parameters:
  3402. * {String | Number | function} [left]
  3403. * {String | Number | function} [top]
  3404. * {String | Number | function} [width]
  3405. * {String | Number | function} [height]
  3406. * {String | function} [className]
  3407. * @constructor Panel
  3408. * @extends Component
  3409. */
  3410. function Panel(parent, depends, options) {
  3411. this.id = util.randomUUID();
  3412. this.parent = parent;
  3413. this.depends = depends;
  3414. this.options = options || {};
  3415. }
  3416. Panel.prototype = new Component();
  3417. /**
  3418. * Set options. Will extend the current options.
  3419. * @param {Object} [options] Available parameters:
  3420. * {String | function} [className]
  3421. * {String | Number | function} [left]
  3422. * {String | Number | function} [top]
  3423. * {String | Number | function} [width]
  3424. * {String | Number | function} [height]
  3425. */
  3426. Panel.prototype.setOptions = Component.prototype.setOptions;
  3427. /**
  3428. * Get the container element of the panel, which can be used by a child to
  3429. * add its own widgets.
  3430. * @returns {HTMLElement} container
  3431. */
  3432. Panel.prototype.getContainer = function () {
  3433. return this.frame;
  3434. };
  3435. /**
  3436. * Repaint the component
  3437. * @return {Boolean} changed
  3438. */
  3439. Panel.prototype.repaint = function () {
  3440. var changed = 0,
  3441. update = util.updateProperty,
  3442. asSize = util.option.asSize,
  3443. options = this.options,
  3444. frame = this.frame;
  3445. if (!frame) {
  3446. frame = document.createElement('div');
  3447. frame.className = 'panel';
  3448. var className = options.className;
  3449. if (className) {
  3450. if (typeof className == 'function') {
  3451. util.addClassName(frame, String(className()));
  3452. }
  3453. else {
  3454. util.addClassName(frame, String(className));
  3455. }
  3456. }
  3457. this.frame = frame;
  3458. changed += 1;
  3459. }
  3460. if (!frame.parentNode) {
  3461. if (!this.parent) {
  3462. throw new Error('Cannot repaint panel: no parent attached');
  3463. }
  3464. var parentContainer = this.parent.getContainer();
  3465. if (!parentContainer) {
  3466. throw new Error('Cannot repaint panel: parent has no container element');
  3467. }
  3468. parentContainer.appendChild(frame);
  3469. changed += 1;
  3470. }
  3471. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3472. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3473. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3474. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3475. return (changed > 0);
  3476. };
  3477. /**
  3478. * Reflow the component
  3479. * @return {Boolean} resized
  3480. */
  3481. Panel.prototype.reflow = function () {
  3482. var changed = 0,
  3483. update = util.updateProperty,
  3484. frame = this.frame;
  3485. if (frame) {
  3486. changed += update(this, 'top', frame.offsetTop);
  3487. changed += update(this, 'left', frame.offsetLeft);
  3488. changed += update(this, 'width', frame.offsetWidth);
  3489. changed += update(this, 'height', frame.offsetHeight);
  3490. }
  3491. else {
  3492. changed += 1;
  3493. }
  3494. return (changed > 0);
  3495. };
  3496. /**
  3497. * A root panel can hold components. The root panel must be initialized with
  3498. * a DOM element as container.
  3499. * @param {HTMLElement} container
  3500. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3501. * @constructor RootPanel
  3502. * @extends Panel
  3503. */
  3504. function RootPanel(container, options) {
  3505. this.id = util.randomUUID();
  3506. this.container = container;
  3507. this.options = options || {};
  3508. this.defaultOptions = {
  3509. autoResize: true
  3510. };
  3511. this.listeners = {}; // event listeners
  3512. }
  3513. RootPanel.prototype = new Panel();
  3514. /**
  3515. * Set options. Will extend the current options.
  3516. * @param {Object} [options] Available parameters:
  3517. * {String | function} [className]
  3518. * {String | Number | function} [left]
  3519. * {String | Number | function} [top]
  3520. * {String | Number | function} [width]
  3521. * {String | Number | function} [height]
  3522. * {Boolean | function} [autoResize]
  3523. */
  3524. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3525. /**
  3526. * Repaint the component
  3527. * @return {Boolean} changed
  3528. */
  3529. RootPanel.prototype.repaint = function () {
  3530. var changed = 0,
  3531. update = util.updateProperty,
  3532. asSize = util.option.asSize,
  3533. options = this.options,
  3534. frame = this.frame;
  3535. if (!frame) {
  3536. frame = document.createElement('div');
  3537. frame.className = 'graph panel';
  3538. var className = options.className;
  3539. if (className) {
  3540. util.addClassName(frame, util.option.asString(className));
  3541. }
  3542. this.frame = frame;
  3543. changed += 1;
  3544. }
  3545. if (!frame.parentNode) {
  3546. if (!this.container) {
  3547. throw new Error('Cannot repaint root panel: no container attached');
  3548. }
  3549. this.container.appendChild(frame);
  3550. changed += 1;
  3551. }
  3552. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3553. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3554. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3555. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3556. this._updateEventEmitters();
  3557. this._updateWatch();
  3558. return (changed > 0);
  3559. };
  3560. /**
  3561. * Reflow the component
  3562. * @return {Boolean} resized
  3563. */
  3564. RootPanel.prototype.reflow = function () {
  3565. var changed = 0,
  3566. update = util.updateProperty,
  3567. frame = this.frame;
  3568. if (frame) {
  3569. changed += update(this, 'top', frame.offsetTop);
  3570. changed += update(this, 'left', frame.offsetLeft);
  3571. changed += update(this, 'width', frame.offsetWidth);
  3572. changed += update(this, 'height', frame.offsetHeight);
  3573. }
  3574. else {
  3575. changed += 1;
  3576. }
  3577. return (changed > 0);
  3578. };
  3579. /**
  3580. * Update watching for resize, depending on the current option
  3581. * @private
  3582. */
  3583. RootPanel.prototype._updateWatch = function () {
  3584. var autoResize = this.getOption('autoResize');
  3585. if (autoResize) {
  3586. this._watch();
  3587. }
  3588. else {
  3589. this._unwatch();
  3590. }
  3591. };
  3592. /**
  3593. * Watch for changes in the size of the frame. On resize, the Panel will
  3594. * automatically redraw itself.
  3595. * @private
  3596. */
  3597. RootPanel.prototype._watch = function () {
  3598. var me = this;
  3599. this._unwatch();
  3600. var checkSize = function () {
  3601. var autoResize = me.getOption('autoResize');
  3602. if (!autoResize) {
  3603. // stop watching when the option autoResize is changed to false
  3604. me._unwatch();
  3605. return;
  3606. }
  3607. if (me.frame) {
  3608. // check whether the frame is resized
  3609. if ((me.frame.clientWidth != me.width) ||
  3610. (me.frame.clientHeight != me.height)) {
  3611. me.requestReflow();
  3612. }
  3613. }
  3614. };
  3615. // TODO: automatically cleanup the event listener when the frame is deleted
  3616. util.addEventListener(window, 'resize', checkSize);
  3617. this.watchTimer = setInterval(checkSize, 1000);
  3618. };
  3619. /**
  3620. * Stop watching for a resize of the frame.
  3621. * @private
  3622. */
  3623. RootPanel.prototype._unwatch = function () {
  3624. if (this.watchTimer) {
  3625. clearInterval(this.watchTimer);
  3626. this.watchTimer = undefined;
  3627. }
  3628. // TODO: remove event listener on window.resize
  3629. };
  3630. /**
  3631. * Event handler
  3632. * @param {String} event name of the event, for example 'click', 'mousemove'
  3633. * @param {function} callback callback handler, invoked with the raw HTML Event
  3634. * as parameter.
  3635. */
  3636. RootPanel.prototype.on = function (event, callback) {
  3637. // register the listener at this component
  3638. var arr = this.listeners[event];
  3639. if (!arr) {
  3640. arr = [];
  3641. this.listeners[event] = arr;
  3642. }
  3643. arr.push(callback);
  3644. this._updateEventEmitters();
  3645. };
  3646. /**
  3647. * Update the event listeners for all event emitters
  3648. * @private
  3649. */
  3650. RootPanel.prototype._updateEventEmitters = function () {
  3651. if (this.listeners) {
  3652. var me = this;
  3653. util.forEach(this.listeners, function (listeners, event) {
  3654. if (!me.emitters) {
  3655. me.emitters = {};
  3656. }
  3657. if (!(event in me.emitters)) {
  3658. // create event
  3659. var frame = me.frame;
  3660. if (frame) {
  3661. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3662. var callback = function(event) {
  3663. listeners.forEach(function (listener) {
  3664. // TODO: filter on event target!
  3665. listener(event);
  3666. });
  3667. };
  3668. me.emitters[event] = callback;
  3669. util.addEventListener(frame, event, callback);
  3670. }
  3671. }
  3672. });
  3673. // TODO: be able to delete event listeners
  3674. // TODO: be able to move event listeners to a parent when available
  3675. }
  3676. };
  3677. /**
  3678. * A horizontal time axis
  3679. * @param {Component} parent
  3680. * @param {Component[]} [depends] Components on which this components depends
  3681. * (except for the parent)
  3682. * @param {Object} [options] See TimeAxis.setOptions for the available
  3683. * options.
  3684. * @constructor TimeAxis
  3685. * @extends Component
  3686. */
  3687. function TimeAxis (parent, depends, options) {
  3688. this.id = util.randomUUID();
  3689. this.parent = parent;
  3690. this.depends = depends;
  3691. this.dom = {
  3692. majorLines: [],
  3693. majorTexts: [],
  3694. minorLines: [],
  3695. minorTexts: [],
  3696. redundant: {
  3697. majorLines: [],
  3698. majorTexts: [],
  3699. minorLines: [],
  3700. minorTexts: []
  3701. }
  3702. };
  3703. this.props = {
  3704. range: {
  3705. start: 0,
  3706. end: 0,
  3707. minimumStep: 0
  3708. },
  3709. lineTop: 0
  3710. };
  3711. this.options = options || {};
  3712. this.defaultOptions = {
  3713. orientation: 'bottom', // supported: 'top', 'bottom'
  3714. // TODO: implement timeaxis orientations 'left' and 'right'
  3715. showMinorLabels: true,
  3716. showMajorLabels: true
  3717. };
  3718. this.conversion = null;
  3719. this.range = null;
  3720. }
  3721. TimeAxis.prototype = new Component();
  3722. // TODO: comment options
  3723. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3724. /**
  3725. * Set a range (start and end)
  3726. * @param {Range | Object} range A Range or an object containing start and end.
  3727. */
  3728. TimeAxis.prototype.setRange = function (range) {
  3729. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3730. throw new TypeError('Range must be an instance of Range, ' +
  3731. 'or an object containing start and end.');
  3732. }
  3733. this.range = range;
  3734. };
  3735. /**
  3736. * Convert a position on screen (pixels) to a datetime
  3737. * @param {int} x Position on the screen in pixels
  3738. * @return {Date} time The datetime the corresponds with given position x
  3739. */
  3740. TimeAxis.prototype.toTime = function(x) {
  3741. var conversion = this.conversion;
  3742. return new Date(x / conversion.factor + conversion.offset);
  3743. };
  3744. /**
  3745. * Convert a datetime (Date object) into a position on the screen
  3746. * @param {Date} time A date
  3747. * @return {int} x The position on the screen in pixels which corresponds
  3748. * with the given date.
  3749. * @private
  3750. */
  3751. TimeAxis.prototype.toScreen = function(time) {
  3752. var conversion = this.conversion;
  3753. return (time.valueOf() - conversion.offset) * conversion.factor;
  3754. };
  3755. /**
  3756. * Repaint the component
  3757. * @return {Boolean} changed
  3758. */
  3759. TimeAxis.prototype.repaint = function () {
  3760. var changed = 0,
  3761. update = util.updateProperty,
  3762. asSize = util.option.asSize,
  3763. options = this.options,
  3764. orientation = this.getOption('orientation'),
  3765. props = this.props,
  3766. step = this.step;
  3767. var frame = this.frame;
  3768. if (!frame) {
  3769. frame = document.createElement('div');
  3770. this.frame = frame;
  3771. changed += 1;
  3772. }
  3773. frame.className = 'axis ' + orientation;
  3774. // TODO: custom className?
  3775. if (!frame.parentNode) {
  3776. if (!this.parent) {
  3777. throw new Error('Cannot repaint time axis: no parent attached');
  3778. }
  3779. var parentContainer = this.parent.getContainer();
  3780. if (!parentContainer) {
  3781. throw new Error('Cannot repaint time axis: parent has no container element');
  3782. }
  3783. parentContainer.appendChild(frame);
  3784. changed += 1;
  3785. }
  3786. var parent = frame.parentNode;
  3787. if (parent) {
  3788. var beforeChild = frame.nextSibling;
  3789. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3790. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3791. (this.props.parentHeight - this.height) + 'px' :
  3792. '0px';
  3793. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3794. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3795. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3796. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3797. // get characters width and height
  3798. this._repaintMeasureChars();
  3799. if (this.step) {
  3800. this._repaintStart();
  3801. step.first();
  3802. var xFirstMajorLabel = undefined;
  3803. var max = 0;
  3804. while (step.hasNext() && max < 1000) {
  3805. max++;
  3806. var cur = step.getCurrent(),
  3807. x = this.toScreen(cur),
  3808. isMajor = step.isMajor();
  3809. // TODO: lines must have a width, such that we can create css backgrounds
  3810. if (this.getOption('showMinorLabels')) {
  3811. this._repaintMinorText(x, step.getLabelMinor());
  3812. }
  3813. if (isMajor && this.getOption('showMajorLabels')) {
  3814. if (x > 0) {
  3815. if (xFirstMajorLabel == undefined) {
  3816. xFirstMajorLabel = x;
  3817. }
  3818. this._repaintMajorText(x, step.getLabelMajor());
  3819. }
  3820. this._repaintMajorLine(x);
  3821. }
  3822. else {
  3823. this._repaintMinorLine(x);
  3824. }
  3825. step.next();
  3826. }
  3827. // create a major label on the left when needed
  3828. if (this.getOption('showMajorLabels')) {
  3829. var leftTime = this.toTime(0),
  3830. leftText = step.getLabelMajor(leftTime),
  3831. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3832. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3833. this._repaintMajorText(0, leftText);
  3834. }
  3835. }
  3836. this._repaintEnd();
  3837. }
  3838. this._repaintLine();
  3839. // put frame online again
  3840. if (beforeChild) {
  3841. parent.insertBefore(frame, beforeChild);
  3842. }
  3843. else {
  3844. parent.appendChild(frame)
  3845. }
  3846. }
  3847. return (changed > 0);
  3848. };
  3849. /**
  3850. * Start a repaint. Move all DOM elements to a redundant list, where they
  3851. * can be picked for re-use, or can be cleaned up in the end
  3852. * @private
  3853. */
  3854. TimeAxis.prototype._repaintStart = function () {
  3855. var dom = this.dom,
  3856. redundant = dom.redundant;
  3857. redundant.majorLines = dom.majorLines;
  3858. redundant.majorTexts = dom.majorTexts;
  3859. redundant.minorLines = dom.minorLines;
  3860. redundant.minorTexts = dom.minorTexts;
  3861. dom.majorLines = [];
  3862. dom.majorTexts = [];
  3863. dom.minorLines = [];
  3864. dom.minorTexts = [];
  3865. };
  3866. /**
  3867. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3868. * @private
  3869. */
  3870. TimeAxis.prototype._repaintEnd = function () {
  3871. util.forEach(this.dom.redundant, function (arr) {
  3872. while (arr.length) {
  3873. var elem = arr.pop();
  3874. if (elem && elem.parentNode) {
  3875. elem.parentNode.removeChild(elem);
  3876. }
  3877. }
  3878. });
  3879. };
  3880. /**
  3881. * Create a minor label for the axis at position x
  3882. * @param {Number} x
  3883. * @param {String} text
  3884. * @private
  3885. */
  3886. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3887. // reuse redundant label
  3888. var label = this.dom.redundant.minorTexts.shift();
  3889. if (!label) {
  3890. // create new label
  3891. var content = document.createTextNode('');
  3892. label = document.createElement('div');
  3893. label.appendChild(content);
  3894. label.className = 'text minor';
  3895. this.frame.appendChild(label);
  3896. }
  3897. this.dom.minorTexts.push(label);
  3898. label.childNodes[0].nodeValue = text;
  3899. label.style.left = x + 'px';
  3900. label.style.top = this.props.minorLabelTop + 'px';
  3901. //label.title = title; // TODO: this is a heavy operation
  3902. };
  3903. /**
  3904. * Create a Major label for the axis at position x
  3905. * @param {Number} x
  3906. * @param {String} text
  3907. * @private
  3908. */
  3909. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3910. // reuse redundant label
  3911. var label = this.dom.redundant.majorTexts.shift();
  3912. if (!label) {
  3913. // create label
  3914. var content = document.createTextNode(text);
  3915. label = document.createElement('div');
  3916. label.className = 'text major';
  3917. label.appendChild(content);
  3918. this.frame.appendChild(label);
  3919. }
  3920. this.dom.majorTexts.push(label);
  3921. label.childNodes[0].nodeValue = text;
  3922. label.style.top = this.props.majorLabelTop + 'px';
  3923. label.style.left = x + 'px';
  3924. //label.title = title; // TODO: this is a heavy operation
  3925. };
  3926. /**
  3927. * Create a minor line for the axis at position x
  3928. * @param {Number} x
  3929. * @private
  3930. */
  3931. TimeAxis.prototype._repaintMinorLine = function (x) {
  3932. // reuse redundant line
  3933. var line = this.dom.redundant.minorLines.shift();
  3934. if (!line) {
  3935. // create vertical line
  3936. line = document.createElement('div');
  3937. line.className = 'grid vertical minor';
  3938. this.frame.appendChild(line);
  3939. }
  3940. this.dom.minorLines.push(line);
  3941. var props = this.props;
  3942. line.style.top = props.minorLineTop + 'px';
  3943. line.style.height = props.minorLineHeight + 'px';
  3944. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3945. };
  3946. /**
  3947. * Create a Major line for the axis at position x
  3948. * @param {Number} x
  3949. * @private
  3950. */
  3951. TimeAxis.prototype._repaintMajorLine = function (x) {
  3952. // reuse redundant line
  3953. var line = this.dom.redundant.majorLines.shift();
  3954. if (!line) {
  3955. // create vertical line
  3956. line = document.createElement('DIV');
  3957. line.className = 'grid vertical major';
  3958. this.frame.appendChild(line);
  3959. }
  3960. this.dom.majorLines.push(line);
  3961. var props = this.props;
  3962. line.style.top = props.majorLineTop + 'px';
  3963. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3964. line.style.height = props.majorLineHeight + 'px';
  3965. };
  3966. /**
  3967. * Repaint the horizontal line for the axis
  3968. * @private
  3969. */
  3970. TimeAxis.prototype._repaintLine = function() {
  3971. var line = this.dom.line,
  3972. frame = this.frame,
  3973. options = this.options;
  3974. // line before all axis elements
  3975. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3976. if (line) {
  3977. // put this line at the end of all childs
  3978. frame.removeChild(line);
  3979. frame.appendChild(line);
  3980. }
  3981. else {
  3982. // create the axis line
  3983. line = document.createElement('div');
  3984. line.className = 'grid horizontal major';
  3985. frame.appendChild(line);
  3986. this.dom.line = line;
  3987. }
  3988. line.style.top = this.props.lineTop + 'px';
  3989. }
  3990. else {
  3991. if (line && axis.parentElement) {
  3992. frame.removeChild(axis.line);
  3993. delete this.dom.line;
  3994. }
  3995. }
  3996. };
  3997. /**
  3998. * Create characters used to determine the size of text on the axis
  3999. * @private
  4000. */
  4001. TimeAxis.prototype._repaintMeasureChars = function () {
  4002. // calculate the width and height of a single character
  4003. // this is used to calculate the step size, and also the positioning of the
  4004. // axis
  4005. var dom = this.dom,
  4006. text;
  4007. if (!dom.measureCharMinor) {
  4008. text = document.createTextNode('0');
  4009. var measureCharMinor = document.createElement('DIV');
  4010. measureCharMinor.className = 'text minor measure';
  4011. measureCharMinor.appendChild(text);
  4012. this.frame.appendChild(measureCharMinor);
  4013. dom.measureCharMinor = measureCharMinor;
  4014. }
  4015. if (!dom.measureCharMajor) {
  4016. text = document.createTextNode('0');
  4017. var measureCharMajor = document.createElement('DIV');
  4018. measureCharMajor.className = 'text major measure';
  4019. measureCharMajor.appendChild(text);
  4020. this.frame.appendChild(measureCharMajor);
  4021. dom.measureCharMajor = measureCharMajor;
  4022. }
  4023. };
  4024. /**
  4025. * Reflow the component
  4026. * @return {Boolean} resized
  4027. */
  4028. TimeAxis.prototype.reflow = function () {
  4029. var changed = 0,
  4030. update = util.updateProperty,
  4031. frame = this.frame,
  4032. range = this.range;
  4033. if (!range) {
  4034. throw new Error('Cannot repaint time axis: no range configured');
  4035. }
  4036. if (frame) {
  4037. changed += update(this, 'top', frame.offsetTop);
  4038. changed += update(this, 'left', frame.offsetLeft);
  4039. // calculate size of a character
  4040. var props = this.props,
  4041. showMinorLabels = this.getOption('showMinorLabels'),
  4042. showMajorLabels = this.getOption('showMajorLabels'),
  4043. measureCharMinor = this.dom.measureCharMinor,
  4044. measureCharMajor = this.dom.measureCharMajor;
  4045. if (measureCharMinor) {
  4046. props.minorCharHeight = measureCharMinor.clientHeight;
  4047. props.minorCharWidth = measureCharMinor.clientWidth;
  4048. }
  4049. if (measureCharMajor) {
  4050. props.majorCharHeight = measureCharMajor.clientHeight;
  4051. props.majorCharWidth = measureCharMajor.clientWidth;
  4052. }
  4053. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4054. if (parentHeight != props.parentHeight) {
  4055. props.parentHeight = parentHeight;
  4056. changed += 1;
  4057. }
  4058. switch (this.getOption('orientation')) {
  4059. case 'bottom':
  4060. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4061. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4062. props.minorLabelTop = 0;
  4063. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4064. props.minorLineTop = -this.top;
  4065. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4066. props.minorLineWidth = 1; // TODO: really calculate width
  4067. props.majorLineTop = -this.top;
  4068. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4069. props.majorLineWidth = 1; // TODO: really calculate width
  4070. props.lineTop = 0;
  4071. break;
  4072. case 'top':
  4073. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4074. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4075. props.majorLabelTop = 0;
  4076. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4077. props.minorLineTop = props.minorLabelTop;
  4078. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4079. props.minorLineWidth = 1; // TODO: really calculate width
  4080. props.majorLineTop = 0;
  4081. props.majorLineHeight = Math.max(parentHeight - this.top);
  4082. props.majorLineWidth = 1; // TODO: really calculate width
  4083. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4084. break;
  4085. default:
  4086. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4087. }
  4088. var height = props.minorLabelHeight + props.majorLabelHeight;
  4089. changed += update(this, 'width', frame.offsetWidth);
  4090. changed += update(this, 'height', height);
  4091. // calculate range and step
  4092. this._updateConversion();
  4093. var start = util.cast(range.start, 'Date'),
  4094. end = util.cast(range.end, 'Date'),
  4095. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  4096. this.step = new TimeStep(start, end, minimumStep);
  4097. changed += update(props.range, 'start', start.valueOf());
  4098. changed += update(props.range, 'end', end.valueOf());
  4099. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4100. }
  4101. return (changed > 0);
  4102. };
  4103. /**
  4104. * Calculate the factor and offset to convert a position on screen to the
  4105. * corresponding date and vice versa.
  4106. * After the method _updateConversion is executed once, the methods toTime
  4107. * and toScreen can be used.
  4108. * @private
  4109. */
  4110. TimeAxis.prototype._updateConversion = function() {
  4111. var range = this.range;
  4112. if (!range) {
  4113. throw new Error('No range configured');
  4114. }
  4115. if (range.conversion) {
  4116. this.conversion = range.conversion(this.width);
  4117. }
  4118. else {
  4119. this.conversion = Range.conversion(range.start, range.end, this.width);
  4120. }
  4121. };
  4122. /**
  4123. * An ItemSet holds a set of items and ranges which can be displayed in a
  4124. * range. The width is determined by the parent of the ItemSet, and the height
  4125. * is determined by the size of the items.
  4126. * @param {Component} parent
  4127. * @param {Component[]} [depends] Components on which this components depends
  4128. * (except for the parent)
  4129. * @param {Object} [options] See ItemSet.setOptions for the available
  4130. * options.
  4131. * @constructor ItemSet
  4132. * @extends Panel
  4133. */
  4134. // TODO: improve performance by replacing all Array.forEach with a for loop
  4135. function ItemSet(parent, depends, options) {
  4136. this.id = util.randomUUID();
  4137. this.parent = parent;
  4138. this.depends = depends;
  4139. // one options object is shared by this itemset and all its items
  4140. this.options = options || {};
  4141. this.defaultOptions = {
  4142. style: 'box',
  4143. align: 'center',
  4144. orientation: 'bottom',
  4145. margin: {
  4146. axis: 20,
  4147. item: 10
  4148. },
  4149. padding: 5
  4150. };
  4151. this.dom = {};
  4152. var me = this;
  4153. this.itemsData = null; // DataSet
  4154. this.range = null; // Range or Object {start: number, end: number}
  4155. this.listeners = {
  4156. 'add': function (event, params, senderId) {
  4157. if (senderId != me.id) {
  4158. me._onAdd(params.items);
  4159. }
  4160. },
  4161. 'update': function (event, params, senderId) {
  4162. if (senderId != me.id) {
  4163. me._onUpdate(params.items);
  4164. }
  4165. },
  4166. 'remove': function (event, params, senderId) {
  4167. if (senderId != me.id) {
  4168. me._onRemove(params.items);
  4169. }
  4170. }
  4171. };
  4172. this.items = {}; // object with an Item for every data item
  4173. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4174. this.stack = new Stack(this, Object.create(this.options));
  4175. this.conversion = null;
  4176. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4177. }
  4178. ItemSet.prototype = new Panel();
  4179. // available item types will be registered here
  4180. ItemSet.types = {
  4181. box: ItemBox,
  4182. range: ItemRange,
  4183. point: ItemPoint
  4184. };
  4185. /**
  4186. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4187. * @param {Object} [options] The following options are available:
  4188. * {String | function} [className]
  4189. * class name for the itemset
  4190. * {String} [style]
  4191. * Default style for the items. Choose from 'box'
  4192. * (default), 'point', or 'range'. The default
  4193. * Style can be overwritten by individual items.
  4194. * {String} align
  4195. * Alignment for the items, only applicable for
  4196. * ItemBox. Choose 'center' (default), 'left', or
  4197. * 'right'.
  4198. * {String} orientation
  4199. * Orientation of the item set. Choose 'top' or
  4200. * 'bottom' (default).
  4201. * {Number} margin.axis
  4202. * Margin between the axis and the items in pixels.
  4203. * Default is 20.
  4204. * {Number} margin.item
  4205. * Margin between items in pixels. Default is 10.
  4206. * {Number} padding
  4207. * Padding of the contents of an item in pixels.
  4208. * Must correspond with the items css. Default is 5.
  4209. */
  4210. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4211. /**
  4212. * Set range (start and end).
  4213. * @param {Range | Object} range A Range or an object containing start and end.
  4214. */
  4215. ItemSet.prototype.setRange = function setRange(range) {
  4216. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4217. throw new TypeError('Range must be an instance of Range, ' +
  4218. 'or an object containing start and end.');
  4219. }
  4220. this.range = range;
  4221. };
  4222. /**
  4223. * Repaint the component
  4224. * @return {Boolean} changed
  4225. */
  4226. ItemSet.prototype.repaint = function repaint() {
  4227. var changed = 0,
  4228. update = util.updateProperty,
  4229. asSize = util.option.asSize,
  4230. options = this.options,
  4231. orientation = this.getOption('orientation'),
  4232. defaultOptions = this.defaultOptions,
  4233. frame = this.frame;
  4234. if (!frame) {
  4235. frame = document.createElement('div');
  4236. frame.className = 'itemset';
  4237. var className = options.className;
  4238. if (className) {
  4239. util.addClassName(frame, util.option.asString(className));
  4240. }
  4241. // create background panel
  4242. var background = document.createElement('div');
  4243. background.className = 'background';
  4244. frame.appendChild(background);
  4245. this.dom.background = background;
  4246. // create foreground panel
  4247. var foreground = document.createElement('div');
  4248. foreground.className = 'foreground';
  4249. frame.appendChild(foreground);
  4250. this.dom.foreground = foreground;
  4251. // create axis panel
  4252. var axis = document.createElement('div');
  4253. axis.className = 'itemset-axis';
  4254. //frame.appendChild(axis);
  4255. this.dom.axis = axis;
  4256. this.frame = frame;
  4257. changed += 1;
  4258. }
  4259. if (!this.parent) {
  4260. throw new Error('Cannot repaint itemset: no parent attached');
  4261. }
  4262. var parentContainer = this.parent.getContainer();
  4263. if (!parentContainer) {
  4264. throw new Error('Cannot repaint itemset: parent has no container element');
  4265. }
  4266. if (!frame.parentNode) {
  4267. parentContainer.appendChild(frame);
  4268. changed += 1;
  4269. }
  4270. if (!this.dom.axis.parentNode) {
  4271. parentContainer.appendChild(this.dom.axis);
  4272. changed += 1;
  4273. }
  4274. // reposition frame
  4275. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4276. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4277. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4278. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4279. // reposition axis
  4280. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4281. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4282. if (orientation == 'bottom') {
  4283. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4284. }
  4285. else { // orientation == 'top'
  4286. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4287. }
  4288. this._updateConversion();
  4289. var me = this,
  4290. queue = this.queue,
  4291. itemsData = this.itemsData,
  4292. items = this.items,
  4293. dataOptions = {
  4294. fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type']
  4295. };
  4296. // TODO: copy options from the itemset itself?
  4297. // show/hide added/changed/removed items
  4298. Object.keys(queue).forEach(function (id) {
  4299. //var entry = queue[id];
  4300. var action = queue[id];
  4301. var item = items[id];
  4302. //var item = entry.item;
  4303. //noinspection FallthroughInSwitchStatementJS
  4304. switch (action) {
  4305. case 'add':
  4306. case 'update':
  4307. var itemData = itemsData && itemsData.get(id, dataOptions);
  4308. if (itemData) {
  4309. var type = itemData.type ||
  4310. (itemData.start && itemData.end && 'range') ||
  4311. 'box';
  4312. var constructor = ItemSet.types[type];
  4313. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4314. if (item) {
  4315. // update item
  4316. if (!constructor || !(item instanceof constructor)) {
  4317. // item type has changed, hide and delete the item
  4318. changed += item.hide();
  4319. item = null;
  4320. }
  4321. else {
  4322. item.data = itemData; // TODO: create a method item.setData ?
  4323. changed++;
  4324. }
  4325. }
  4326. if (!item) {
  4327. // create item
  4328. if (constructor) {
  4329. item = new constructor(me, itemData, options, defaultOptions);
  4330. changed++;
  4331. }
  4332. else {
  4333. throw new TypeError('Unknown item type "' + type + '"');
  4334. }
  4335. }
  4336. // force a repaint (not only a reposition)
  4337. item.repaint();
  4338. items[id] = item;
  4339. }
  4340. // update queue
  4341. delete queue[id];
  4342. break;
  4343. case 'remove':
  4344. if (item) {
  4345. // remove DOM of the item
  4346. changed += item.hide();
  4347. }
  4348. // update lists
  4349. delete items[id];
  4350. delete queue[id];
  4351. break;
  4352. default:
  4353. console.log('Error: unknown action "' + action + '"');
  4354. }
  4355. });
  4356. // reposition all items. Show items only when in the visible area
  4357. util.forEach(this.items, function (item) {
  4358. if (item.visible) {
  4359. changed += item.show();
  4360. item.reposition();
  4361. }
  4362. else {
  4363. changed += item.hide();
  4364. }
  4365. });
  4366. return (changed > 0);
  4367. };
  4368. /**
  4369. * Get the foreground container element
  4370. * @return {HTMLElement} foreground
  4371. */
  4372. ItemSet.prototype.getForeground = function getForeground() {
  4373. return this.dom.foreground;
  4374. };
  4375. /**
  4376. * Get the background container element
  4377. * @return {HTMLElement} background
  4378. */
  4379. ItemSet.prototype.getBackground = function getBackground() {
  4380. return this.dom.background;
  4381. };
  4382. /**
  4383. * Get the axis container element
  4384. * @return {HTMLElement} axis
  4385. */
  4386. ItemSet.prototype.getAxis = function getAxis() {
  4387. return this.dom.axis;
  4388. };
  4389. /**
  4390. * Reflow the component
  4391. * @return {Boolean} resized
  4392. */
  4393. ItemSet.prototype.reflow = function reflow () {
  4394. var changed = 0,
  4395. options = this.options,
  4396. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4397. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4398. update = util.updateProperty,
  4399. asNumber = util.option.asNumber,
  4400. asSize = util.option.asSize,
  4401. frame = this.frame;
  4402. if (frame) {
  4403. this._updateConversion();
  4404. util.forEach(this.items, function (item) {
  4405. changed += item.reflow();
  4406. });
  4407. // TODO: stack.update should be triggered via an event, in stack itself
  4408. // TODO: only update the stack when there are changed items
  4409. this.stack.update();
  4410. var maxHeight = asNumber(options.maxHeight);
  4411. var fixedHeight = (asSize(options.height) != null);
  4412. var height;
  4413. if (fixedHeight) {
  4414. height = frame.offsetHeight;
  4415. }
  4416. else {
  4417. // height is not specified, determine the height from the height and positioned items
  4418. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4419. if (visibleItems.length) {
  4420. var min = visibleItems[0].top;
  4421. var max = visibleItems[0].top + visibleItems[0].height;
  4422. util.forEach(visibleItems, function (item) {
  4423. min = Math.min(min, item.top);
  4424. max = Math.max(max, (item.top + item.height));
  4425. });
  4426. height = (max - min) + marginAxis + marginItem;
  4427. }
  4428. else {
  4429. height = marginAxis + marginItem;
  4430. }
  4431. }
  4432. if (maxHeight != null) {
  4433. height = Math.min(height, maxHeight);
  4434. }
  4435. changed += update(this, 'height', height);
  4436. // calculate height from items
  4437. changed += update(this, 'top', frame.offsetTop);
  4438. changed += update(this, 'left', frame.offsetLeft);
  4439. changed += update(this, 'width', frame.offsetWidth);
  4440. }
  4441. else {
  4442. changed += 1;
  4443. }
  4444. return (changed > 0);
  4445. };
  4446. /**
  4447. * Hide this component from the DOM
  4448. * @return {Boolean} changed
  4449. */
  4450. ItemSet.prototype.hide = function hide() {
  4451. var changed = false;
  4452. // remove the DOM
  4453. if (this.frame && this.frame.parentNode) {
  4454. this.frame.parentNode.removeChild(this.frame);
  4455. changed = true;
  4456. }
  4457. if (this.dom.axis && this.dom.axis.parentNode) {
  4458. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4459. changed = true;
  4460. }
  4461. return changed;
  4462. };
  4463. /**
  4464. * Set items
  4465. * @param {vis.DataSet | null} items
  4466. */
  4467. ItemSet.prototype.setItems = function setItems(items) {
  4468. var me = this,
  4469. ids;
  4470. // unsubscribe from current dataset
  4471. var current = this.itemsData;
  4472. if (current) {
  4473. util.forEach(this.listeners, function (callback, event) {
  4474. current.unsubscribe(event, callback);
  4475. });
  4476. // remove all drawn items
  4477. ids = current.getIds();
  4478. this._onRemove(ids);
  4479. }
  4480. // replace the dataset
  4481. if (!items) {
  4482. this.itemsData = null;
  4483. }
  4484. else if (items instanceof DataSet || items instanceof DataView) {
  4485. this.itemsData = items;
  4486. }
  4487. else {
  4488. throw new TypeError('Data must be an instance of DataSet');
  4489. }
  4490. if (this.itemsData) {
  4491. // subscribe to new dataset
  4492. var id = this.id;
  4493. util.forEach(this.listeners, function (callback, event) {
  4494. me.itemsData.subscribe(event, callback, id);
  4495. });
  4496. // draw all new items
  4497. ids = this.itemsData.getIds();
  4498. this._onAdd(ids);
  4499. }
  4500. };
  4501. /**
  4502. * Get the current items items
  4503. * @returns {vis.DataSet | null}
  4504. */
  4505. ItemSet.prototype.getItems = function getItems() {
  4506. return this.itemsData;
  4507. };
  4508. /**
  4509. * Handle updated items
  4510. * @param {Number[]} ids
  4511. * @private
  4512. */
  4513. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4514. this._toQueue('update', ids);
  4515. };
  4516. /**
  4517. * Handle changed items
  4518. * @param {Number[]} ids
  4519. * @private
  4520. */
  4521. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4522. this._toQueue('add', ids);
  4523. };
  4524. /**
  4525. * Handle removed items
  4526. * @param {Number[]} ids
  4527. * @private
  4528. */
  4529. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4530. this._toQueue('remove', ids);
  4531. };
  4532. /**
  4533. * Put items in the queue to be added/updated/remove
  4534. * @param {String} action can be 'add', 'update', 'remove'
  4535. * @param {Number[]} ids
  4536. */
  4537. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4538. var queue = this.queue;
  4539. ids.forEach(function (id) {
  4540. queue[id] = action;
  4541. });
  4542. if (this.controller) {
  4543. //this.requestReflow();
  4544. this.requestRepaint();
  4545. }
  4546. };
  4547. /**
  4548. * Calculate the factor and offset to convert a position on screen to the
  4549. * corresponding date and vice versa.
  4550. * After the method _updateConversion is executed once, the methods toTime
  4551. * and toScreen can be used.
  4552. * @private
  4553. */
  4554. ItemSet.prototype._updateConversion = function _updateConversion() {
  4555. var range = this.range;
  4556. if (!range) {
  4557. throw new Error('No range configured');
  4558. }
  4559. if (range.conversion) {
  4560. this.conversion = range.conversion(this.width);
  4561. }
  4562. else {
  4563. this.conversion = Range.conversion(range.start, range.end, this.width);
  4564. }
  4565. };
  4566. /**
  4567. * Convert a position on screen (pixels) to a datetime
  4568. * Before this method can be used, the method _updateConversion must be
  4569. * executed once.
  4570. * @param {int} x Position on the screen in pixels
  4571. * @return {Date} time The datetime the corresponds with given position x
  4572. */
  4573. ItemSet.prototype.toTime = function toTime(x) {
  4574. var conversion = this.conversion;
  4575. return new Date(x / conversion.factor + conversion.offset);
  4576. };
  4577. /**
  4578. * Convert a datetime (Date object) into a position on the screen
  4579. * Before this method can be used, the method _updateConversion must be
  4580. * executed once.
  4581. * @param {Date} time A date
  4582. * @return {int} x The position on the screen in pixels which corresponds
  4583. * with the given date.
  4584. */
  4585. ItemSet.prototype.toScreen = function toScreen(time) {
  4586. var conversion = this.conversion;
  4587. return (time.valueOf() - conversion.offset) * conversion.factor;
  4588. };
  4589. /**
  4590. * @constructor Item
  4591. * @param {ItemSet} parent
  4592. * @param {Object} data Object containing (optional) parameters type,
  4593. * start, end, content, group, className.
  4594. * @param {Object} [options] Options to set initial property values
  4595. * @param {Object} [defaultOptions] default options
  4596. * // TODO: describe available options
  4597. */
  4598. function Item (parent, data, options, defaultOptions) {
  4599. this.parent = parent;
  4600. this.data = data;
  4601. this.dom = null;
  4602. this.options = options || {};
  4603. this.defaultOptions = defaultOptions || {};
  4604. this.selected = false;
  4605. this.visible = false;
  4606. this.top = 0;
  4607. this.left = 0;
  4608. this.width = 0;
  4609. this.height = 0;
  4610. }
  4611. /**
  4612. * Select current item
  4613. */
  4614. Item.prototype.select = function select() {
  4615. this.selected = true;
  4616. };
  4617. /**
  4618. * Unselect current item
  4619. */
  4620. Item.prototype.unselect = function unselect() {
  4621. this.selected = false;
  4622. };
  4623. /**
  4624. * Show the Item in the DOM (when not already visible)
  4625. * @return {Boolean} changed
  4626. */
  4627. Item.prototype.show = function show() {
  4628. return false;
  4629. };
  4630. /**
  4631. * Hide the Item from the DOM (when visible)
  4632. * @return {Boolean} changed
  4633. */
  4634. Item.prototype.hide = function hide() {
  4635. return false;
  4636. };
  4637. /**
  4638. * Repaint the item
  4639. * @return {Boolean} changed
  4640. */
  4641. Item.prototype.repaint = function repaint() {
  4642. // should be implemented by the item
  4643. return false;
  4644. };
  4645. /**
  4646. * Reflow the item
  4647. * @return {Boolean} resized
  4648. */
  4649. Item.prototype.reflow = function reflow() {
  4650. // should be implemented by the item
  4651. return false;
  4652. };
  4653. /**
  4654. * @constructor ItemBox
  4655. * @extends Item
  4656. * @param {ItemSet} parent
  4657. * @param {Object} data Object containing parameters start
  4658. * content, className.
  4659. * @param {Object} [options] Options to set initial property values
  4660. * @param {Object} [defaultOptions] default options
  4661. * // TODO: describe available options
  4662. */
  4663. function ItemBox (parent, data, options, defaultOptions) {
  4664. this.props = {
  4665. dot: {
  4666. left: 0,
  4667. top: 0,
  4668. width: 0,
  4669. height: 0
  4670. },
  4671. line: {
  4672. top: 0,
  4673. left: 0,
  4674. width: 0,
  4675. height: 0
  4676. }
  4677. };
  4678. Item.call(this, parent, data, options, defaultOptions);
  4679. }
  4680. ItemBox.prototype = new Item (null, null);
  4681. /**
  4682. * Select the item
  4683. * @override
  4684. */
  4685. ItemBox.prototype.select = function select() {
  4686. this.selected = true;
  4687. // TODO: select and unselect
  4688. };
  4689. /**
  4690. * Unselect the item
  4691. * @override
  4692. */
  4693. ItemBox.prototype.unselect = function unselect() {
  4694. this.selected = false;
  4695. // TODO: select and unselect
  4696. };
  4697. /**
  4698. * Repaint the item
  4699. * @return {Boolean} changed
  4700. */
  4701. ItemBox.prototype.repaint = function repaint() {
  4702. // TODO: make an efficient repaint
  4703. var changed = false;
  4704. var dom = this.dom;
  4705. if (!dom) {
  4706. this._create();
  4707. dom = this.dom;
  4708. changed = true;
  4709. }
  4710. if (dom) {
  4711. if (!this.parent) {
  4712. throw new Error('Cannot repaint item: no parent attached');
  4713. }
  4714. var foreground = this.parent.getForeground();
  4715. if (!foreground) {
  4716. throw new Error('Cannot repaint time axis: ' +
  4717. 'parent has no foreground container element');
  4718. }
  4719. var background = this.parent.getBackground();
  4720. if (!background) {
  4721. throw new Error('Cannot repaint time axis: ' +
  4722. 'parent has no background container element');
  4723. }
  4724. var axis = this.parent.getAxis();
  4725. if (!background) {
  4726. throw new Error('Cannot repaint time axis: ' +
  4727. 'parent has no axis container element');
  4728. }
  4729. if (!dom.box.parentNode) {
  4730. foreground.appendChild(dom.box);
  4731. changed = true;
  4732. }
  4733. if (!dom.line.parentNode) {
  4734. background.appendChild(dom.line);
  4735. changed = true;
  4736. }
  4737. if (!dom.dot.parentNode) {
  4738. axis.appendChild(dom.dot);
  4739. changed = true;
  4740. }
  4741. // update contents
  4742. if (this.data.content != this.content) {
  4743. this.content = this.data.content;
  4744. if (this.content instanceof Element) {
  4745. dom.content.innerHTML = '';
  4746. dom.content.appendChild(this.content);
  4747. }
  4748. else if (this.data.content != undefined) {
  4749. dom.content.innerHTML = this.content;
  4750. }
  4751. else {
  4752. throw new Error('Property "content" missing in item ' + this.data.id);
  4753. }
  4754. changed = true;
  4755. }
  4756. // update class
  4757. var className = (this.data.className? ' ' + this.data.className : '') +
  4758. (this.selected ? ' selected' : '');
  4759. if (this.className != className) {
  4760. this.className = className;
  4761. dom.box.className = 'item box' + className;
  4762. dom.line.className = 'item line' + className;
  4763. dom.dot.className = 'item dot' + className;
  4764. changed = true;
  4765. }
  4766. }
  4767. return changed;
  4768. };
  4769. /**
  4770. * Show the item in the DOM (when not already visible). The items DOM will
  4771. * be created when needed.
  4772. * @return {Boolean} changed
  4773. */
  4774. ItemBox.prototype.show = function show() {
  4775. if (!this.dom || !this.dom.box.parentNode) {
  4776. return this.repaint();
  4777. }
  4778. else {
  4779. return false;
  4780. }
  4781. };
  4782. /**
  4783. * Hide the item from the DOM (when visible)
  4784. * @return {Boolean} changed
  4785. */
  4786. ItemBox.prototype.hide = function hide() {
  4787. var changed = false,
  4788. dom = this.dom;
  4789. if (dom) {
  4790. if (dom.box.parentNode) {
  4791. dom.box.parentNode.removeChild(dom.box);
  4792. changed = true;
  4793. }
  4794. if (dom.line.parentNode) {
  4795. dom.line.parentNode.removeChild(dom.line);
  4796. }
  4797. if (dom.dot.parentNode) {
  4798. dom.dot.parentNode.removeChild(dom.dot);
  4799. }
  4800. }
  4801. return changed;
  4802. };
  4803. /**
  4804. * Reflow the item: calculate its actual size and position from the DOM
  4805. * @return {boolean} resized returns true if the axis is resized
  4806. * @override
  4807. */
  4808. ItemBox.prototype.reflow = function reflow() {
  4809. var changed = 0,
  4810. update,
  4811. dom,
  4812. props,
  4813. options,
  4814. margin,
  4815. start,
  4816. align,
  4817. orientation,
  4818. top,
  4819. left,
  4820. data,
  4821. range;
  4822. if (this.data.start == undefined) {
  4823. throw new Error('Property "start" missing in item ' + this.data.id);
  4824. }
  4825. data = this.data;
  4826. range = this.parent && this.parent.range;
  4827. if (data && range) {
  4828. // TODO: account for the width of the item. Take some margin
  4829. this.visible = (data.start > range.start) && (data.start < range.end);
  4830. }
  4831. else {
  4832. this.visible = false;
  4833. }
  4834. if (this.visible) {
  4835. dom = this.dom;
  4836. if (dom) {
  4837. update = util.updateProperty;
  4838. props = this.props;
  4839. options = this.options;
  4840. start = this.parent.toScreen(this.data.start);
  4841. align = options.align || this.defaultOptions.align;
  4842. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  4843. orientation = options.orientation || this.defaultOptions.orientation;
  4844. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4845. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4846. changed += update(props.line, 'width', dom.line.offsetWidth);
  4847. changed += update(props.line, 'height', dom.line.offsetHeight);
  4848. changed += update(props.line, 'top', dom.line.offsetTop);
  4849. changed += update(this, 'width', dom.box.offsetWidth);
  4850. changed += update(this, 'height', dom.box.offsetHeight);
  4851. if (align == 'right') {
  4852. left = start - this.width;
  4853. }
  4854. else if (align == 'left') {
  4855. left = start;
  4856. }
  4857. else {
  4858. // default or 'center'
  4859. left = start - this.width / 2;
  4860. }
  4861. changed += update(this, 'left', left);
  4862. changed += update(props.line, 'left', start - props.line.width / 2);
  4863. changed += update(props.dot, 'left', start - props.dot.width / 2);
  4864. changed += update(props.dot, 'top', -props.dot.height / 2);
  4865. if (orientation == 'top') {
  4866. top = margin;
  4867. changed += update(this, 'top', top);
  4868. }
  4869. else {
  4870. // default or 'bottom'
  4871. var parentHeight = this.parent.height;
  4872. top = parentHeight - this.height - margin;
  4873. changed += update(this, 'top', top);
  4874. }
  4875. }
  4876. else {
  4877. changed += 1;
  4878. }
  4879. }
  4880. return (changed > 0);
  4881. };
  4882. /**
  4883. * Create an items DOM
  4884. * @private
  4885. */
  4886. ItemBox.prototype._create = function _create() {
  4887. var dom = this.dom;
  4888. if (!dom) {
  4889. this.dom = dom = {};
  4890. // create the box
  4891. dom.box = document.createElement('DIV');
  4892. // className is updated in repaint()
  4893. // contents box (inside the background box). used for making margins
  4894. dom.content = document.createElement('DIV');
  4895. dom.content.className = 'content';
  4896. dom.box.appendChild(dom.content);
  4897. // line to axis
  4898. dom.line = document.createElement('DIV');
  4899. dom.line.className = 'line';
  4900. // dot on axis
  4901. dom.dot = document.createElement('DIV');
  4902. dom.dot.className = 'dot';
  4903. }
  4904. };
  4905. /**
  4906. * Reposition the item, recalculate its left, top, and width, using the current
  4907. * range and size of the items itemset
  4908. * @override
  4909. */
  4910. ItemBox.prototype.reposition = function reposition() {
  4911. var dom = this.dom,
  4912. props = this.props,
  4913. orientation = this.options.orientation || this.defaultOptions.orientation;
  4914. if (dom) {
  4915. var box = dom.box,
  4916. line = dom.line,
  4917. dot = dom.dot;
  4918. box.style.left = this.left + 'px';
  4919. box.style.top = this.top + 'px';
  4920. line.style.left = props.line.left + 'px';
  4921. if (orientation == 'top') {
  4922. line.style.top = 0 + 'px';
  4923. line.style.height = this.top + 'px';
  4924. }
  4925. else {
  4926. // orientation 'bottom'
  4927. line.style.top = (this.top + this.height) + 'px';
  4928. line.style.height = Math.max(this.parent.height - this.top - this.height +
  4929. this.props.dot.height / 2, 0) + 'px';
  4930. }
  4931. dot.style.left = props.dot.left + 'px';
  4932. dot.style.top = props.dot.top + 'px';
  4933. }
  4934. };
  4935. /**
  4936. * @constructor ItemPoint
  4937. * @extends Item
  4938. * @param {ItemSet} parent
  4939. * @param {Object} data Object containing parameters start
  4940. * content, className.
  4941. * @param {Object} [options] Options to set initial property values
  4942. * @param {Object} [defaultOptions] default options
  4943. * // TODO: describe available options
  4944. */
  4945. function ItemPoint (parent, data, options, defaultOptions) {
  4946. this.props = {
  4947. dot: {
  4948. top: 0,
  4949. width: 0,
  4950. height: 0
  4951. },
  4952. content: {
  4953. height: 0,
  4954. marginLeft: 0
  4955. }
  4956. };
  4957. Item.call(this, parent, data, options, defaultOptions);
  4958. }
  4959. ItemPoint.prototype = new Item (null, null);
  4960. /**
  4961. * Select the item
  4962. * @override
  4963. */
  4964. ItemPoint.prototype.select = function select() {
  4965. this.selected = true;
  4966. // TODO: select and unselect
  4967. };
  4968. /**
  4969. * Unselect the item
  4970. * @override
  4971. */
  4972. ItemPoint.prototype.unselect = function unselect() {
  4973. this.selected = false;
  4974. // TODO: select and unselect
  4975. };
  4976. /**
  4977. * Repaint the item
  4978. * @return {Boolean} changed
  4979. */
  4980. ItemPoint.prototype.repaint = function repaint() {
  4981. // TODO: make an efficient repaint
  4982. var changed = false;
  4983. var dom = this.dom;
  4984. if (!dom) {
  4985. this._create();
  4986. dom = this.dom;
  4987. changed = true;
  4988. }
  4989. if (dom) {
  4990. if (!this.parent) {
  4991. throw new Error('Cannot repaint item: no parent attached');
  4992. }
  4993. var foreground = this.parent.getForeground();
  4994. if (!foreground) {
  4995. throw new Error('Cannot repaint time axis: ' +
  4996. 'parent has no foreground container element');
  4997. }
  4998. if (!dom.point.parentNode) {
  4999. foreground.appendChild(dom.point);
  5000. foreground.appendChild(dom.point);
  5001. changed = true;
  5002. }
  5003. // update contents
  5004. if (this.data.content != this.content) {
  5005. this.content = this.data.content;
  5006. if (this.content instanceof Element) {
  5007. dom.content.innerHTML = '';
  5008. dom.content.appendChild(this.content);
  5009. }
  5010. else if (this.data.content != undefined) {
  5011. dom.content.innerHTML = this.content;
  5012. }
  5013. else {
  5014. throw new Error('Property "content" missing in item ' + this.data.id);
  5015. }
  5016. changed = true;
  5017. }
  5018. // update class
  5019. var className = (this.data.className? ' ' + this.data.className : '') +
  5020. (this.selected ? ' selected' : '');
  5021. if (this.className != className) {
  5022. this.className = className;
  5023. dom.point.className = 'item point' + className;
  5024. changed = true;
  5025. }
  5026. }
  5027. return changed;
  5028. };
  5029. /**
  5030. * Show the item in the DOM (when not already visible). The items DOM will
  5031. * be created when needed.
  5032. * @return {Boolean} changed
  5033. */
  5034. ItemPoint.prototype.show = function show() {
  5035. if (!this.dom || !this.dom.point.parentNode) {
  5036. return this.repaint();
  5037. }
  5038. else {
  5039. return false;
  5040. }
  5041. };
  5042. /**
  5043. * Hide the item from the DOM (when visible)
  5044. * @return {Boolean} changed
  5045. */
  5046. ItemPoint.prototype.hide = function hide() {
  5047. var changed = false,
  5048. dom = this.dom;
  5049. if (dom) {
  5050. if (dom.point.parentNode) {
  5051. dom.point.parentNode.removeChild(dom.point);
  5052. changed = true;
  5053. }
  5054. }
  5055. return changed;
  5056. };
  5057. /**
  5058. * Reflow the item: calculate its actual size from the DOM
  5059. * @return {boolean} resized returns true if the axis is resized
  5060. * @override
  5061. */
  5062. ItemPoint.prototype.reflow = function reflow() {
  5063. var changed = 0,
  5064. update,
  5065. dom,
  5066. props,
  5067. options,
  5068. margin,
  5069. orientation,
  5070. start,
  5071. top,
  5072. data,
  5073. range;
  5074. if (this.data.start == undefined) {
  5075. throw new Error('Property "start" missing in item ' + this.data.id);
  5076. }
  5077. data = this.data;
  5078. range = this.parent && this.parent.range;
  5079. if (data && range) {
  5080. // TODO: account for the width of the item. Take some margin
  5081. this.visible = (data.start > range.start) && (data.start < range.end);
  5082. }
  5083. else {
  5084. this.visible = false;
  5085. }
  5086. if (this.visible) {
  5087. dom = this.dom;
  5088. if (dom) {
  5089. update = util.updateProperty;
  5090. props = this.props;
  5091. options = this.options;
  5092. orientation = options.orientation || this.defaultOptions.orientation;
  5093. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5094. start = this.parent.toScreen(this.data.start);
  5095. changed += update(this, 'width', dom.point.offsetWidth);
  5096. changed += update(this, 'height', dom.point.offsetHeight);
  5097. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5098. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5099. changed += update(props.content, 'height', dom.content.offsetHeight);
  5100. if (orientation == 'top') {
  5101. top = margin;
  5102. }
  5103. else {
  5104. // default or 'bottom'
  5105. var parentHeight = this.parent.height;
  5106. top = Math.max(parentHeight - this.height - margin, 0);
  5107. }
  5108. changed += update(this, 'top', top);
  5109. changed += update(this, 'left', start - props.dot.width / 2);
  5110. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5111. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5112. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5113. }
  5114. else {
  5115. changed += 1;
  5116. }
  5117. }
  5118. return (changed > 0);
  5119. };
  5120. /**
  5121. * Create an items DOM
  5122. * @private
  5123. */
  5124. ItemPoint.prototype._create = function _create() {
  5125. var dom = this.dom;
  5126. if (!dom) {
  5127. this.dom = dom = {};
  5128. // background box
  5129. dom.point = document.createElement('div');
  5130. // className is updated in repaint()
  5131. // contents box, right from the dot
  5132. dom.content = document.createElement('div');
  5133. dom.content.className = 'content';
  5134. dom.point.appendChild(dom.content);
  5135. // dot at start
  5136. dom.dot = document.createElement('div');
  5137. dom.dot.className = 'dot';
  5138. dom.point.appendChild(dom.dot);
  5139. }
  5140. };
  5141. /**
  5142. * Reposition the item, recalculate its left, top, and width, using the current
  5143. * range and size of the items itemset
  5144. * @override
  5145. */
  5146. ItemPoint.prototype.reposition = function reposition() {
  5147. var dom = this.dom,
  5148. props = this.props;
  5149. if (dom) {
  5150. dom.point.style.top = this.top + 'px';
  5151. dom.point.style.left = this.left + 'px';
  5152. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5153. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5154. dom.dot.style.top = props.dot.top + 'px';
  5155. }
  5156. };
  5157. /**
  5158. * @constructor ItemRange
  5159. * @extends Item
  5160. * @param {ItemSet} parent
  5161. * @param {Object} data Object containing parameters start, end
  5162. * content, className.
  5163. * @param {Object} [options] Options to set initial property values
  5164. * @param {Object} [defaultOptions] default options
  5165. * // TODO: describe available options
  5166. */
  5167. function ItemRange (parent, data, options, defaultOptions) {
  5168. this.props = {
  5169. content: {
  5170. left: 0,
  5171. width: 0
  5172. }
  5173. };
  5174. Item.call(this, parent, data, options, defaultOptions);
  5175. }
  5176. ItemRange.prototype = new Item (null, null);
  5177. /**
  5178. * Select the item
  5179. * @override
  5180. */
  5181. ItemRange.prototype.select = function select() {
  5182. this.selected = true;
  5183. // TODO: select and unselect
  5184. };
  5185. /**
  5186. * Unselect the item
  5187. * @override
  5188. */
  5189. ItemRange.prototype.unselect = function unselect() {
  5190. this.selected = false;
  5191. // TODO: select and unselect
  5192. };
  5193. /**
  5194. * Repaint the item
  5195. * @return {Boolean} changed
  5196. */
  5197. ItemRange.prototype.repaint = function repaint() {
  5198. // TODO: make an efficient repaint
  5199. var changed = false;
  5200. var dom = this.dom;
  5201. if (!dom) {
  5202. this._create();
  5203. dom = this.dom;
  5204. changed = true;
  5205. }
  5206. if (dom) {
  5207. if (!this.parent) {
  5208. throw new Error('Cannot repaint item: no parent attached');
  5209. }
  5210. var foreground = this.parent.getForeground();
  5211. if (!foreground) {
  5212. throw new Error('Cannot repaint time axis: ' +
  5213. 'parent has no foreground container element');
  5214. }
  5215. if (!dom.box.parentNode) {
  5216. foreground.appendChild(dom.box);
  5217. changed = true;
  5218. }
  5219. // update content
  5220. if (this.data.content != this.content) {
  5221. this.content = this.data.content;
  5222. if (this.content instanceof Element) {
  5223. dom.content.innerHTML = '';
  5224. dom.content.appendChild(this.content);
  5225. }
  5226. else if (this.data.content != undefined) {
  5227. dom.content.innerHTML = this.content;
  5228. }
  5229. else {
  5230. throw new Error('Property "content" missing in item ' + this.data.id);
  5231. }
  5232. changed = true;
  5233. }
  5234. // update class
  5235. var className = this.data.className ? ('' + this.data.className) : '';
  5236. if (this.className != className) {
  5237. this.className = className;
  5238. dom.box.className = 'item range' + className;
  5239. changed = true;
  5240. }
  5241. }
  5242. return changed;
  5243. };
  5244. /**
  5245. * Show the item in the DOM (when not already visible). The items DOM will
  5246. * be created when needed.
  5247. * @return {Boolean} changed
  5248. */
  5249. ItemRange.prototype.show = function show() {
  5250. if (!this.dom || !this.dom.box.parentNode) {
  5251. return this.repaint();
  5252. }
  5253. else {
  5254. return false;
  5255. }
  5256. };
  5257. /**
  5258. * Hide the item from the DOM (when visible)
  5259. * @return {Boolean} changed
  5260. */
  5261. ItemRange.prototype.hide = function hide() {
  5262. var changed = false,
  5263. dom = this.dom;
  5264. if (dom) {
  5265. if (dom.box.parentNode) {
  5266. dom.box.parentNode.removeChild(dom.box);
  5267. changed = true;
  5268. }
  5269. }
  5270. return changed;
  5271. };
  5272. /**
  5273. * Reflow the item: calculate its actual size from the DOM
  5274. * @return {boolean} resized returns true if the axis is resized
  5275. * @override
  5276. */
  5277. ItemRange.prototype.reflow = function reflow() {
  5278. var changed = 0,
  5279. dom,
  5280. props,
  5281. options,
  5282. margin,
  5283. padding,
  5284. parent,
  5285. start,
  5286. end,
  5287. data,
  5288. range,
  5289. update,
  5290. box,
  5291. parentWidth,
  5292. contentLeft,
  5293. orientation,
  5294. top;
  5295. if (this.data.start == undefined) {
  5296. throw new Error('Property "start" missing in item ' + this.data.id);
  5297. }
  5298. if (this.data.end == undefined) {
  5299. throw new Error('Property "end" missing in item ' + this.data.id);
  5300. }
  5301. data = this.data;
  5302. range = this.parent && this.parent.range;
  5303. if (data && range) {
  5304. // TODO: account for the width of the item. Take some margin
  5305. this.visible = (data.start < range.end) && (data.end > range.start);
  5306. }
  5307. else {
  5308. this.visible = false;
  5309. }
  5310. if (this.visible) {
  5311. dom = this.dom;
  5312. if (dom) {
  5313. props = this.props;
  5314. options = this.options;
  5315. parent = this.parent;
  5316. start = parent.toScreen(this.data.start);
  5317. end = parent.toScreen(this.data.end);
  5318. update = util.updateProperty;
  5319. box = dom.box;
  5320. parentWidth = parent.width;
  5321. orientation = options.orientation || this.defaultOptions.orientation;
  5322. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5323. padding = options.padding || this.defaultOptions.padding;
  5324. changed += update(props.content, 'width', dom.content.offsetWidth);
  5325. changed += update(this, 'height', box.offsetHeight);
  5326. // limit the width of the this, as browsers cannot draw very wide divs
  5327. if (start < -parentWidth) {
  5328. start = -parentWidth;
  5329. }
  5330. if (end > 2 * parentWidth) {
  5331. end = 2 * parentWidth;
  5332. }
  5333. // when range exceeds left of the window, position the contents at the left of the visible area
  5334. if (start < 0) {
  5335. contentLeft = Math.min(-start,
  5336. (end - start - props.content.width - 2 * padding));
  5337. // TODO: remove the need for options.padding. it's terrible.
  5338. }
  5339. else {
  5340. contentLeft = 0;
  5341. }
  5342. changed += update(props.content, 'left', contentLeft);
  5343. if (orientation == 'top') {
  5344. top = margin;
  5345. changed += update(this, 'top', top);
  5346. }
  5347. else {
  5348. // default or 'bottom'
  5349. top = parent.height - this.height - margin;
  5350. changed += update(this, 'top', top);
  5351. }
  5352. changed += update(this, 'left', start);
  5353. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5354. }
  5355. else {
  5356. changed += 1;
  5357. }
  5358. }
  5359. return (changed > 0);
  5360. };
  5361. /**
  5362. * Create an items DOM
  5363. * @private
  5364. */
  5365. ItemRange.prototype._create = function _create() {
  5366. var dom = this.dom;
  5367. if (!dom) {
  5368. this.dom = dom = {};
  5369. // background box
  5370. dom.box = document.createElement('div');
  5371. // className is updated in repaint()
  5372. // contents box
  5373. dom.content = document.createElement('div');
  5374. dom.content.className = 'content';
  5375. dom.box.appendChild(dom.content);
  5376. }
  5377. };
  5378. /**
  5379. * Reposition the item, recalculate its left, top, and width, using the current
  5380. * range and size of the items itemset
  5381. * @override
  5382. */
  5383. ItemRange.prototype.reposition = function reposition() {
  5384. var dom = this.dom,
  5385. props = this.props;
  5386. if (dom) {
  5387. dom.box.style.top = this.top + 'px';
  5388. dom.box.style.left = this.left + 'px';
  5389. dom.box.style.width = this.width + 'px';
  5390. dom.content.style.left = props.content.left + 'px';
  5391. }
  5392. };
  5393. /**
  5394. * @constructor Group
  5395. * @param {GroupSet} parent
  5396. * @param {Number | String} groupId
  5397. * @param {Object} [options] Options to set initial property values
  5398. * // TODO: describe available options
  5399. * @extends Component
  5400. */
  5401. function Group (parent, groupId, options) {
  5402. this.id = util.randomUUID();
  5403. this.parent = parent;
  5404. this.groupId = groupId;
  5405. this.itemsData = null; // DataSet
  5406. this.itemset = null; // ItemSet
  5407. this.options = options || {};
  5408. this.options.top = 0;
  5409. this.top = 0;
  5410. this.left = 0;
  5411. this.width = 0;
  5412. this.height = 0;
  5413. }
  5414. Group.prototype = new Component();
  5415. // TODO: comment
  5416. Group.prototype.setOptions = Component.prototype.setOptions;
  5417. /**
  5418. * Get the container element of the panel, which can be used by a child to
  5419. * add its own widgets.
  5420. * @returns {HTMLElement} container
  5421. */
  5422. Group.prototype.getContainer = function () {
  5423. return this.parent.getContainer();
  5424. };
  5425. /**
  5426. * Set item set for the group. The group will create a view on the itemset,
  5427. * filtered by the groups id.
  5428. * @param {DataSet | DataView} items
  5429. */
  5430. Group.prototype.setItems = function setItems(items) {
  5431. if (this.itemset) {
  5432. // remove current item set
  5433. this.itemset.hide();
  5434. this.itemset.setItems();
  5435. this.parent.controller.remove(this.itemset);
  5436. this.itemset = null;
  5437. }
  5438. if (items) {
  5439. var groupId = this.groupId;
  5440. var itemsetOptions = Object.create(this.options);
  5441. this.itemset = new ItemSet(this, null, itemsetOptions);
  5442. this.itemset.setRange(this.parent.range);
  5443. this.view = new DataView(items, {
  5444. filter: function (item) {
  5445. return item.group == groupId;
  5446. }
  5447. });
  5448. this.itemset.setItems(this.view);
  5449. this.parent.controller.add(this.itemset);
  5450. }
  5451. };
  5452. /**
  5453. * Repaint the item
  5454. * @return {Boolean} changed
  5455. */
  5456. Group.prototype.repaint = function repaint() {
  5457. return false;
  5458. };
  5459. /**
  5460. * Reflow the item
  5461. * @return {Boolean} resized
  5462. */
  5463. Group.prototype.reflow = function reflow() {
  5464. var changed = 0,
  5465. update = util.updateProperty;
  5466. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5467. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5468. return (changed > 0);
  5469. };
  5470. /**
  5471. * An GroupSet holds a set of groups
  5472. * @param {Component} parent
  5473. * @param {Component[]} [depends] Components on which this components depends
  5474. * (except for the parent)
  5475. * @param {Object} [options] See GroupSet.setOptions for the available
  5476. * options.
  5477. * @constructor GroupSet
  5478. * @extends Panel
  5479. */
  5480. function GroupSet(parent, depends, options) {
  5481. this.id = util.randomUUID();
  5482. this.parent = parent;
  5483. this.depends = depends;
  5484. this.options = options || {};
  5485. this.range = null; // Range or Object {start: number, end: number}
  5486. this.itemsData = null; // DataSet with items
  5487. this.groupsData = null; // DataSet with groups
  5488. this.groups = {}; // map with groups
  5489. // changes in groups are queued key/value map containing id/action
  5490. this.queue = {};
  5491. var me = this;
  5492. this.listeners = {
  5493. 'add': function (event, params) {
  5494. me._onAdd(params.items);
  5495. },
  5496. 'update': function (event, params) {
  5497. me._onUpdate(params.items);
  5498. },
  5499. 'remove': function (event, params) {
  5500. me._onRemove(params.items);
  5501. }
  5502. };
  5503. }
  5504. GroupSet.prototype = new Panel();
  5505. /**
  5506. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5507. * @param {Object} [options] The following options are available:
  5508. * {String | function} groupsOrder
  5509. * TODO: describe options
  5510. */
  5511. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5512. GroupSet.prototype.setRange = function (range) {
  5513. // TODO: implement setRange
  5514. };
  5515. /**
  5516. * Set items
  5517. * @param {vis.DataSet | null} items
  5518. */
  5519. GroupSet.prototype.setItems = function setItems(items) {
  5520. this.itemsData = items;
  5521. for (var id in this.groups) {
  5522. if (this.groups.hasOwnProperty(id)) {
  5523. var group = this.groups[id];
  5524. group.setItems(items);
  5525. }
  5526. }
  5527. };
  5528. /**
  5529. * Get items
  5530. * @return {vis.DataSet | null} items
  5531. */
  5532. GroupSet.prototype.getItems = function getItems() {
  5533. return this.itemsData;
  5534. };
  5535. /**
  5536. * Set range (start and end).
  5537. * @param {Range | Object} range A Range or an object containing start and end.
  5538. */
  5539. GroupSet.prototype.setRange = function setRange(range) {
  5540. this.range = range;
  5541. };
  5542. /**
  5543. * Set groups
  5544. * @param {vis.DataSet} groups
  5545. */
  5546. GroupSet.prototype.setGroups = function setGroups(groups) {
  5547. var me = this,
  5548. ids;
  5549. // unsubscribe from current dataset
  5550. if (this.groupsData) {
  5551. util.forEach(this.listeners, function (callback, event) {
  5552. me.groupsData.unsubscribe(event, callback);
  5553. });
  5554. // remove all drawn groups
  5555. ids = this.groupsData.getIds();
  5556. this._onRemove(ids);
  5557. }
  5558. // replace the dataset
  5559. if (!groups) {
  5560. this.groupsData = null;
  5561. }
  5562. else if (groups instanceof DataSet) {
  5563. this.groupsData = groups;
  5564. }
  5565. else {
  5566. this.groupsData = new DataSet({
  5567. fieldTypes: {
  5568. start: 'Date',
  5569. end: 'Date'
  5570. }
  5571. });
  5572. this.groupsData.add(groups);
  5573. }
  5574. if (this.groupsData) {
  5575. // subscribe to new dataset
  5576. var id = this.id;
  5577. util.forEach(this.listeners, function (callback, event) {
  5578. me.groupsData.subscribe(event, callback, id);
  5579. });
  5580. // draw all new groups
  5581. ids = this.groupsData.getIds();
  5582. this._onAdd(ids);
  5583. }
  5584. };
  5585. /**
  5586. * Get groups
  5587. * @return {vis.DataSet | null} groups
  5588. */
  5589. GroupSet.prototype.getGroups = function getGroups() {
  5590. return this.groupsData;
  5591. };
  5592. /**
  5593. * Repaint the component
  5594. * @return {Boolean} changed
  5595. */
  5596. GroupSet.prototype.repaint = function repaint() {
  5597. var changed = 0,
  5598. update = util.updateProperty,
  5599. asSize = util.option.asSize,
  5600. options = this.options,
  5601. frame = this.frame;
  5602. if (!frame) {
  5603. frame = document.createElement('div');
  5604. frame.className = 'groupset';
  5605. var className = options.className;
  5606. if (className) {
  5607. util.addClassName(frame, util.option.asString(className));
  5608. }
  5609. this.frame = frame;
  5610. changed += 1;
  5611. }
  5612. if (!this.parent) {
  5613. throw new Error('Cannot repaint groupset: no parent attached');
  5614. }
  5615. var parentContainer = this.parent.getContainer();
  5616. if (!parentContainer) {
  5617. throw new Error('Cannot repaint groupset: parent has no container element');
  5618. }
  5619. if (!frame.parentNode) {
  5620. parentContainer.appendChild(frame);
  5621. changed += 1;
  5622. }
  5623. // reposition frame
  5624. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  5625. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  5626. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  5627. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  5628. var me = this,
  5629. queue = this.queue,
  5630. groups = this.groups,
  5631. groupsData = this.groupsData;
  5632. // show/hide added/changed/removed items
  5633. var ids = Object.keys(queue);
  5634. if (ids.length) {
  5635. ids.forEach(function (id) {
  5636. var action = queue[id];
  5637. var group = groups[id];
  5638. //noinspection FallthroughInSwitchStatementJS
  5639. switch (action) {
  5640. case 'add':
  5641. case 'update':
  5642. if (!group) {
  5643. var groupOptions = Object.create(me.options);
  5644. group = new Group(me, id, groupOptions);
  5645. group.setItems(me.itemsData); // attach items data
  5646. groups[id] = group;
  5647. me.controller.add(group);
  5648. }
  5649. // TODO: update group data
  5650. group.data = groupsData.get(id);
  5651. delete queue[id];
  5652. break;
  5653. case 'remove':
  5654. if (group) {
  5655. group.setItems(); // detach items data
  5656. delete groups[id];
  5657. me.controller.remove(group);
  5658. }
  5659. // update lists
  5660. delete queue[id];
  5661. break;
  5662. default:
  5663. console.log('Error: unknown action "' + action + '"');
  5664. }
  5665. });
  5666. // the groupset depends on each of the groups
  5667. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  5668. // TODO: apply dependencies of the groupset
  5669. // update the top positions of the groups in the correct order
  5670. var orderedGroups = this.groupsData.getIds({
  5671. order: this.options.groupsOrder
  5672. });
  5673. for (var i = 0; i < orderedGroups.length; i++) {
  5674. (function (group, prevGroup) {
  5675. var top = 0;
  5676. if (prevGroup) {
  5677. top = function () {
  5678. // TODO: top must reckon with options.maxHeight
  5679. return prevGroup.top + prevGroup.height;
  5680. }
  5681. }
  5682. group.setOptions({
  5683. top: top
  5684. });
  5685. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  5686. }
  5687. changed++;
  5688. }
  5689. return (changed > 0);
  5690. };
  5691. /**
  5692. * Get container element
  5693. * @return {HTMLElement} container
  5694. */
  5695. GroupSet.prototype.getContainer = function getContainer() {
  5696. // TODO: replace later on with container element for holding itemsets
  5697. return this.frame;
  5698. };
  5699. /**
  5700. * Reflow the component
  5701. * @return {Boolean} resized
  5702. */
  5703. GroupSet.prototype.reflow = function reflow() {
  5704. var changed = 0,
  5705. options = this.options,
  5706. update = util.updateProperty,
  5707. asNumber = util.option.asNumber,
  5708. asSize = util.option.asSize,
  5709. frame = this.frame;
  5710. if (frame) {
  5711. var maxHeight = asNumber(options.maxHeight);
  5712. var fixedHeight = (asSize(options.height) != null);
  5713. var height;
  5714. if (fixedHeight) {
  5715. height = frame.offsetHeight;
  5716. }
  5717. else {
  5718. // height is not specified, calculate the sum of the height of all groups
  5719. height = 0;
  5720. for (var id in this.groups) {
  5721. if (this.groups.hasOwnProperty(id)) {
  5722. var group = this.groups[id];
  5723. height += group.height;
  5724. }
  5725. }
  5726. }
  5727. if (maxHeight != null) {
  5728. height = Math.min(height, maxHeight);
  5729. }
  5730. changed += update(this, 'height', height);
  5731. changed += update(this, 'top', frame.offsetTop);
  5732. changed += update(this, 'left', frame.offsetLeft);
  5733. changed += update(this, 'width', frame.offsetWidth);
  5734. }
  5735. return (changed > 0);
  5736. };
  5737. /**
  5738. * Hide the component from the DOM
  5739. * @return {Boolean} changed
  5740. */
  5741. GroupSet.prototype.hide = function hide() {
  5742. if (this.frame && this.frame.parentNode) {
  5743. this.frame.parentNode.removeChild(this.frame);
  5744. return true;
  5745. }
  5746. else {
  5747. return false;
  5748. }
  5749. };
  5750. /**
  5751. * Show the component in the DOM (when not already visible).
  5752. * A repaint will be executed when the component is not visible
  5753. * @return {Boolean} changed
  5754. */
  5755. GroupSet.prototype.show = function show() {
  5756. if (!this.frame || !this.frame.parentNode) {
  5757. return this.repaint();
  5758. }
  5759. else {
  5760. return false;
  5761. }
  5762. };
  5763. /**
  5764. * Handle updated groups
  5765. * @param {Number[]} ids
  5766. * @private
  5767. */
  5768. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  5769. this._toQueue(ids, 'update');
  5770. };
  5771. /**
  5772. * Handle changed groups
  5773. * @param {Number[]} ids
  5774. * @private
  5775. */
  5776. GroupSet.prototype._onAdd = function _onAdd(ids) {
  5777. this._toQueue(ids, 'add');
  5778. };
  5779. /**
  5780. * Handle removed groups
  5781. * @param {Number[]} ids
  5782. * @private
  5783. */
  5784. GroupSet.prototype._onRemove = function _onRemove(ids) {
  5785. this._toQueue(ids, 'remove');
  5786. };
  5787. /**
  5788. * Put groups in the queue to be added/updated/remove
  5789. * @param {Number[]} ids
  5790. * @param {String} action can be 'add', 'update', 'remove'
  5791. */
  5792. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  5793. var queue = this.queue;
  5794. ids.forEach(function (id) {
  5795. queue[id] = action;
  5796. });
  5797. if (this.controller) {
  5798. //this.requestReflow();
  5799. this.requestRepaint();
  5800. }
  5801. };
  5802. /**
  5803. * Create a timeline visualization
  5804. * @param {HTMLElement} container
  5805. * @param {vis.DataSet | Array | DataTable} [items]
  5806. * @param {Object} [options] See Timeline.setOptions for the available options.
  5807. * @constructor
  5808. */
  5809. function Timeline (container, items, options) {
  5810. var me = this;
  5811. this.options = util.extend({
  5812. orientation: 'bottom',
  5813. min: null,
  5814. max: null,
  5815. zoomMin: 10, // milliseconds
  5816. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  5817. moveable: true,
  5818. zoomable: true,
  5819. showMinorLabels: true,
  5820. showMajorLabels: true,
  5821. autoResize: false
  5822. }, options);
  5823. // controller
  5824. this.controller = new Controller();
  5825. // root panel
  5826. if (!container) {
  5827. throw new Error('No container element provided');
  5828. }
  5829. var mainOptions = Object.create(this.options);
  5830. mainOptions.height = function () {
  5831. if (me.options.height) {
  5832. // fixed height
  5833. return me.options.height;
  5834. }
  5835. else {
  5836. // auto height
  5837. return me.timeaxis.height + me.content.height;
  5838. }
  5839. };
  5840. this.root = new RootPanel(container, mainOptions);
  5841. this.controller.add(this.root);
  5842. // range
  5843. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  5844. this.range = new Range({
  5845. start: now.clone().add('days', -3).valueOf(),
  5846. end: now.clone().add('days', 4).valueOf()
  5847. });
  5848. // TODO: reckon with options moveable and zoomable
  5849. this.range.subscribe(this.root, 'move', 'horizontal');
  5850. this.range.subscribe(this.root, 'zoom', 'horizontal');
  5851. this.range.on('rangechange', function () {
  5852. var force = true;
  5853. me.controller.requestReflow(force);
  5854. });
  5855. this.range.on('rangechanged', function () {
  5856. var force = true;
  5857. me.controller.requestReflow(force);
  5858. });
  5859. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  5860. // time axis
  5861. var timeaxisOptions = Object.create(mainOptions);
  5862. timeaxisOptions.range = this.range;
  5863. this.timeaxis = new TimeAxis(this.root, [], timeaxisOptions);
  5864. this.timeaxis.setRange(this.range);
  5865. this.controller.add(this.timeaxis);
  5866. // create itemset or groupset
  5867. this.setGroups(null);
  5868. this.itemsData = null; // DataSet
  5869. this.groupsData = null; // DataSet
  5870. // set data
  5871. if (items) {
  5872. this.setItems(items);
  5873. }
  5874. }
  5875. /**
  5876. * Set options
  5877. * @param {Object} options TODO: describe the available options
  5878. */
  5879. Timeline.prototype.setOptions = function (options) {
  5880. if (options) {
  5881. util.extend(this.options, options);
  5882. }
  5883. this.controller.reflow();
  5884. this.controller.repaint();
  5885. };
  5886. /**
  5887. * Set items
  5888. * @param {vis.DataSet | Array | DataTable | null} items
  5889. */
  5890. Timeline.prototype.setItems = function(items) {
  5891. var initialLoad = (this.itemsData == null);
  5892. // convert to type DataSet when needed
  5893. var newItemSet;
  5894. if (!items) {
  5895. newItemSet = null;
  5896. }
  5897. else if (items instanceof DataSet) {
  5898. newItemSet = items;
  5899. }
  5900. if (!(items instanceof DataSet)) {
  5901. newItemSet = new DataSet({
  5902. fieldTypes: {
  5903. start: 'Date',
  5904. end: 'Date'
  5905. }
  5906. });
  5907. newItemSet.add(items);
  5908. }
  5909. // set items
  5910. this.itemsData = newItemSet;
  5911. this.content.setItems(newItemSet);
  5912. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  5913. // apply the data range as range
  5914. var dataRange = this.getItemRange();
  5915. // add 5% on both sides
  5916. var min = dataRange.min;
  5917. var max = dataRange.max;
  5918. if (min != null && max != null) {
  5919. var interval = (max.valueOf() - min.valueOf());
  5920. min = new Date(min.valueOf() - interval * 0.05);
  5921. max = new Date(max.valueOf() + interval * 0.05);
  5922. }
  5923. // override specified start and/or end date
  5924. if (this.options.start != undefined) {
  5925. min = new Date(this.options.start.valueOf());
  5926. }
  5927. if (this.options.end != undefined) {
  5928. max = new Date(this.options.end.valueOf());
  5929. }
  5930. // apply range if there is a min or max available
  5931. if (min != null || max != null) {
  5932. this.range.setRange(min, max);
  5933. }
  5934. }
  5935. };
  5936. /**
  5937. * Set groups
  5938. * @param {vis.DataSet | Array | DataTable} groups
  5939. */
  5940. Timeline.prototype.setGroups = function(groups) {
  5941. var me = this;
  5942. this.groupsData = groups;
  5943. // switch content type between ItemSet or GroupSet when needed
  5944. var type = this.groupsData ? GroupSet : ItemSet;
  5945. if (!(this.content instanceof type)) {
  5946. // remove old content set
  5947. if (this.content) {
  5948. this.content.hide();
  5949. if (this.content.setItems) {
  5950. this.content.setItems(); // disconnect from items
  5951. }
  5952. if (this.content.setGroups) {
  5953. this.content.setGroups(); // disconnect from groups
  5954. }
  5955. this.controller.remove(this.content);
  5956. }
  5957. // create new content set
  5958. var options = Object.create(this.options);
  5959. util.extend(options, {
  5960. top: function () {
  5961. if (me.options.orientation == 'top') {
  5962. return me.timeaxis.height;
  5963. }
  5964. else {
  5965. return me.root.height - me.timeaxis.height - me.content.height;
  5966. }
  5967. },
  5968. height: function () {
  5969. if (me.options.height) {
  5970. return me.root.height - me.timeaxis.height;
  5971. }
  5972. else {
  5973. return null;
  5974. }
  5975. },
  5976. maxHeight: function () {
  5977. if (me.options.maxHeight) {
  5978. if (!util.isNumber(me.options.maxHeight)) {
  5979. throw new TypeError('Number expected for property maxHeight');
  5980. }
  5981. return me.options.maxHeight - me.timeaxis.height;
  5982. }
  5983. else {
  5984. return null;
  5985. }
  5986. }
  5987. });
  5988. this.content = new type(this.root, [this.timeaxis], options);
  5989. if (this.content.setRange) {
  5990. this.content.setRange(this.range);
  5991. }
  5992. if (this.content.setItems) {
  5993. this.content.setItems(this.itemsData);
  5994. }
  5995. if (this.content.setGroups) {
  5996. this.content.setGroups(this.groupsData);
  5997. }
  5998. this.controller.add(this.content);
  5999. }
  6000. };
  6001. /**
  6002. * Get the data range of the item set.
  6003. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6004. * When no minimum is found, min==null
  6005. * When no maximum is found, max==null
  6006. */
  6007. Timeline.prototype.getItemRange = function getItemRange() {
  6008. // calculate min from start filed
  6009. var itemsData = this.itemsData,
  6010. min = null,
  6011. max = null;
  6012. if (itemsData) {
  6013. // calculate the minimum value of the field 'start'
  6014. var minItem = itemsData.min('start');
  6015. min = minItem ? minItem.start.valueOf() : null;
  6016. // calculate maximum value of fields 'start' and 'end'
  6017. var maxStartItem = itemsData.max('start');
  6018. if (maxStartItem) {
  6019. max = maxStartItem.start.valueOf();
  6020. }
  6021. var maxEndItem = itemsData.max('end');
  6022. if (maxEndItem) {
  6023. if (max == null) {
  6024. max = maxEndItem.end.valueOf();
  6025. }
  6026. else {
  6027. max = Math.max(max, maxEndItem.end.valueOf());
  6028. }
  6029. }
  6030. }
  6031. return {
  6032. min: (min != null) ? new Date(min) : null,
  6033. max: (max != null) ? new Date(max) : null
  6034. };
  6035. };
  6036. /**
  6037. * @constructor Graph
  6038. * Create a graph visualization connecting nodes via edges.
  6039. * @param {Element} container The DOM element in which the Graph will
  6040. * be created. Normally a div element.
  6041. */
  6042. function Graph (container) {
  6043. // create variables and set default values
  6044. this.containerElement = container;
  6045. this.width = "100%";
  6046. this.height = "100%";
  6047. this.refreshRate = 50; // milliseconds
  6048. this.stabilize = true; // stabilize before displaying the network
  6049. this.selectable = true;
  6050. // set constant values
  6051. this.constants = {
  6052. "nodes": {
  6053. "radiusMin": 5,
  6054. "radiusMax": 20,
  6055. "radius": 5,
  6056. "distance": 100, // px
  6057. "style": "rect",
  6058. "image": undefined,
  6059. "widthMin": 16, // px
  6060. "widthMax": 64, // px
  6061. "fontColor": "black",
  6062. "fontSize": 14, // px
  6063. //"fontFace": "verdana",
  6064. "fontFace": "arial",
  6065. "borderColor": "#2B7CE9",
  6066. "backgroundColor": "#97C2FC",
  6067. "highlightColor": "#D2E5FF",
  6068. "group": undefined
  6069. },
  6070. "edges": {
  6071. "widthMin": 1,
  6072. "widthMax": 15,
  6073. "width": 1,
  6074. "style": "line",
  6075. "color": "#343434",
  6076. "fontColor": "#343434",
  6077. "fontSize": 14, // px
  6078. "fontFace": "arial",
  6079. //"distance": 100, //px
  6080. "length": 100, // px
  6081. "dashlength": 10,
  6082. "dashgap": 5
  6083. },
  6084. "packages": {
  6085. "radius": 5,
  6086. "radiusMin": 5,
  6087. "radiusMax": 10,
  6088. "style": "dot",
  6089. "color": "#2B7CE9",
  6090. "image": undefined,
  6091. "widthMin": 16, // px
  6092. "widthMax": 64, // px
  6093. "duration": 1.0 // seconds
  6094. },
  6095. "minForce": 0.05,
  6096. "minVelocity": 0.02, // px/s
  6097. "maxIterations": 1000 // maximum number of iteration to stabilize
  6098. };
  6099. this.nodes = []; // array with Node objects
  6100. this.edges = []; // array with Edge objects
  6101. this.packages = []; // array with all Package packages
  6102. this.images = new Graph.Images(); // object with images
  6103. this.groups = new Graph.Groups(); // object with groups
  6104. // properties of the data
  6105. this.hasMovingEdges = false; // True if one or more of the edges or nodes have an animation
  6106. this.hasMovingNodes = false; // True if any of the nodes have an undefined position
  6107. this.hasMovingPackages = false; // True if there are one or more packages
  6108. this.selection = [];
  6109. this.timer = undefined;
  6110. // create a frame and canvas
  6111. this._create();
  6112. };
  6113. /**
  6114. * Main drawing logic. This is the function that needs to be called
  6115. * in the html page, to draw the Network.
  6116. * Note that Object DataTable is defined in google.visualization.DataTable
  6117. *
  6118. * A data table with the events must be provided, and an options table.
  6119. * @param {google.visualization.DataTable | Array} [nodes] The data containing the nodes.
  6120. * @param {google.visualization.DataTable | Array} [edges] The data containing the edges.
  6121. * @param {google.visualization.DataTable | Array} [packages] The data containing the packages
  6122. * @param {Object} options A name/value map containing settings
  6123. */
  6124. Graph.prototype.draw = function(nodes, edges, packages, options) {
  6125. var nodesTable, edgesTable, packagesTable;
  6126. // correctly read the parameters. edges and packages are optional.
  6127. if (options != undefined) {
  6128. nodesTable = nodes;
  6129. edgesTable = edges;
  6130. packagesTable = packages;
  6131. }
  6132. else if (packages != undefined) {
  6133. nodesTable = nodes;
  6134. edgesTable = edges;
  6135. packagesTable = undefined;
  6136. options = packages;
  6137. }
  6138. else if (edges != undefined) {
  6139. nodesTable = nodes;
  6140. edgesTable = undefined;
  6141. packagesTable = undefined;
  6142. options = edges;
  6143. }
  6144. else if (nodes != undefined) {
  6145. nodesTable = undefined;
  6146. edgesTable = undefined;
  6147. packagesTable = undefined;
  6148. options = nodes;
  6149. }
  6150. if (options != undefined) {
  6151. // retrieve parameter values
  6152. if (options.width != undefined) {this.width = options.width;}
  6153. if (options.height != undefined) {this.height = options.height;}
  6154. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  6155. if (options.selectable != undefined) {this.selectable = options.selectable;}
  6156. // TODO: work out these options and document them
  6157. if (options.edges) {
  6158. for (var prop in options.edges) {
  6159. if (options.edges.hasOwnProperty(prop)) {
  6160. this.constants.edges[prop] = options.edges[prop];
  6161. }
  6162. }
  6163. if (options.edges.length != undefined &&
  6164. options.nodes && options.nodes.distance == undefined) {
  6165. this.constants.edges.length = options.edges.length;
  6166. this.constants.nodes.distance = options.edges.length * 1.25;
  6167. }
  6168. if (!options.edges.fontColor) {
  6169. this.constants.edges.fontColor = options.edges.color;
  6170. }
  6171. // Added to support dashed lines
  6172. // David Jordan
  6173. // 2012-08-08
  6174. if (options.edges.dashlength != undefined) {
  6175. this.constants.edges.dashlength = options.edges.dashlength;
  6176. }
  6177. if (options.edges.dashgap != undefined) {
  6178. this.constants.edges.dashgap = options.edges.dashgap;
  6179. }
  6180. if (options.edges.altdashlength != undefined) {
  6181. this.constants.edges.altdashlength = options.edges.altdashlength;
  6182. }
  6183. }
  6184. if (options.nodes) {
  6185. for (prop in options.nodes) {
  6186. if (options.nodes.hasOwnProperty(prop)) {
  6187. this.constants.nodes[prop] = options.nodes[prop];
  6188. }
  6189. }
  6190. /*
  6191. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  6192. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  6193. */
  6194. }
  6195. if (options.packages) {
  6196. for (prop in options.packages) {
  6197. if (options.packages.hasOwnProperty(prop)) {
  6198. this.constants.packages[prop] = options.packages[prop];
  6199. }
  6200. }
  6201. /*
  6202. if (options.packages.widthMin) this.constants.packages.radiusMin = options.packages.widthMin;
  6203. if (options.packages.widthMax) this.constants.packages.radiusMax = options.packages.widthMax;
  6204. */
  6205. }
  6206. if (options.groups) {
  6207. for (var groupname in options.groups) {
  6208. if (options.groups.hasOwnProperty(groupname)) {
  6209. var group = options.groups[groupname];
  6210. this.groups.add(groupname, group);
  6211. }
  6212. }
  6213. }
  6214. }
  6215. this._setBackgroundColor(options.backgroundColor);
  6216. this._setSize(this.width, this.height);
  6217. this._setTranslation(0, 0);
  6218. this._setScale(1.0);
  6219. // set all data
  6220. this.hasTimestamps = false;
  6221. this.setNodes(nodesTable);
  6222. this.setEdges(edgesTable);
  6223. this.setPackages(packagesTable);
  6224. this._reposition(); // TODO: bad solution
  6225. if (this.stabilize) {
  6226. this._doStabilize();
  6227. }
  6228. this.start();
  6229. // create an onload callback method for the images
  6230. var network = this;
  6231. var callback = function () {
  6232. network._redraw();
  6233. };
  6234. this.images.setOnloadCallback(callback);
  6235. // fire the ready event
  6236. this.trigger('ready');
  6237. };
  6238. /**
  6239. * fire an event
  6240. * @param {String} event The name of an event, for example "select" or "ready"
  6241. * @param {Object} params Optional object with event parameters
  6242. */
  6243. Graph.prototype.trigger = function (event, params) {
  6244. // trigger the edges event bus
  6245. events.trigger(this, event, params);
  6246. // trigger the google event bus
  6247. if (typeof google !== 'undefined' && google.visualization && google.visualization.events) {
  6248. google.visualization.events.trigger(this, event, params);
  6249. }
  6250. };
  6251. /**
  6252. * Create the main frame for the Network.
  6253. * This function is executed once when a Network object is created. The frame
  6254. * contains a canvas, and this canvas contains all objects like the axis and
  6255. * nodes.
  6256. */
  6257. Graph.prototype._create = function () {
  6258. // remove all elements from the container element.
  6259. while (this.containerElement.hasChildNodes()) {
  6260. this.containerElement.removeChild(this.containerElement.firstChild);
  6261. }
  6262. this.frame = document.createElement("div");
  6263. this.frame.className = "network-frame";
  6264. this.frame.style.position = "relative";
  6265. this.frame.style.overflow = "hidden";
  6266. // create the graph canvas (HTML canvas element)
  6267. this.frame.canvas = document.createElement( "canvas" );
  6268. this.frame.canvas.style.position = "relative";
  6269. this.frame.appendChild(this.frame.canvas);
  6270. if (!this.frame.canvas.getContext) {
  6271. var noCanvas = document.createElement( "DIV" );
  6272. noCanvas.style.color = "red";
  6273. noCanvas.style.fontWeight = "bold" ;
  6274. noCanvas.style.padding = "10px";
  6275. noCanvas.innerHTML = "Error: your browser does not support HTML canvas";
  6276. this.frame.canvas.appendChild(noCanvas);
  6277. }
  6278. // create event listeners
  6279. var me = this;
  6280. var onmousedown = function (event) {me._onMouseDown(event);};
  6281. var onmousemove = function (event) {me._onMouseMoveTitle(event);};
  6282. var onmousewheel = function (event) {me._onMouseWheel(event);};
  6283. var ontouchstart = function (event) {me._onTouchStart(event);};
  6284. Graph.addEventListener(this.frame.canvas, "mousedown", onmousedown);
  6285. Graph.addEventListener(this.frame.canvas, "mousemove", onmousemove);
  6286. Graph.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
  6287. Graph.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
  6288. // add the frame to the container element
  6289. this.containerElement.appendChild(this.frame);
  6290. };
  6291. /**
  6292. * Set the background and border styling for the graph
  6293. * @param {String | Object} backgroundColor
  6294. */
  6295. Graph.prototype._setBackgroundColor = function(backgroundColor) {
  6296. var fill = "white";
  6297. var stroke = "lightgray";
  6298. var strokeWidth = 1;
  6299. if (typeof(backgroundColor) == "string") {
  6300. fill = backgroundColor;
  6301. stroke = "none";
  6302. strokeWidth = 0;
  6303. }
  6304. else if (typeof(backgroundColor) == "object") {
  6305. if (backgroundColor.fill != undefined) fill = backgroundColor.fill;
  6306. if (backgroundColor.stroke != undefined) stroke = backgroundColor.stroke;
  6307. if (backgroundColor.strokeWidth != undefined) strokeWidth = backgroundColor.strokeWidth;
  6308. }
  6309. else if (backgroundColor == undefined) {
  6310. // use use defaults
  6311. }
  6312. else {
  6313. throw "Unsupported type of backgroundColor";
  6314. }
  6315. this.frame.style.boxSizing = 'border-box';
  6316. this.frame.style.backgroundColor = fill;
  6317. this.frame.style.borderColor = stroke;
  6318. this.frame.style.borderWidth = strokeWidth + "px";
  6319. this.frame.style.borderStyle = "solid";
  6320. };
  6321. /**
  6322. * handle on mouse down event
  6323. */
  6324. Graph.prototype._onMouseDown = function (event) {
  6325. event = event || window.event;
  6326. if (!this.selectable) {
  6327. return;
  6328. }
  6329. // check if mouse is still down (may be up when focus is lost for example
  6330. // in an iframe)
  6331. if (this.leftButtonDown) {
  6332. this._onMouseUp(event);
  6333. }
  6334. // only react on left mouse button down
  6335. this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  6336. if (!this.leftButtonDown && !this.touchDown) {
  6337. return;
  6338. }
  6339. // add event listeners to handle moving the contents
  6340. // we store the function onmousemove and onmouseup in the timeline, so we can
  6341. // remove the eventlisteners lateron in the function mouseUp()
  6342. var me = this;
  6343. if (!this.onmousemove) {
  6344. this.onmousemove = function (event) {me._onMouseMove(event);};
  6345. Graph.addEventListener(document, "mousemove", me.onmousemove);
  6346. }
  6347. if (!this.onmouseup) {
  6348. this.onmouseup = function (event) {me._onMouseUp(event);};
  6349. Graph.addEventListener(document, "mouseup", me.onmouseup);
  6350. }
  6351. Graph.preventDefault(event);
  6352. // store the start x and y position of the mouse
  6353. this.startMouseX = event.clientX || event.targetTouches[0].clientX;
  6354. this.startMouseY = event.clientY || event.targetTouches[0].clientY;
  6355. this.startFrameLeft = Graph._getAbsoluteLeft(this.frame.canvas);
  6356. this.startFrameTop = Graph._getAbsoluteTop(this.frame.canvas);
  6357. this.startTranslation = this._getTranslation();
  6358. this.ctrlKeyDown = event.ctrlKey;
  6359. this.shiftKeyDown = event.shiftKey;
  6360. var obj = {
  6361. "left" : this._xToCanvas(this.startMouseX - this.startFrameLeft),
  6362. "top" : this._yToCanvas(this.startMouseY - this.startFrameTop),
  6363. "right" : this._xToCanvas(this.startMouseX - this.startFrameLeft),
  6364. "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop)
  6365. };
  6366. var overlappingNodes = this._getNodesOverlappingWith(obj);
  6367. // if there are overlapping nodes, select the last one, this is the
  6368. // one which is drawn on top of the others
  6369. this.startClickedObj = (overlappingNodes.length > 0) ?
  6370. overlappingNodes[overlappingNodes.length - 1] : undefined;
  6371. if (this.startClickedObj) {
  6372. // move clicked node with the mouse
  6373. // make the clicked node temporarily fixed, and store their original state
  6374. var node = this.nodes[this.startClickedObj.row];
  6375. this.startClickedObj.xFixed = node.xFixed;
  6376. this.startClickedObj.yFixed = node.yFixed;
  6377. node.xFixed = true;
  6378. node.yFixed = true;
  6379. if (!this.ctrlKeyDown || !node.isSelected()) {
  6380. // select this node
  6381. this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
  6382. }
  6383. else {
  6384. // unselect this node
  6385. this._unselectNodes([this.startClickedObj]);
  6386. }
  6387. if (!this.hasMovingNodes) {
  6388. this._redraw();
  6389. }
  6390. }
  6391. else if (this.shiftKeyDown) {
  6392. // start selection of multiple nodes
  6393. }
  6394. else {
  6395. // start moving the graph
  6396. this.moved = false;
  6397. }
  6398. };
  6399. /**
  6400. * handle on mouse move event
  6401. */
  6402. Graph.prototype._onMouseMove = function (event) {
  6403. event = event || window.event;
  6404. if (!this.selectable) {
  6405. return;
  6406. }
  6407. var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0;
  6408. var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0;
  6409. this.mouseX = mouseX;
  6410. this.mouseY = mouseY;
  6411. if (this.startClickedObj) {
  6412. var node = this.nodes[this.startClickedObj.row];
  6413. if (!this.startClickedObj.xFixed)
  6414. node.x = this._xToCanvas(mouseX - this.startFrameLeft);
  6415. if (!this.startClickedObj.yFixed)
  6416. node.y = this._yToCanvas(mouseY - this.startFrameTop);
  6417. // start animation if not yet running
  6418. if (!this.hasMovingNodes) {
  6419. this.hasMovingNodes = true;
  6420. this.start();
  6421. }
  6422. }
  6423. else if (this.shiftKeyDown) {
  6424. // draw a rect from start mouse location to current mouse location
  6425. if (this.frame.selRect == undefined) {
  6426. this.frame.selRect = document.createElement("DIV");
  6427. this.frame.appendChild(this.frame.selRect);
  6428. this.frame.selRect.style.position = "absolute";
  6429. this.frame.selRect.style.border = "1px dashed red";
  6430. }
  6431. var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
  6432. var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop;
  6433. var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
  6434. var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
  6435. this.frame.selRect.style.left = left + "px";
  6436. this.frame.selRect.style.top = top + "px";
  6437. this.frame.selRect.style.width = (right - left) + "px";
  6438. this.frame.selRect.style.height = (bottom - top) + "px";
  6439. }
  6440. else {
  6441. // move the network
  6442. var diffX = mouseX - this.startMouseX;
  6443. var diffY = mouseY - this.startMouseY;
  6444. this._setTranslation(
  6445. this.startTranslation.x + diffX,
  6446. this.startTranslation.y + diffY);
  6447. this._redraw();
  6448. this.moved = true;
  6449. }
  6450. Graph.preventDefault(event);
  6451. };
  6452. /**
  6453. * handle on mouse up event
  6454. */
  6455. Graph.prototype._onMouseUp = function (event) {
  6456. event = event || window.event;
  6457. if (!this.selectable) {
  6458. return;
  6459. }
  6460. // remove event listeners here, important for Safari
  6461. if (this.onmousemove) {
  6462. Graph.removeEventListener(document, "mousemove", this.onmousemove);
  6463. this.onmousemove = undefined;
  6464. }
  6465. if (this.onmouseup) {
  6466. Graph.removeEventListener(document, "mouseup", this.onmouseup);
  6467. this.onmouseup = undefined;
  6468. }
  6469. Graph.preventDefault(event);
  6470. // check selected nodes
  6471. var endMouseX = event.clientX || this.mouseX || 0;
  6472. var endMouseY = event.clientY || this.mouseY || 0;
  6473. var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
  6474. if (this.startClickedObj) {
  6475. // restore the original fixed state
  6476. var node = this.nodes[this.startClickedObj.row];
  6477. node.xFixed = this.startClickedObj.xFixed;
  6478. node.yFixed = this.startClickedObj.yFixed;
  6479. }
  6480. else if (this.shiftKeyDown) {
  6481. // select nodes inside selection area
  6482. var obj = {
  6483. "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
  6484. "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
  6485. "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
  6486. "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
  6487. };
  6488. var overlappingNodes = this._getNodesOverlappingWith(obj);
  6489. this._selectNodes(overlappingNodes, ctrlKey);
  6490. this.redraw();
  6491. // remove the selection rectangle
  6492. if (this.frame.selRect) {
  6493. this.frame.removeChild(this.frame.selRect);
  6494. this.frame.selRect = undefined;
  6495. }
  6496. }
  6497. else {
  6498. if (!this.ctrlKeyDown && !this.moved) {
  6499. // remove selection
  6500. this._unselectNodes();
  6501. this._redraw();
  6502. }
  6503. }
  6504. this.leftButtonDown = false;
  6505. this.ctrlKeyDown = false;
  6506. };
  6507. /**
  6508. * Event handler for mouse wheel event, used to zoom the timeline
  6509. * Code from http://adomas.org/javascript-mouse-wheel/
  6510. * @param {event} event The event
  6511. */
  6512. Graph.prototype._onMouseWheel = function(event) {
  6513. event = event || window.event;
  6514. var mouseX = event.clientX;
  6515. var mouseY = event.clientY;
  6516. // retrieve delta
  6517. var delta = 0;
  6518. if (event.wheelDelta) { /* IE/Opera. */
  6519. delta = event.wheelDelta/120;
  6520. } else if (event.detail) { /* Mozilla case. */
  6521. // In Mozilla, sign of delta is different than in IE.
  6522. // Also, delta is multiple of 3.
  6523. delta = -event.detail/3;
  6524. }
  6525. // If delta is nonzero, handle it.
  6526. // Basically, delta is now positive if wheel was scrolled up,
  6527. // and negative, if wheel was scrolled down.
  6528. if (delta) {
  6529. // determine zoom factor, and adjust the zoom factor such that zooming in
  6530. // and zooming out correspond wich each other
  6531. var zoom = delta / 10;
  6532. if (delta < 0) {
  6533. zoom = zoom / (1 - zoom);
  6534. }
  6535. var scaleOld = this._getScale();
  6536. var scaleNew = scaleOld * (1 + zoom);
  6537. if (scaleNew < 0.01) {
  6538. scaleNew = 0.01;
  6539. }
  6540. if (scaleNew > 10) {
  6541. scaleNew = 10;
  6542. }
  6543. var frameLeft = Graph._getAbsoluteLeft(this.frame.canvas);
  6544. var frameTop = Graph._getAbsoluteTop(this.frame.canvas);
  6545. var x = mouseX - frameLeft;
  6546. var y = mouseY - frameTop;
  6547. var translation = this._getTranslation();
  6548. var scaleFrac = scaleNew / scaleOld;
  6549. var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
  6550. var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
  6551. this._setScale(scaleNew);
  6552. this._setTranslation(tx, ty);
  6553. this._redraw();
  6554. }
  6555. // Prevent default actions caused by mouse wheel.
  6556. // That might be ugly, but we handle scrolls somehow
  6557. // anyway, so don't bother here...
  6558. Graph.preventDefault(event);
  6559. };
  6560. /**
  6561. * Mouse move handler for checking whether the title moves over a node or
  6562. * package with a title.
  6563. */
  6564. Graph.prototype._onMouseMoveTitle = function (event) {
  6565. event = event || window.event;
  6566. var startMouseX = event.clientX;
  6567. var startMouseY = event.clientY;
  6568. this.startFrameLeft = this.startFrameLeft || Graph._getAbsoluteLeft(this.frame.canvas);
  6569. this.startFrameTop = this.startFrameTop || Graph._getAbsoluteTop(this.frame.canvas);
  6570. var x = startMouseX - this.startFrameLeft;
  6571. var y = startMouseY - this.startFrameTop;
  6572. // check if the previously selected node is still selected
  6573. if (this.popupNode) {
  6574. this._checkHidePopup(x, y);
  6575. }
  6576. // start a timeout that will check if the mouse is positioned above
  6577. // an element
  6578. var me = this;
  6579. var checkShow = function() {
  6580. me._checkShowPopup(x, y);
  6581. };
  6582. if (this.popupTimer) {
  6583. clearInterval(this.popupTimer); // stop any running timer
  6584. }
  6585. if (!this.leftButtonDown) {
  6586. this.popupTimer = setTimeout(checkShow, 300);
  6587. }
  6588. };
  6589. /**
  6590. * Check if there is an element on the given position in the network (
  6591. * (a node, package, or edge). If so, and if this element has a title,
  6592. * show a popup window with its title.
  6593. *
  6594. * @param {number} x
  6595. * @param {number} y
  6596. */
  6597. Graph.prototype._checkShowPopup = function (x, y) {
  6598. var obj = {
  6599. "left" : this._xToCanvas(x),
  6600. "top" : this._yToCanvas(y),
  6601. "right" : this._xToCanvas(x),
  6602. "bottom" : this._yToCanvas(y)
  6603. };
  6604. var i, len;
  6605. var lastPopupNode = this.popupNode;
  6606. if (this.popupNode == undefined) {
  6607. // search the packages for overlap
  6608. for (i = 0, len = this.packages.length; i < len; i++) {
  6609. var p = this.packages[i];
  6610. if (p.getTitle() != undefined && p.isOverlappingWith(obj)) {
  6611. this.popupNode = p;
  6612. break;
  6613. }
  6614. }
  6615. }
  6616. if (this.popupNode == undefined) {
  6617. // search the nodes for overlap, select the top one in case of multiple nodes
  6618. var nodes = this.nodes;
  6619. for (i = nodes.length - 1; i >= 0; i--) {
  6620. var node = nodes[i];
  6621. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  6622. this.popupNode = node;
  6623. break;
  6624. }
  6625. }
  6626. }
  6627. if (this.popupNode == undefined) {
  6628. // search the edges for overlap
  6629. var allEdges = this.edges;
  6630. for (i = 0, len = allEdges.length; i < len; i++) {
  6631. var edge = allEdges[i];
  6632. if (edge.getTitle() != undefined && edge.isOverlappingWith(obj)) {
  6633. this.popupNode = edge;
  6634. break;
  6635. }
  6636. }
  6637. }
  6638. if (this.popupNode) {
  6639. // show popup message window
  6640. if (this.popupNode != lastPopupNode) {
  6641. var me = this;
  6642. if (!me.popup) {
  6643. me.popup = new Graph.Popup(me.frame);
  6644. }
  6645. // adjust a small offset such that the mouse cursor is located in the
  6646. // bottom left location of the popup, and you can easily move over the
  6647. // popup area
  6648. me.popup.setPosition(x - 3, y - 3);
  6649. me.popup.setText(me.popupNode.getTitle());
  6650. me.popup.show();
  6651. }
  6652. }
  6653. else {
  6654. if (this.popup) {
  6655. this.popup.hide();
  6656. }
  6657. }
  6658. };
  6659. /**
  6660. * Check if the popup must be hided, which is the case when the mouse is no
  6661. * longer hovering on the object
  6662. * @param {number} x
  6663. * @param {number} y
  6664. */
  6665. Graph.prototype._checkHidePopup = function (x, y) {
  6666. var obj = {
  6667. "left" : x,
  6668. "top" : y,
  6669. "right" : x,
  6670. "bottom" : y
  6671. };
  6672. if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
  6673. this.popupNode = undefined;
  6674. if (this.popup) {
  6675. this.popup.hide();
  6676. }
  6677. }
  6678. };
  6679. /**
  6680. * Event handler for touchstart event on mobile devices
  6681. */
  6682. Graph.prototype._onTouchStart = function(event) {
  6683. Graph.preventDefault(event);
  6684. if (this.touchDown) {
  6685. // if already moving, return
  6686. return;
  6687. }
  6688. this.touchDown = true;
  6689. var me = this;
  6690. if (!this.ontouchmove) {
  6691. this.ontouchmove = function (event) {me._onTouchMove(event);};
  6692. Graph.addEventListener(document, "touchmove", this.ontouchmove);
  6693. }
  6694. if (!this.ontouchend) {
  6695. this.ontouchend = function (event) {me._onTouchEnd(event);};
  6696. Graph.addEventListener(document, "touchend", this.ontouchend);
  6697. }
  6698. this._onMouseDown(event);
  6699. };
  6700. /**
  6701. * Event handler for touchmove event on mobile devices
  6702. */
  6703. Graph.prototype._onTouchMove = function(event) {
  6704. Graph.preventDefault(event);
  6705. this._onMouseMove(event);
  6706. };
  6707. /**
  6708. * Event handler for touchend event on mobile devices
  6709. */
  6710. Graph.prototype._onTouchEnd = function(event) {
  6711. Graph.preventDefault(event);
  6712. this.touchDown = false;
  6713. if (this.ontouchmove) {
  6714. Graph.removeEventListener(document, "touchmove", this.ontouchmove);
  6715. this.ontouchmove = undefined;
  6716. }
  6717. if (this.ontouchend) {
  6718. Graph.removeEventListener(document, "touchend", this.ontouchend);
  6719. this.ontouchend = undefined;
  6720. }
  6721. this._onMouseUp(event);
  6722. };
  6723. /**
  6724. * Unselect selected nodes. If no selection array is provided, all nodes
  6725. * are unselected
  6726. * @param {Object[]} selection Array with selection objects, each selection
  6727. * object has a parameter row. Optional
  6728. * @param {Boolean} triggerSelect If true (default), the select event
  6729. * is triggered when nodes are unselected
  6730. * @return {Boolean} changed True if the selection is changed
  6731. */
  6732. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  6733. var changed = false;
  6734. var i, iMax, row;
  6735. if (selection) {
  6736. // remove provided selections
  6737. for (i = 0, iMax = selection.length; i < iMax; i++) {
  6738. row = selection[i].row;
  6739. this.nodes[row].unselect();
  6740. var j = 0;
  6741. while (j < this.selection.length) {
  6742. if (this.selection[j].row == row) {
  6743. this.selection.splice(j, 1);
  6744. changed = true;
  6745. }
  6746. else {
  6747. j++;
  6748. }
  6749. }
  6750. }
  6751. }
  6752. else if (this.selection && this.selection.length) {
  6753. // remove all selections
  6754. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  6755. row = this.selection[i].row;
  6756. this.nodes[row].unselect();
  6757. changed = true;
  6758. }
  6759. this.selection = [];
  6760. }
  6761. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  6762. // fire the select event
  6763. this.trigger('select');
  6764. }
  6765. return changed;
  6766. };
  6767. /**
  6768. * select all nodes on given location x, y
  6769. * @param {Array} selection an array with selection objects. Each selection
  6770. * object has a parameter row
  6771. * @param {boolean} append If true, the new selection will be appended to the
  6772. * current selection (except for duplicate entries)
  6773. * @return {Boolean} changed True if the selection is changed
  6774. */
  6775. Graph.prototype._selectNodes = function(selection, append) {
  6776. var changed = false;
  6777. var i, iMax;
  6778. // TODO: the selectNodes method is a little messy, rework this
  6779. // check if the current selection equals the desired selection
  6780. var selectionAlreadyDone = true;
  6781. if (selection.length != this.selection.length) {
  6782. selectionAlreadyDone = false;
  6783. }
  6784. else {
  6785. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  6786. if (selection[i].row != this.selection[i].row) {
  6787. selectionAlreadyDone = false;
  6788. break;
  6789. }
  6790. }
  6791. }
  6792. if (selectionAlreadyDone) {
  6793. return changed;
  6794. }
  6795. if (append == undefined || append == false) {
  6796. // first deselect any selected node
  6797. var triggerSelect = false;
  6798. changed = this._unselectNodes(undefined, triggerSelect);
  6799. }
  6800. for (i = 0, iMax = selection.length; i < iMax; i++) {
  6801. // add each of the new selections, but only when they are not duplicate
  6802. var row = selection[i].row;
  6803. var isDuplicate = false;
  6804. for (var j = 0, jMax = this.selection.length; j < jMax; j++) {
  6805. if (this.selection[j].row == row) {
  6806. isDuplicate = true;
  6807. break;
  6808. }
  6809. }
  6810. if (!isDuplicate) {
  6811. this.nodes[row].select();
  6812. this.selection.push(selection[i]);
  6813. changed = true;
  6814. }
  6815. }
  6816. if (changed) {
  6817. // fire the select event
  6818. this.trigger('select');
  6819. }
  6820. return changed;
  6821. };
  6822. /**
  6823. * retrieve all nodes overlapping with given object
  6824. * @param {Object} obj An object with parameters left, top, right, bottom
  6825. * @return {Object[]} An array with selection objects containing
  6826. * the parameter row.
  6827. */
  6828. Graph.prototype._getNodesOverlappingWith = function (obj) {
  6829. var overlappingNodes = [];
  6830. for (var i = 0; i < this.nodes.length; i++) {
  6831. if (this.nodes[i].isOverlappingWith(obj)) {
  6832. var sel = {"row": i};
  6833. overlappingNodes.push(sel);
  6834. }
  6835. }
  6836. return overlappingNodes;
  6837. };
  6838. /**
  6839. * retrieve the currently selected nodes
  6840. * @return {Object[]} an array with zero or more objects. Each object
  6841. * contains the parameter row
  6842. */
  6843. Graph.prototype.getSelection = function() {
  6844. var selection = [];
  6845. for (var i = 0; i < this.selection.length; i++) {
  6846. var row = this.selection[i].row;
  6847. selection.push({"row": row});
  6848. }
  6849. return selection;
  6850. };
  6851. /**
  6852. * select zero or more nodes
  6853. * @param {object[]} selection an array with zero or more objects. Each object
  6854. * contains the parameter row
  6855. */
  6856. Graph.prototype.setSelection = function(selection) {
  6857. var i, iMax, row;
  6858. if (selection.length == undefined)
  6859. throw "Selection must be an array with objects";
  6860. // first unselect any selected node
  6861. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  6862. row = this.selection[i].row;
  6863. this.nodes[row].unselect();
  6864. }
  6865. this.selection = [];
  6866. for (i = 0, iMax = selection.length; i < iMax; i++) {
  6867. row = selection[i].row;
  6868. if (row == undefined)
  6869. throw "Parameter row missing in selection object";
  6870. if (row > this.nodes.length-1)
  6871. throw "Parameter row out of range";
  6872. var sel = {"row": row};
  6873. this.selection.push(sel);
  6874. this.nodes[row].select();
  6875. }
  6876. this.redraw();
  6877. };
  6878. /**
  6879. * Temporary method to test calculating a hub value for the nodes
  6880. * @param {number} level Maximum number edges between two nodes in order
  6881. * to call them connected. Optional, 1 by default
  6882. * @return {Number[]} connectioncount array with the connection count
  6883. * for each node
  6884. */
  6885. Graph.prototype._getConnectionCount = function(level) {
  6886. var conn = this.edges;
  6887. if (level == undefined) {
  6888. level = 1;
  6889. }
  6890. // get the nodes connected to given nodes
  6891. function getConnectedNodes(nodes) {
  6892. var connectedNodes = [];
  6893. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  6894. var node = nodes[j];
  6895. // find all nodes connected to this node
  6896. for (var i = 0, iMax = conn.length; i < iMax; i++) {
  6897. var other = null;
  6898. // check if connected
  6899. if (conn[i].from == node)
  6900. other = conn[i].to;
  6901. else if (conn[i].to == node)
  6902. other = conn[i].from;
  6903. // check if the other node is not already in the list with nodes
  6904. var k, kMax;
  6905. if (other) {
  6906. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  6907. if (nodes[k] == other) {
  6908. other = null;
  6909. break;
  6910. }
  6911. }
  6912. }
  6913. if (other) {
  6914. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  6915. if (connectedNodes[k] == other) {
  6916. other = null;
  6917. break;
  6918. }
  6919. }
  6920. }
  6921. if (other)
  6922. connectedNodes.push(other);
  6923. }
  6924. }
  6925. return connectedNodes;
  6926. }
  6927. var connections = [];
  6928. var level0 = [];
  6929. var nodes = this.nodes;
  6930. var i, iMax;
  6931. for (i = 0, iMax = nodes.length; i < iMax; i++) {
  6932. var c = [nodes[i]];
  6933. for (var l = 0; l < level; l++) {
  6934. c = c.concat(getConnectedNodes(c));
  6935. }
  6936. connections.push(c);
  6937. }
  6938. var hubs = [];
  6939. for (i = 0, len = connections.length; i < len; i++) {
  6940. hubs.push(connections[i].length);
  6941. }
  6942. return hubs;
  6943. };
  6944. /**
  6945. * Set a new size for the network
  6946. * @param {string} width Width in pixels or percentage (for example "800px"
  6947. * or "50%")
  6948. * @param {string} height Height in pixels or percentage (for example "400px"
  6949. * or "30%")
  6950. */
  6951. Graph.prototype._setSize = function(width, height) {
  6952. this.frame.style.width = width;
  6953. this.frame.style.height = height;
  6954. this.frame.canvas.style.width = "100%";
  6955. this.frame.canvas.style.height = "100%";
  6956. this.frame.canvas.width = this.frame.canvas.clientWidth;
  6957. this.frame.canvas.height = this.frame.canvas.clientHeight;
  6958. if (this.slider) {
  6959. this.slider.redraw();
  6960. }
  6961. };
  6962. /**
  6963. * Convert a Google DataTable to a Javascript Array
  6964. * @param {google.visualization.DataTable} table
  6965. * @return {Array} array
  6966. */
  6967. Graph.tableToArray = function(table) {
  6968. var array = [];
  6969. var col;
  6970. // read the column names
  6971. var colCount = table.getNumberOfColumns();
  6972. var cols = {};
  6973. for (col = 0; col < colCount; col++) {
  6974. var label = table.getColumnLabel(col);
  6975. cols[label] = col;
  6976. }
  6977. var rowCount = table.getNumberOfRows();
  6978. for (var i = 0; i < rowCount; i++) {
  6979. // copy all properties from the table columns to an object
  6980. var properties = {};
  6981. for (col in cols) {
  6982. if (cols.hasOwnProperty(col)) {
  6983. properties[col] = table.getValue(i, cols[col]);
  6984. }
  6985. }
  6986. array.push(properties);
  6987. }
  6988. return array;
  6989. };
  6990. /**
  6991. * Append nodes
  6992. * Nodes with a duplicate id will be replaced
  6993. * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes.
  6994. */
  6995. Graph.prototype.addNodes = function(nodesTable) {
  6996. var table;
  6997. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  6998. nodesTable instanceof google.visualization.DataTable) {
  6999. // Google DataTable.
  7000. // Convert to a Javascript Array
  7001. table = Graph.tableToArray(nodesTable);
  7002. }
  7003. else if (Graph.isArray(nodesTable)){
  7004. // Javascript Array
  7005. table = nodesTable;
  7006. }
  7007. else {
  7008. return;
  7009. }
  7010. var hasValues = false;
  7011. var rowCount = table.length;
  7012. for (var i = 0; i < rowCount; i++) {
  7013. var properties = table[i];
  7014. if (properties.value != undefined) {
  7015. hasValues = true;
  7016. }
  7017. if (properties.id == undefined) {
  7018. throw "Column 'id' missing in table with nodes (row " + i + ")";
  7019. }
  7020. this._createNode(properties);
  7021. }
  7022. // calculate scaling function when value is provided
  7023. if (hasValues) {
  7024. this._updateValueRange(this.nodes);
  7025. }
  7026. this.start();
  7027. };
  7028. /**
  7029. * Load all nodes by reading the data table nodesTable
  7030. * Note that Object DataTable is defined in google.visualization.DataTable
  7031. * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes.
  7032. */
  7033. Graph.prototype.setNodes = function(nodesTable) {
  7034. var table;
  7035. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  7036. nodesTable instanceof google.visualization.DataTable) {
  7037. // Google DataTable.
  7038. // Convert to a Javascript Array
  7039. table = Graph.tableToArray(nodesTable);
  7040. }
  7041. else if (Graph.isArray(nodesTable)){
  7042. // Javascript Array
  7043. table = nodesTable;
  7044. }
  7045. else {
  7046. return;
  7047. }
  7048. this.hasMovingNodes = false;
  7049. this.nodesTable = table;
  7050. this.nodes = [];
  7051. this.selection = [];
  7052. var hasValues = false;
  7053. var rowCount = table.length;
  7054. for (var i = 0; i < rowCount; i++) {
  7055. var properties = table[i];
  7056. if (properties.value != undefined) {
  7057. hasValues = true;
  7058. }
  7059. if (properties.timestamp) {
  7060. this.hasTimestamps = this.hasTimestamps || properties.timestamp;
  7061. }
  7062. if (properties.id == undefined) {
  7063. throw "Column 'id' missing in table with nodes (row " + i + ")";
  7064. }
  7065. this._createNode(properties);
  7066. }
  7067. // calculate scaling function when value is provided
  7068. if (hasValues) {
  7069. this._updateValueRange(this.nodes);
  7070. }
  7071. };
  7072. /**
  7073. * Filter the current nodes table for nodes with a timestamp older than given
  7074. * timestamp. Can only be used for nodes added via setNodes(), not via
  7075. * addNodes().
  7076. * @param {*} [timestamp] If timestamp is undefined, all nodes are shown
  7077. */
  7078. Graph.prototype._filterNodes = function(timestamp) {
  7079. if (this.nodesTable == undefined) {
  7080. return;
  7081. }
  7082. // remove existing nodes with a too new timestamp
  7083. if (timestamp !== undefined) {
  7084. var ns = this.nodes;
  7085. var n = 0;
  7086. while (n < ns.length) {
  7087. var t = ns[n].timestamp;
  7088. if (t !== undefined && t > timestamp) {
  7089. // remove this node
  7090. ns.splice(n, 1);
  7091. }
  7092. else {
  7093. n++;
  7094. }
  7095. }
  7096. }
  7097. // add all nodes with an old enough timestamp
  7098. var table = this.nodesTable;
  7099. var rowCount = table.length;
  7100. for (var i = 0; i < rowCount; i++) {
  7101. // copy all properties
  7102. var properties = table[i];
  7103. if (properties.id === undefined) {
  7104. throw "Column 'id' missing in table with nodes (row " + i + ")";
  7105. }
  7106. // check what the timestamp is
  7107. var ts = properties.timestamp ? properties.timestamp : undefined;
  7108. var visible = true;
  7109. if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
  7110. visible = false;
  7111. }
  7112. if (visible) {
  7113. // create or update the node
  7114. this._createNode(properties);
  7115. }
  7116. }
  7117. this.start();
  7118. };
  7119. /**
  7120. * Create a node with the given properties
  7121. * If the new node has an id identical to an existing package, the existing
  7122. * node will be overwritten.
  7123. * The properties can contain a property "action", which can have values
  7124. * "create", "update", or "delete"
  7125. * @param {Object} properties An object with properties
  7126. */
  7127. Graph.prototype._createNode = function(properties) {
  7128. var action = properties.action ? properties.action : "update";
  7129. var id, index, newNode, oldNode;
  7130. if (action === "create") {
  7131. // create the node
  7132. newNode = new Graph.Node(properties, this.images, this.groups, this.constants);
  7133. id = properties.id;
  7134. index = (id !== undefined) ? this._findNode(id) : undefined;
  7135. if (index !== undefined) {
  7136. // replace node
  7137. oldNode = this.nodes[index];
  7138. this.nodes[index] = newNode;
  7139. // remove selection of old node
  7140. if (oldNode.selected) {
  7141. this._unselectNodes([{'row': index}], false);
  7142. }
  7143. /* TODO: implement this? -> will give performance issues, searching all edges and node...
  7144. // update edges linking to this node
  7145. var edgesTable = this.edges;
  7146. for (var i = 0, iMax = edgesTable.length; i < iMax; i++) {
  7147. var edge = edgesTable[i];
  7148. if (edge.from == oldNode) {
  7149. edge.from = newNode;
  7150. }
  7151. if (edge.to == oldNode) {
  7152. edge.to = newNode;
  7153. }
  7154. }
  7155. // update packages linking to this node
  7156. var packagesTable = this.packages;
  7157. for (var i = 0, iMax = packagesTable.length; i < iMax; i++) {
  7158. var package = packagesTable[i];
  7159. if (package.from == oldNode) {
  7160. package.from = newNode;
  7161. }
  7162. if (package.to == oldNode) {
  7163. package.to = newNode;
  7164. }
  7165. }
  7166. */
  7167. }
  7168. else {
  7169. // add new node
  7170. this.nodes.push(newNode);
  7171. }
  7172. if (!newNode.isFixed()) {
  7173. // note: no not use node.isMoving() here, as that gives the current
  7174. // velocity of the node, which is zero after creation of the node.
  7175. this.hasMovingNodes = true;
  7176. }
  7177. }
  7178. else if (action === "update") {
  7179. // update existing node, or create it when not yet existing
  7180. id = properties.id;
  7181. if (id === undefined) {
  7182. throw "Cannot update a node without id";
  7183. }
  7184. index = this._findNode(id);
  7185. if (index !== undefined) {
  7186. // update node
  7187. this.nodes[index].setProperties(properties, this.constants);
  7188. }
  7189. else {
  7190. // create node
  7191. newNode = new Graph.Node(properties, this.images, this.groups, this.constants);
  7192. this.nodes.push(newNode);
  7193. if (!newNode.isFixed()) {
  7194. // note: no not use node.isMoving() here, as that gives the current
  7195. // velocity of the node, which is zero after creation of the node.
  7196. this.hasMovingNodes = true;
  7197. }
  7198. }
  7199. }
  7200. else if (action === "delete") {
  7201. // delete existing node
  7202. id = properties.id;
  7203. if (id === undefined) {
  7204. throw "Cannot delete node without its id";
  7205. }
  7206. index = this._findNode(id);
  7207. if (index !== undefined) {
  7208. oldNode = this.nodes[index];
  7209. // remove selection of old node
  7210. if (oldNode.selected) {
  7211. this._unselectNodes([{'row': index}], false);
  7212. }
  7213. this.nodes.splice(index, 1);
  7214. }
  7215. else {
  7216. throw "Node with id " + id + " not found";
  7217. }
  7218. }
  7219. else {
  7220. throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
  7221. }
  7222. };
  7223. /**
  7224. * Find a node by its id
  7225. * @param {Number} id Id of the node
  7226. * @return {Number} index Index of the node in the array this.nodes, or
  7227. * undefined when not found. *
  7228. */
  7229. Graph.prototype._findNode = function (id) {
  7230. var nodes = this.nodes;
  7231. for (var n = 0, len = nodes.length; n < len; n++) {
  7232. if (nodes[n].id === id) {
  7233. return n;
  7234. }
  7235. }
  7236. return undefined;
  7237. };
  7238. /**
  7239. * Find a node by its rowNumber
  7240. * @param {Number} row Row number of the node
  7241. * @return {Graph.Node} node     The node with the given row number, or
  7242. *                            undefined when not found.
  7243. */
  7244. Graph.prototype._findNodeByRow = function (row) {
  7245. return this.nodes[row];
  7246. };
  7247. /**
  7248. * Load edges by reading the data table
  7249. * Note that Object DataTable is defined in google.visualization.DataTable
  7250. * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges.
  7251. */
  7252. Graph.prototype.setEdges = function(edgesTable) {
  7253. var table;
  7254. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  7255. edgesTable instanceof google.visualization.DataTable) {
  7256. // Google DataTable.
  7257. // Convert to a Javascript Array
  7258. table = Graph.tableToArray(edgesTable);
  7259. }
  7260. else if (Graph.isArray(edgesTable)){
  7261. // Javascript Array
  7262. table = edgesTable;
  7263. }
  7264. else {
  7265. return;
  7266. }
  7267. this.edgesTable = table;
  7268. this.edges = [];
  7269. this.hasMovingEdges = false;
  7270. var hasValues = false;
  7271. var rowCount = table.length;
  7272. for (var i = 0; i < rowCount; i++) {
  7273. var properties = table[i];
  7274. if (properties.from === undefined) {
  7275. throw "Column 'from' missing in table with edges (row " + i + ")";
  7276. }
  7277. if (properties.to === undefined) {
  7278. throw "Column 'to' missing in table with edges (row " + i + ")";
  7279. }
  7280. if (properties.timestamp != undefined) {
  7281. this.hasTimestamps = this.hasTimestamps || properties.timestamp;
  7282. }
  7283. if (properties.value != undefined) {
  7284. hasValues = true;
  7285. }
  7286. this._createEdge(properties);
  7287. }
  7288. // calculate scaling function when value is provided
  7289. if (hasValues) {
  7290. this._updateValueRange(this.edges);
  7291. }
  7292. };
  7293. /**
  7294. * Load edges by reading the data table
  7295. * Note that Object DataTable is defined in google.visualization.DataTable
  7296. * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges.
  7297. */
  7298. Graph.prototype.addEdges = function(edgesTable) {
  7299. var table;
  7300. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  7301. edgesTable instanceof google.visualization.DataTable) {
  7302. // Google DataTable.
  7303. // Convert to a Javascript Array
  7304. table = Graph.tableToArray(edgesTable);
  7305. }
  7306. else if (Graph.isArray(edgesTable)){
  7307. // Javascript Array
  7308. table = edgesTable;
  7309. }
  7310. else {
  7311. return;
  7312. }
  7313. var hasValues = false;
  7314. var rowCount = table.length;
  7315. for (var i = 0; i < rowCount; i++) {
  7316. // copy all properties
  7317. var properties = table[i];
  7318. if (properties.from === undefined) {
  7319. throw "Column 'from' missing in table with edges (row " + i + ")";
  7320. }
  7321. if (properties.to === undefined) {
  7322. throw "Column 'to' missing in table with edges (row " + i + ")";
  7323. }
  7324. if (properties.value != undefined) {
  7325. hasValues = true;
  7326. }
  7327. this._createEdge(properties);
  7328. }
  7329. // calculate scaling function when value is provided
  7330. if (hasValues) {
  7331. this._updateValueRange(this.edges);
  7332. }
  7333. this.start();
  7334. };
  7335. /**
  7336. * Filter the current edges table for edges with a timestamp below given
  7337. * timestamp. Can only be used for edges added via setEdges(), not via
  7338. * addEdges().
  7339. * @param {*} [timestamp] If timestamp is undefined, all edges are shown
  7340. */
  7341. Graph.prototype._filterEdges = function(timestamp) {
  7342. if (this.edgesTable == undefined) {
  7343. return;
  7344. }
  7345. // remove existing packages with a too new timestamp
  7346. if (timestamp !== undefined) {
  7347. var ls = this.edges;
  7348. var l = 0;
  7349. while (l < ls.length) {
  7350. var t = ls[l].timestamp;
  7351. if (t !== undefined && t > timestamp) {
  7352. // remove this edge
  7353. ls.splice(l, 1);
  7354. }
  7355. else {
  7356. l++;
  7357. }
  7358. }
  7359. }
  7360. // add all edges with an old enough timestamp
  7361. var table = this.edgesTable;
  7362. var rowCount = table.length;
  7363. for (var i = 0; i < rowCount; i++) {
  7364. var properties = table[i];
  7365. if (properties.from === undefined) {
  7366. throw "Column 'from' missing in table with edges (row " + i + ")";
  7367. }
  7368. if (properties.to === undefined) {
  7369. throw "Column 'to' missing in table with edges (row " + i + ")";
  7370. }
  7371. // check what the timestamp is
  7372. var ts = properties.timestamp ? properties.timestamp : undefined;
  7373. var visible = true;
  7374. if (ts !== undefined && timestamp !== undefined && ts > timestamp) {
  7375. visible = false;
  7376. }
  7377. if (visible) {
  7378. // create or update the edge
  7379. this._createEdge(properties);
  7380. }
  7381. }
  7382. this.start();
  7383. };
  7384. /**
  7385. * Create a edge with the given properties
  7386. * If the new edge has an id identical to an existing edge, the existing
  7387. * edge will be overwritten or updated.
  7388. * The properties can contain a property "action", which can have values
  7389. * "create", "update", or "delete"
  7390. * @param {Object} properties An object with properties
  7391. */
  7392. Graph.prototype._createEdge = function(properties) {
  7393. var action = properties.action ? properties.action : "create";
  7394. var id, index, edge, oldEdge, newEdge;
  7395. if (action === "create") {
  7396. // create the edge, or replace it if already existing
  7397. id = properties.id;
  7398. index = (id !== undefined) ? this._findEdge(id) : undefined;
  7399. edge = new Graph.Edge(properties, this, this.constants);
  7400. if (index !== undefined) {
  7401. // replace existing edge
  7402. oldEdge = this.edges[index];
  7403. oldEdge.from.detachEdge(oldEdge);
  7404. oldEdge.to.detachEdge(oldEdge);
  7405. this.edges[index] = edge;
  7406. }
  7407. else {
  7408. // add new edge
  7409. this.edges.push(edge);
  7410. }
  7411. edge.from.attachEdge(edge);
  7412. edge.to.attachEdge(edge);
  7413. if (edge.isMoving()) {
  7414. this.hasMovingEdges = true;
  7415. }
  7416. }
  7417. else if (action === "update") {
  7418. // update existing edge, or create the edge if not existing
  7419. id = properties.id;
  7420. if (id === undefined) {
  7421. throw "Cannot update a edge without id";
  7422. }
  7423. index = this._findEdge(id);
  7424. if (index !== undefined) {
  7425. // update edge
  7426. edge = this.edges[index];
  7427. edge.from.detachEdge(edge);
  7428. edge.to.detachEdge(edge);
  7429. edge.setProperties(properties, this.constants);
  7430. edge.from.attachEdge(edge);
  7431. edge.to.attachEdge(edge);
  7432. }
  7433. else {
  7434. // add new edge
  7435. edge = new Graph.Edge(properties, this, this.constants);
  7436. edge.from.attachEdge(edge);
  7437. edge.to.attachEdge(edge);
  7438. this.edges.push(edge);
  7439. if (edge.isMoving()) {
  7440. this.hasMovingEdges = true;
  7441. }
  7442. }
  7443. }
  7444. else if (action === "delete") {
  7445. // delete existing edge
  7446. id = properties.id;
  7447. if (id === undefined) {
  7448. throw "Cannot delete edge without its id";
  7449. }
  7450. index = this._findEdge(id);
  7451. if (index !== undefined) {
  7452. oldEdge = this.edges[id];
  7453. edge.from.detachEdge(oldEdge);
  7454. edge.to.detachEdge(oldEdge);
  7455. this.edges.splice(index, 1);
  7456. }
  7457. else {
  7458. throw "Edge with id " + id + " not found";
  7459. }
  7460. }
  7461. else {
  7462. throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
  7463. }
  7464. };
  7465. /**
  7466. * Update the edge to oldNode in all edges and packages.
  7467. * @param {Node} oldNode
  7468. * @param {Node} newNode
  7469. */
  7470. // TODO: start utilizing this method _updateNodeReferences
  7471. Graph.prototype._updateNodeReferences = function(oldNode, newNode) {
  7472. var arrays = [this.edges, this.packages];
  7473. for (var a = 0, aMax = arrays.length; a < aMax; a++) {
  7474. var array = arrays[a];
  7475. for (var i = 0, iMax = array.length; i < iMax; i++) {
  7476. if (array.from === oldNode) {
  7477. array.from = newNode;
  7478. }
  7479. if (array.to === oldNode) {
  7480. array.to = newNode;
  7481. }
  7482. }
  7483. }
  7484. };
  7485. /**
  7486. * Find a edge by its id
  7487. * @param {Number} id Id of the edge
  7488. * @return {Number} index Index of the edge in the array this.edges, or
  7489. * undefined when not found. *
  7490. */
  7491. Graph.prototype._findEdge = function (id) {
  7492. var edges = this.edges;
  7493. for (var n = 0, len = edges.length; n < len; n++) {
  7494. if (edges[n].id === id) {
  7495. return n;
  7496. }
  7497. }
  7498. return undefined;
  7499. };
  7500. /**
  7501. * Find a edge by its row
  7502. * @param {Number} row Row of the edge
  7503. * @return {Graph.Edge} the found edge, or undefined when not found
  7504. */
  7505. Graph.prototype._findEdgeByRow = function (row) {
  7506. return this.edges[row];
  7507. };
  7508. /**
  7509. * Append packages
  7510. * Packages with a duplicate id will be replaced
  7511. * Note that Object DataTable is defined in google.visualization.DataTable
  7512. * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages.
  7513. */
  7514. Graph.prototype.addPackages = function(packagesTable) {
  7515. var table;
  7516. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  7517. packagesTable instanceof google.visualization.DataTable) {
  7518. // Google DataTable.
  7519. // Convert to a Javascript Array
  7520. table = Graph.tableToArray(packagesTable);
  7521. }
  7522. else if (Graph.isArray(packagesTable)){
  7523. // Javascript Array
  7524. table = packagesTable;
  7525. }
  7526. else {
  7527. return;
  7528. }
  7529. var rowCount = table.length;
  7530. for (var i = 0; i < rowCount; i++) {
  7531. var properties = table[i];
  7532. if (properties.from === undefined) {
  7533. throw "Column 'from' missing in table with packages (row " + i + ")";
  7534. }
  7535. if (properties.to === undefined) {
  7536. throw "Column 'to' missing in table with packages (row " + i + ")";
  7537. }
  7538. this._createPackage(properties);
  7539. }
  7540. // calculate scaling function when value is provided
  7541. this._updateValueRange(this.packages);
  7542. this.start();
  7543. };
  7544. /**
  7545. * Set a new packages table
  7546. * Packages with a duplicate id will be replaced
  7547. * Note that Object DataTable is defined in google.visualization.DataTable
  7548. * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages.
  7549. */
  7550. Graph.prototype.setPackages = function(packagesTable) {
  7551. var table;
  7552. if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable &&
  7553. packagesTable instanceof google.visualization.DataTable) {
  7554. // Google DataTable.
  7555. // Convert to a Javascript Array
  7556. table = Graph.tableToArray(packagesTable);
  7557. }
  7558. else if (Graph.isArray(packagesTable)){
  7559. // Javascript Array
  7560. table = packagesTable;
  7561. }
  7562. else {
  7563. return;
  7564. }
  7565. this.packagesTable = table;
  7566. this.packages = [];
  7567. var rowCount = table.length;
  7568. for (var i = 0; i < rowCount; i++) {
  7569. var properties = table[i];
  7570. if (properties.from === undefined) {
  7571. throw "Column 'from' missing in table with packages (row " + i + ")";
  7572. }
  7573. if (properties.to === undefined) {
  7574. throw "Column 'to' missing in table with packages (row " + i + ")";
  7575. }
  7576. if (properties.timestamp) {
  7577. this.hasTimestamps = this.hasTimestamps || properties.timestamp;
  7578. }
  7579. this._createPackage(properties);
  7580. }
  7581. // calculate scaling function when value is provided
  7582. this._updateValueRange(this.packages);
  7583. /* TODO: adjust examples and documentation for this?
  7584. this.start();
  7585. */
  7586. };
  7587. /**
  7588. * Filter the current package table for packages with a timestamp below given
  7589. * timestamp. Can only be used for packages added via setPackages(), not via
  7590. * addPackages().
  7591. * @param {*} [timestamp] If timestamp is undefined, all packages are shown
  7592. */
  7593. Graph.prototype._filterPackages = function(timestamp) {
  7594. if (this.packagesTable == undefined) {
  7595. return;
  7596. }
  7597. // remove all current packages
  7598. this.packages = [];
  7599. /* TODO: cleanup
  7600. // remove existing packages with a too new timestamp
  7601. if (timestamp !== undefined) {
  7602. var packages = this.packages;
  7603. var p = 0;
  7604. while (p < packages.length) {
  7605. var package = packages[p];
  7606. var t = package.timestamp;
  7607. if (t !== undefined && t > timestamp ) {
  7608. // remove this package
  7609. packages.splice(p, 1);
  7610. }
  7611. else {
  7612. p++;
  7613. }
  7614. }
  7615. }
  7616. */
  7617. // add all packages with an old enough timestamp
  7618. var table = this.packagesTable;
  7619. var rowCount = table.length;
  7620. for (var i = 0; i < rowCount; i++) {
  7621. var properties = table[i];
  7622. if (properties.from === undefined) {
  7623. throw "Column 'from' missing in table with packages (row " + i + ")";
  7624. }
  7625. if (properties.to === undefined) {
  7626. throw "Column 'to' missing in table with packages (row " + i + ")";
  7627. }
  7628. // check what the timestamp is
  7629. var pTimestamp = properties.timestamp ? properties.timestamp : undefined;
  7630. var visible = true;
  7631. if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) {
  7632. visible = false;
  7633. }
  7634. if (visible === true) {
  7635. if (properties.progress == undefined) {
  7636. // when no progress is provided, we need to add our own progress
  7637. var duration = properties.duration || this.constants.packages.duration; // seconds
  7638. var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds
  7639. if (diff < duration) {
  7640. // copy the properties, and fill in the current progress based on the
  7641. // timestamp and the duration
  7642. var original = properties;
  7643. properties = {};
  7644. for (var j in original) {
  7645. if (original.hasOwnProperty(j)) {
  7646. properties[j] = original[j];
  7647. }
  7648. }
  7649. properties.progress = diff / duration; // scale 0-1
  7650. }
  7651. else {
  7652. visible = false;
  7653. }
  7654. }
  7655. }
  7656. if (visible === true) {
  7657. // create or update the package
  7658. this._createPackage(properties);
  7659. }
  7660. }
  7661. this.start();
  7662. };
  7663. /**
  7664. * Create a package with the given properties
  7665. * If the new package has an id identical to an existing package, the existing
  7666. * package will be overwritten.
  7667. * The properties can contain a property "action", which can have values
  7668. * "create", "update", or "delete"
  7669. * @param {Object} properties An object with properties
  7670. */
  7671. Graph.prototype._createPackage = function(properties) {
  7672. var action = properties.action ? properties.action : "create";
  7673. var id, index, newPackage;
  7674. if (action === "create") {
  7675. // create the package
  7676. id = properties.id;
  7677. index = (id !== undefined) ? this._findPackage(id) : undefined;
  7678. newPackage = new Graph.Package(properties, this, this.images, this.constants);
  7679. if (index !== undefined) {
  7680. // replace existing package
  7681. this.packages[index] = newPackage;
  7682. }
  7683. else {
  7684. // add new package
  7685. this.packages.push(newPackage);
  7686. }
  7687. if (newPackage.isMoving()) {
  7688. this.hasMovingPackages = true;
  7689. }
  7690. }
  7691. else if (action === "update") {
  7692. // update a package, or create it when not existing
  7693. id = properties.id;
  7694. if (id === undefined) {
  7695. throw "Cannot update a edge without id";
  7696. }
  7697. index = this._findPackage(id);
  7698. if (index !== undefined) {
  7699. // update existing package
  7700. this.packages[index].setProperties(properties, this.constants);
  7701. }
  7702. else {
  7703. // add new package
  7704. newPackage = new Graph.Package(properties, this, this.images, this.constants);
  7705. this.packages.push(newPackage);
  7706. if (newPackage.isMoving()) {
  7707. this.hasMovingPackages = true;
  7708. }
  7709. }
  7710. }
  7711. else if (action === "delete") {
  7712. // delete existing package
  7713. id = properties.id;
  7714. if (id === undefined) {
  7715. throw "Cannot delete package without its id";
  7716. }
  7717. index = this._findPackage(id);
  7718. if (index !== undefined) {
  7719. this.packages.splice(index, 1);
  7720. }
  7721. else {
  7722. throw "Package with id " + id + " not found";
  7723. }
  7724. }
  7725. else {
  7726. throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
  7727. }
  7728. };
  7729. /**
  7730. * Find a package by its id.
  7731. * @param {Number} id
  7732. * @return {Number} index Index of the package in the array this.packages,
  7733. * or undefined when not found
  7734. */
  7735. Graph.prototype._findPackage = function (id) {
  7736. var packages = this.packages;
  7737. for (var n = 0, len = packages.length; n < len; n++) {
  7738. if (packages[n].id === id) {
  7739. return n;
  7740. }
  7741. }
  7742. return undefined;
  7743. };
  7744. /**
  7745. * Find a package by its row
  7746. * @param {Number} row Row of the package
  7747. * @return {Graph.Package} the found package, or undefined when not found
  7748. */
  7749. Graph.prototype._findPackageByRow = function (row) {
  7750. return this.packages[row];
  7751. };
  7752. /**
  7753. * Retrieve an object which maps the column ids by their names
  7754. * For example a table with columns [id, name, value] will return an
  7755. * object {"id": 0, "name": 1, "value": 2}
  7756. * @param {google.visualization.DataTable} table A google datatable
  7757. * @return {Object} columnIds An object
  7758. */
  7759. // TODO: cleanup this unused method
  7760. Graph.prototype._getColumnNames = function (table) {
  7761. var colCount = table.getNumberOfColumns();
  7762. var cols = {};
  7763. for (var col = 0; col < colCount; col++) {
  7764. var label = table.getColumnLabel(col);
  7765. cols[label] = col;
  7766. }
  7767. return cols;
  7768. };
  7769. /**
  7770. * Update the values of all object in the given array according to the current
  7771. * value range of the objects in the array.
  7772. * @param {Array} array. An array with objects like Edges, Nodes, or Packages
  7773. * The objects must have a method getValue() and
  7774. * setValueRange(min, max).
  7775. */
  7776. Graph.prototype._updateValueRange = function(array) {
  7777. var count = array.length;
  7778. var i;
  7779. // determine the range of the node values
  7780. var valueMin = undefined;
  7781. var valueMax = undefined;
  7782. for (i = 0; i < count; i++) {
  7783. var value = array[i].getValue();
  7784. if (value !== undefined) {
  7785. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  7786. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  7787. }
  7788. }
  7789. // adjust the range of all nodes
  7790. if (valueMin !== undefined && valueMax !== undefined) {
  7791. for (i = 0; i < count; i++) {
  7792. array[i].setValueRange(valueMin, valueMax);
  7793. }
  7794. }
  7795. };
  7796. /**
  7797. * Set the current timestamp. All packages with a timestamp smaller or equal
  7798. * than the given timestamp will be drawn.
  7799. * @param {Date | Number} timestamp
  7800. */
  7801. Graph.prototype.setTimestamp = function(timestamp) {
  7802. this._filterNodes(timestamp);
  7803. this._filterEdges(timestamp);
  7804. this._filterPackages(timestamp);
  7805. };
  7806. /**
  7807. * Get the range of all timestamps defined in the nodes, edges and packages
  7808. * @return {Object} A range object, containing parameters start and end.
  7809. */
  7810. Graph.prototype._getRange = function() {
  7811. // range is stored as number. at the end of the method, it is converted to
  7812. // Date when needed.
  7813. var range = {
  7814. "start": undefined,
  7815. "end": undefined
  7816. };
  7817. var tables = [this.nodesTable, this.edgesTable];
  7818. for (var t = 0, tMax = tables.length; t < tMax; t++) {
  7819. var table = tables[t];
  7820. if (table !== undefined) {
  7821. for (var i = 0, iMax = table.length; i < iMax; i++) {
  7822. var timestamp = table[i].timestamp;
  7823. if (timestamp) {
  7824. // to long
  7825. if (timestamp instanceof Date) {
  7826. timestamp = timestamp.getTime();
  7827. }
  7828. // calculate new range
  7829. range.start = range.start ? Math.min(timestamp, range.start) : timestamp;
  7830. range.end = range.end ? Math.max(timestamp, range.end) : timestamp;
  7831. }
  7832. }
  7833. }
  7834. }
  7835. // calculate the range for the packagesTable by hand. In case of packages
  7836. // without a progress provided, we need to calculate the end time by hand.
  7837. if (this.packagesTable) {
  7838. var packagesTable = this.packagesTable;
  7839. for (var row = 0, len = packagesTable.length; row < len; row ++) {
  7840. var pkg = packagesTable[row],
  7841. timestamp = pkg.timestamp,
  7842. progress = pkg.progress,
  7843. duration = pkg.duration || this.constants.packages.duration;
  7844. // convert to number
  7845. if (timestamp instanceof Date) {
  7846. timestamp = timestamp.getTime();
  7847. }
  7848. if (timestamp != undefined) {
  7849. var start = timestamp,
  7850. end = progress ? timestamp : (timestamp + duration * 1000);
  7851. range.start = range.start ? Math.min(start, range.start) : start;
  7852. range.end = range.end ? Math.max(end, range.end) : end;
  7853. }
  7854. }
  7855. }
  7856. // convert to the right type: number or date
  7857. var rangeFormat = {
  7858. "start": new Date(range.start),
  7859. "end": new Date(range.end)
  7860. };
  7861. return rangeFormat;
  7862. };
  7863. /**
  7864. * Start animation.
  7865. * Only applicable when packages with a timestamp are available
  7866. */
  7867. Graph.prototype.animationStart = function() {
  7868. if (this.slider) {
  7869. this.slider.play();
  7870. }
  7871. };
  7872. /**
  7873. * Start animation.
  7874. * Only applicable when packages with a timestamp are available
  7875. */
  7876. Graph.prototype.animationStop = function() {
  7877. if (this.slider) {
  7878. this.slider.stop();
  7879. }
  7880. };
  7881. /**
  7882. * Set framerate for the animation.
  7883. * Only applicable when packages with a timestamp are available
  7884. * @param {number} framerate The framerate in frames per second
  7885. */
  7886. Graph.prototype.setAnimationFramerate = function(framerate) {
  7887. if (this.slider) {
  7888. this.slider.setFramerate(framerate);
  7889. }
  7890. }
  7891. /**
  7892. * Set the duration of playing the whole package history
  7893. * Only applicable when packages with a timestamp are available
  7894. * @param {number} duration The duration in seconds
  7895. */
  7896. Graph.prototype.setAnimationDuration = function(duration) {
  7897. if (this.slider) {
  7898. this.slider.setDuration(duration);
  7899. }
  7900. };
  7901. /**
  7902. * Set the time acceleration for playing the history.
  7903. * Only applicable when packages with a timestamp are available
  7904. * @param {number} acceleration Acceleration, for example 10 means play
  7905. * ten times as fast as real time. A value
  7906. * of 1 will play the history in real time.
  7907. */
  7908. Graph.prototype.setAnimationAcceleration = function(acceleration) {
  7909. if (this.slider) {
  7910. this.slider.setAcceleration(acceleration);
  7911. }
  7912. };
  7913. /**
  7914. * Redraw the graph with the current data
  7915. * chart will be resized too.
  7916. */
  7917. Graph.prototype.redraw = function() {
  7918. this._setSize(this.width, this.height);
  7919. this._redraw();
  7920. };
  7921. /**
  7922. * Redraw the graph with the current data
  7923. */
  7924. Graph.prototype._redraw = function() {
  7925. var ctx = this.frame.canvas.getContext("2d");
  7926. // clear the canvas
  7927. var w = this.frame.canvas.width;
  7928. var h = this.frame.canvas.height;
  7929. ctx.clearRect(0, 0, w, h);
  7930. // set scaling and translation
  7931. ctx.save();
  7932. ctx.translate(this.translation.x, this.translation.y);
  7933. ctx.scale(this.scale, this.scale);
  7934. this._drawEdges(ctx);
  7935. this._drawNodes(ctx);
  7936. this._drawPackages(ctx);
  7937. this._drawSlider();
  7938. // restore original scaling and translation
  7939. ctx.restore();
  7940. };
  7941. /**
  7942. * Set the translation of the graph
  7943. * @param {Number} offsetX Horizontal offset
  7944. * @param {Number} offsetY Vertical offset
  7945. */
  7946. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  7947. if (this.translation === undefined) {
  7948. this.translation = {
  7949. "x": 0,
  7950. "y": 0
  7951. };
  7952. }
  7953. if (offsetX !== undefined) {
  7954. this.translation.x = offsetX;
  7955. }
  7956. if (offsetY !== undefined) {
  7957. this.translation.y = offsetY;
  7958. }
  7959. };
  7960. /**
  7961. * Get the translation of the graph
  7962. * @return {Object} translation An object with parameters x and y, both a number
  7963. */
  7964. Graph.prototype._getTranslation = function() {
  7965. return {
  7966. "x": this.translation.x,
  7967. "y": this.translation.y
  7968. };
  7969. };
  7970. /**
  7971. * Scale the graph
  7972. * @param {Number} scale Scaling factor 1.0 is unscaled
  7973. */
  7974. Graph.prototype._setScale = function(scale) {
  7975. this.scale = scale;
  7976. };
  7977. /**
  7978. * Get the current scale of the graph
  7979. * @return {Number} scale Scaling factor 1.0 is unscaled
  7980. */
  7981. Graph.prototype._getScale = function() {
  7982. return this.scale;
  7983. };
  7984. Graph.prototype._xToCanvas = function(x) {
  7985. return (x - this.translation.x) / this.scale;
  7986. };
  7987. Graph.prototype._canvasToX = function(x) {
  7988. return x * this.scale + this.translation.x;
  7989. };
  7990. Graph.prototype._yToCanvas = function(y) {
  7991. return (y - this.translation.y) / this.scale;
  7992. };
  7993. Graph.prototype._canvasToY = function(y) {
  7994. return y * this.scale + this.translation.y ;
  7995. };
  7996. /**
  7997. * Get a node by its id
  7998. * @param {number} id
  7999. * @return {Node} node, or null if not found
  8000. */
  8001. Graph.prototype._getNode = function(id) {
  8002. for (var i = 0; i < this.nodes.length; i++) {
  8003. if (this.nodes[i].id == id)
  8004. return this.nodes[i];
  8005. }
  8006. return null;
  8007. };
  8008. /**
  8009. * Redraw all nodes
  8010. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8011. * @param {CanvasRenderingContext2D} ctx
  8012. */
  8013. Graph.prototype._drawNodes = function(ctx) {
  8014. // first draw the unselected nodes
  8015. var nodes = this.nodes;
  8016. var selected = [];
  8017. for (var i = 0, iMax = nodes.length; i < iMax; i++) {
  8018. if (nodes[i].isSelected()) {
  8019. selected.push(i);
  8020. }
  8021. else {
  8022. nodes[i].draw(ctx);
  8023. }
  8024. }
  8025. // draw the selected nodes on top
  8026. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  8027. nodes[selected[s]].draw(ctx);
  8028. }
  8029. };
  8030. /**
  8031. * Redraw all edges
  8032. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8033. * @param {CanvasRenderingContext2D} ctx
  8034. */
  8035. Graph.prototype._drawEdges = function(ctx) {
  8036. var edges = this.edges;
  8037. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  8038. edges[i].draw(ctx);
  8039. }
  8040. };
  8041. /**
  8042. * Redraw all packages
  8043. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8044. * @param {CanvasRenderingContext2D} ctx
  8045. */
  8046. Graph.prototype._drawPackages = function(ctx) {
  8047. var packages = this.packages;
  8048. for (var i = 0, iMax = packages.length; i < iMax; i++) {
  8049. packages[i].draw(ctx);
  8050. }
  8051. };
  8052. /**
  8053. * Redraw the filter
  8054. */
  8055. Graph.prototype._drawSlider = function() {
  8056. var sliderNode;
  8057. if (this.hasTimestamps) {
  8058. sliderNode = this.frame.slider;
  8059. if (sliderNode === undefined) {
  8060. sliderNode = document.createElement( "div" );
  8061. sliderNode.style.position = "absolute";
  8062. sliderNode.style.bottom = "0px";
  8063. sliderNode.style.left = "0px";
  8064. sliderNode.style.right = "0px";
  8065. sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)";
  8066. this.frame.slider = sliderNode;
  8067. this.frame.slider.style.padding = "10px";
  8068. //this.frame.filter.style.backgroundColor = "#EFEFEF";
  8069. this.frame.appendChild(sliderNode);
  8070. var range = this._getRange();
  8071. this.slider = new Graph.Slider(sliderNode);
  8072. this.slider.setLoop(false);
  8073. this.slider.setRange(range.start, range.end);
  8074. // create an event handler
  8075. var me = this;
  8076. var onchange = function () {
  8077. var timestamp = me.slider.getValue();
  8078. me.setTimestamp(timestamp);
  8079. // TODO: do only a redraw when the graph is not still moving
  8080. me.redraw();
  8081. };
  8082. this.slider.setOnChangeCallback(onchange);
  8083. onchange(); // perform the first update by hand.
  8084. }
  8085. }
  8086. else {
  8087. sliderNode = this.frame.slider;
  8088. if (sliderNode !== undefined) {
  8089. this.frame.removeChild(sliderNode);
  8090. this.frame.slider = undefined;
  8091. this.slider = undefined;
  8092. }
  8093. }
  8094. };
  8095. /**
  8096. * Recalculate the best positions for all nodes
  8097. */
  8098. Graph.prototype._reposition = function() {
  8099. // TODO: implement function reposition
  8100. /*
  8101. var w = this.frame.canvas.clientWidth;
  8102. var h = this.frame.canvas.clientHeight;
  8103. for (var i = 0; i < this.nodes.length; i++) {
  8104. if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random();
  8105. if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random();
  8106. }
  8107. //*/
  8108. //*
  8109. // TODO
  8110. var radius = this.constants.edges.length * 2;
  8111. var cx = this.frame.canvas.clientWidth / 2;
  8112. var cy = this.frame.canvas.clientHeight / 2;
  8113. for (var i = 0; i < this.nodes.length; i++) {
  8114. var angle = 2*Math.PI * (i / this.nodes.length);
  8115. if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle);
  8116. if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle);
  8117. }
  8118. //*/
  8119. /*
  8120. // TODO
  8121. var radius = this.constants.edges.length * 2;
  8122. var w = this.frame.canvas.clientWidth,
  8123. h = this.frame.canvas.clientHeight;
  8124. var cx = this.frame.canvas.clientWidth / 2;
  8125. var cy = this.frame.canvas.clientHeight / 2;
  8126. var s = Math.sqrt(this.nodes.length);
  8127. for (var i = 0; i < this.nodes.length; i++) {
  8128. //var angle = 2*Math.PI * (i / this.nodes.length);
  8129. if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s);
  8130. if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s);
  8131. }
  8132. //*/
  8133. /*
  8134. var cx = this.frame.canvas.clientWidth / 2;
  8135. var cy = this.frame.canvas.clientHeight / 2;
  8136. for (var i = 0; i < this.nodes.length; i++) {
  8137. this.nodes[i].x = cx;
  8138. this.nodes[i].y = cy;
  8139. }
  8140. //*/
  8141. };
  8142. /**
  8143. * Find a stable position for all nodes
  8144. */
  8145. Graph.prototype._doStabilize = function() {
  8146. var start = new Date();
  8147. // find stable position
  8148. var count = 0;
  8149. var vmin = this.constants.minVelocity;
  8150. var stable = false;
  8151. while (!stable && count < this.constants.maxIterations) {
  8152. this._calculateForces();
  8153. this._discreteStepNodes();
  8154. stable = !this.isMoving(vmin);
  8155. count++;
  8156. }
  8157. var end = new Date();
  8158. //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
  8159. };
  8160. /**
  8161. * Calculate the external forces acting on the nodes
  8162. * Forces are caused by: edges, repulsing forces between nodes, gravity
  8163. */
  8164. Graph.prototype._calculateForces = function(nodeId) {
  8165. // create a local edge to the nodes and edges, that is faster
  8166. var nodes = this.nodes,
  8167. edges = this.edges;
  8168. // gravity, add a small constant force to pull the nodes towards the center of
  8169. // the graph
  8170. // Also, the forces are reset to zero in this loop by using _setForce instead
  8171. // of _addForce
  8172. var gravity = 0.01,
  8173. gx = this.frame.canvas.clientWidth / 2,
  8174. gy = this.frame.canvas.clientHeight / 2;
  8175. for (var n = 0; n < nodes.length; n++) {
  8176. var dx = gx - nodes[n].x,
  8177. dy = gy - nodes[n].y,
  8178. angle = Math.atan2(dy, dx),
  8179. fx = Math.cos(angle) * gravity,
  8180. fy = Math.sin(angle) * gravity;
  8181. this.nodes[n]._setForce(fx, fy);
  8182. }
  8183. // repulsing forces between nodes
  8184. var minimumDistance = this.constants.nodes.distance,
  8185. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  8186. for (var n = 0; n < nodes.length; n++) {
  8187. for (var n2 = n + 1; n2 < this.nodes.length; n2++) {
  8188. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  8189. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  8190. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  8191. // calculate normally distributed force
  8192. var dx = nodes[n2].x - nodes[n].x,
  8193. dy = nodes[n2].y - nodes[n].y,
  8194. distance = Math.sqrt(dx * dx + dy * dy),
  8195. angle = Math.atan2(dy, dx),
  8196. // TODO: correct factor for repulsing force
  8197. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  8198. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  8199. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  8200. fx = Math.cos(angle) * repulsingforce,
  8201. fy = Math.sin(angle) * repulsingforce;
  8202. this.nodes[n]._addForce(-fx, -fy);
  8203. this.nodes[n2]._addForce(fx, fy);
  8204. }
  8205. /* TODO: re-implement repulsion of edges
  8206. for (var l = 0; l < edges.length; l++) {
  8207. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  8208. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  8209. // calculate normally distributed force
  8210. dx = nodes[n].x - lx,
  8211. dy = nodes[n].y - ly,
  8212. distance = Math.sqrt(dx * dx + dy * dy),
  8213. angle = Math.atan2(dy, dx),
  8214. // TODO: correct factor for repulsing force
  8215. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  8216. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  8217. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  8218. fx = Math.cos(angle) * repulsingforce,
  8219. fy = Math.sin(angle) * repulsingforce;
  8220. nodes[n]._addForce(fx, fy);
  8221. edges[l].from._addForce(-fx/2,-fy/2);
  8222. edges[l].to._addForce(-fx/2,-fy/2);
  8223. }
  8224. */
  8225. }
  8226. // forces caused by the edges, modelled as springs
  8227. for (var l = 0, lMax = edges.length; l < lMax; l++) {
  8228. var edge = edges[l],
  8229. dx = (edge.to.x - edge.from.x),
  8230. dy = (edge.to.y - edge.from.y),
  8231. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin
  8232. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin
  8233. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2,
  8234. edgeLength = edge.length,
  8235. length = Math.sqrt(dx * dx + dy * dy),
  8236. angle = Math.atan2(dy, dx),
  8237. springforce = edge.stiffness * (edgeLength - length),
  8238. fx = Math.cos(angle) * springforce,
  8239. fy = Math.sin(angle) * springforce;
  8240. edge.from._addForce(-fx, -fy);
  8241. edge.to._addForce(fx, fy);
  8242. }
  8243. /* TODO: re-implement repulsion of edges
  8244. // repulsing forces between edges
  8245. var minimumDistance = this.constants.edges.distance,
  8246. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  8247. for (var l = 0; l < edges.length; l++) {
  8248. //Keep distance from other edge centers
  8249. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  8250. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  8251. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  8252. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  8253. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  8254. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  8255. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  8256. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  8257. // calculate normally distributed force
  8258. dx = l2x - lx,
  8259. dy = l2y - ly,
  8260. distance = Math.sqrt(dx * dx + dy * dy),
  8261. angle = Math.atan2(dy, dx),
  8262. // TODO: correct factor for repulsing force
  8263. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  8264. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  8265. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  8266. fx = Math.cos(angle) * repulsingforce,
  8267. fy = Math.sin(angle) * repulsingforce;
  8268. edges[l].from._addForce(-fx, -fy);
  8269. edges[l].to._addForce(-fx, -fy);
  8270. edges[l2].from._addForce(fx, fy);
  8271. edges[l2].to._addForce(fx, fy);
  8272. }
  8273. }
  8274. */
  8275. };
  8276. /**
  8277. * Check if any of the nodes is still moving
  8278. * @param {number} vmin the minimum velocity considered as "moving"
  8279. * @return {boolean} true if moving, false if non of the nodes is moving
  8280. */
  8281. Graph.prototype.isMoving = function(vmin) {
  8282. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  8283. var nodes = this.nodes;
  8284. for (var n = 0, nMax = nodes.length; n < nMax; n++) {
  8285. if (nodes[n].isMoving(vmin)) {
  8286. return true;
  8287. }
  8288. }
  8289. return false;
  8290. };
  8291. /**
  8292. * Perform one discrete step for all nodes
  8293. */
  8294. Graph.prototype._discreteStepNodes = function() {
  8295. var interval = this.refreshRate / 1000.0; // in seconds
  8296. var nodes = this.nodes;
  8297. for (var n = 0, nMax = nodes.length; n < nMax; n++) {
  8298. nodes[n].discreteStep(interval);
  8299. }
  8300. };
  8301. /**
  8302. * Perform one discrete step for all packages
  8303. */
  8304. Graph.prototype._discreteStepPackages = function() {
  8305. var interval = this.refreshRate / 1000.0; // in seconds
  8306. var packages = this.packages;
  8307. for (var n = 0, nMax = packages.length; n < nMax; n++) {
  8308. packages[n].discreteStep(interval);
  8309. }
  8310. };
  8311. /**
  8312. * Cleanup finished packages.
  8313. * also checks if there are moving packages
  8314. */
  8315. Graph.prototype._deleteFinishedPackages = function() {
  8316. var n = 0;
  8317. var hasMovingPackages = false;
  8318. while (n < this.packages.length) {
  8319. if (this.packages[n].isFinished()) {
  8320. this.packages.splice(n, 1);
  8321. n--;
  8322. }
  8323. else if (this.packages[n].isMoving()) {
  8324. hasMovingPackages = true;
  8325. }
  8326. n++;
  8327. }
  8328. this.hasMovingPackages = hasMovingPackages;
  8329. };
  8330. /**
  8331. * Start animating nodes, edges, and packages.
  8332. */
  8333. Graph.prototype.start = function() {
  8334. if (this.hasMovingNodes) {
  8335. this._calculateForces();
  8336. this._discreteStepNodes();
  8337. var vmin = this.constants.minVelocity;
  8338. this.hasMovingNodes = this.isMoving(vmin);
  8339. }
  8340. if (this.hasMovingPackages) {
  8341. this._discreteStepPackages();
  8342. this._deleteFinishedPackages();
  8343. }
  8344. if (this.hasMovingNodes || this.hasMovingEdges || this.hasMovingPackages) {
  8345. // start animation. only start timer if it is not already running
  8346. if (!this.timer) {
  8347. var graph = this;
  8348. this.timer = window.setTimeout(function () {
  8349. graph.timer = undefined;
  8350. graph.start();
  8351. graph._redraw();
  8352. }, this.refreshRate);
  8353. }
  8354. }
  8355. else {
  8356. this._redraw();
  8357. }
  8358. };
  8359. /**
  8360. * Stop animating nodes, edges, and packages.
  8361. */
  8362. Graph.prototype.stop = function () {
  8363. if (this.timer) {
  8364. window.clearInterval(this.timer);
  8365. this.timer = undefined;
  8366. }
  8367. };
  8368. /**--------------------------------------------------------------------------**/
  8369. /**
  8370. * Add and event listener. Works for all browsers
  8371. * @param {Element} element An html element
  8372. * @param {String} action The action, for example "click",
  8373. * without the prefix "on"
  8374. * @param {function} listener The callback function to be executed
  8375. * @param {boolean} useCapture
  8376. */
  8377. Graph.addEventListener = function (element, action, listener, useCapture) {
  8378. if (element.addEventListener) {
  8379. if (useCapture === undefined)
  8380. useCapture = false;
  8381. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  8382. action = "DOMMouseScroll"; // For Firefox
  8383. }
  8384. element.addEventListener(action, listener, useCapture);
  8385. } else {
  8386. element.attachEvent("on" + action, listener); // IE browsers
  8387. }
  8388. };
  8389. /**
  8390. * Remove an event listener from an element
  8391. * @param {Element} element An html dom element
  8392. * @param {string} action The name of the event, for example "mousedown"
  8393. * @param {function} listener The listener function
  8394. * @param {boolean} useCapture
  8395. */
  8396. Graph.removeEventListener = function(element, action, listener, useCapture) {
  8397. if (element.removeEventListener) {
  8398. // non-IE browsers
  8399. if (useCapture === undefined)
  8400. useCapture = false;
  8401. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  8402. action = "DOMMouseScroll"; // For Firefox
  8403. }
  8404. element.removeEventListener(action, listener, useCapture);
  8405. } else {
  8406. // IE browsers
  8407. element.detachEvent("on" + action, listener);
  8408. }
  8409. };
  8410. /**
  8411. * Stop event propagation
  8412. */
  8413. Graph.stopPropagation = function (event) {
  8414. if (!event)
  8415. event = window.event;
  8416. if (event.stopPropagation) {
  8417. event.stopPropagation(); // non-IE browsers
  8418. }
  8419. else {
  8420. event.cancelBubble = true; // IE browsers
  8421. }
  8422. };
  8423. /**
  8424. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  8425. */
  8426. Graph.preventDefault = function (event) {
  8427. if (!event)
  8428. event = window.event;
  8429. if (event.preventDefault) {
  8430. event.preventDefault(); // non-IE browsers
  8431. }
  8432. else {
  8433. event.returnValue = false; // IE browsers
  8434. }
  8435. };
  8436. /**
  8437. * Retrieve the absolute left value of a DOM element
  8438. * @param {Element} elem A dom element, for example a div
  8439. * @return {number} left The absolute left position of this element
  8440. * in the browser page.
  8441. */
  8442. Graph._getAbsoluteLeft = function(elem) {
  8443. var left = 0;
  8444. while( elem != null ) {
  8445. left += elem.offsetLeft;
  8446. left -= elem.scrollLeft;
  8447. elem = elem.offsetParent;
  8448. }
  8449. if (!document.body.scrollLeft && window.pageXOffset) {
  8450. // FF
  8451. left -= window.pageXOffset;
  8452. }
  8453. return left;
  8454. };
  8455. /**
  8456. * Retrieve the absolute top value of a DOM element
  8457. * @param {Element} elem A dom element, for example a div
  8458. * @return {number} top The absolute top position of this element
  8459. * in the browser page.
  8460. */
  8461. Graph._getAbsoluteTop = function(elem) {
  8462. var top = 0;
  8463. while( elem != null ) {
  8464. top += elem.offsetTop;
  8465. top -= elem.scrollTop;
  8466. elem = elem.offsetParent;
  8467. }
  8468. if (!document.body.scrollTop && window.pageYOffset) {
  8469. // FF
  8470. top -= window.pageYOffset;
  8471. }
  8472. return top;
  8473. };
  8474. /**--------------------------------------------------------------------------**/
  8475. /**
  8476. * @class Node
  8477. * A node. A node can be connected to other nodes via one or multiple edges.
  8478. * @param {object} properties An object containing properties for the node. All
  8479. * properties are optional, except for the id.
  8480. * {number} id Id of the node. Required
  8481. * {string} text Title for the node
  8482. * {number} x Horizontal position of the node
  8483. * {number} y Vertical position of the node
  8484. * {string} style Drawing style, available:
  8485. * "database", "circle", "rect",
  8486. * "image", "text", "dot", "star",
  8487. * "triangle", "triangleDown",
  8488. * "square"
  8489. * {string} image An image url
  8490. * {string} title An title text, can be HTML
  8491. * {anytype} group A group name or number
  8492. * @param {Graph.Images} imagelist A list with images. Only needed
  8493. * when the node has an image
  8494. * @param {Graph.Groups} grouplist A list with groups. Needed for
  8495. * retrieving group properties
  8496. * @param {Object} constants An object with default values for
  8497. * example for the color
  8498. */
  8499. Graph.Node = function (properties, imagelist, grouplist, constants) {
  8500. this.selected = false;
  8501. this.edges = []; // all edges connected to this node
  8502. this.group = constants.nodes.group;
  8503. this.fontSize = constants.nodes.fontSize;
  8504. this.fontFace = constants.nodes.fontFace;
  8505. this.fontColor = constants.nodes.fontColor;
  8506. this.borderColor = constants.nodes.borderColor;
  8507. this.backgroundColor = constants.nodes.backgroundColor;
  8508. this.highlightColor = constants.nodes.highlightColor;
  8509. // set defaults for the properties
  8510. this.id = undefined;
  8511. this.style = constants.nodes.style;
  8512. this.image = constants.nodes.image;
  8513. this.x = 0;
  8514. this.y = 0;
  8515. this.xFixed = false;
  8516. this.yFixed = false;
  8517. this.radius = constants.nodes.radius;
  8518. this.radiusFixed = false;
  8519. this.radiusMin = constants.nodes.radiusMin;
  8520. this.radiusMax = constants.nodes.radiusMax;
  8521. this.imagelist = imagelist;
  8522. this.grouplist = grouplist;
  8523. this.setProperties(properties, constants);
  8524. // mass, force, velocity
  8525. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  8526. this.fx = 0.0; // external force x
  8527. this.fy = 0.0; // external force y
  8528. this.vx = 0.0; // velocity x
  8529. this.vy = 0.0; // velocity y
  8530. this.minForce = constants.minForce;
  8531. this.damping = 0.9; // damping factor
  8532. };
  8533. /**
  8534. * Attach a edge to the node
  8535. * @param {Graph.Edge} edge
  8536. */
  8537. Graph.Node.prototype.attachEdge = function(edge) {
  8538. this.edges.push(edge);
  8539. this._updateMass();
  8540. };
  8541. /**
  8542. * Detach a edge from the node
  8543. * @param {Graph.Edge} edge
  8544. */
  8545. Graph.Node.prototype.detachEdge = function(edge) {
  8546. var index = this.edges.indexOf(edge);
  8547. if (index != -1) {
  8548. this.edges.splice(index, 1);
  8549. }
  8550. this._updateMass();
  8551. };
  8552. /**
  8553. * Update the nodes mass, which is determined by the number of edges connecting
  8554. * to it (more edges -> heavier node).
  8555. * @private
  8556. */
  8557. Graph.Node.prototype._updateMass = function() {
  8558. this.mass = 50 + 20 * this.edges.length; // kg
  8559. };
  8560. /**
  8561. * Set or overwrite properties for the node
  8562. * @param {Object} properties an object with properties
  8563. * @param {Object} constants and object with default, global properties
  8564. */
  8565. Graph.Node.prototype.setProperties = function(properties, constants) {
  8566. if (!properties) {
  8567. return;
  8568. }
  8569. // basic properties
  8570. if (properties.id != undefined) {this.id = properties.id;}
  8571. if (properties.text != undefined) {this.text = properties.text;}
  8572. if (properties.title != undefined) {this.title = properties.title;}
  8573. if (properties.group != undefined) {this.group = properties.group;}
  8574. if (properties.x != undefined) {this.x = properties.x;}
  8575. if (properties.y != undefined) {this.y = properties.y;}
  8576. if (properties.value != undefined) {this.value = properties.value;}
  8577. if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
  8578. if (this.id === undefined) {
  8579. throw "Node must have an id";
  8580. }
  8581. // copy group properties
  8582. if (this.group) {
  8583. var groupObj = this.grouplist.get(this.group);
  8584. for (var prop in groupObj) {
  8585. if (groupObj.hasOwnProperty(prop)) {
  8586. this[prop] = groupObj[prop];
  8587. }
  8588. }
  8589. }
  8590. // individual style properties
  8591. if (properties.style != undefined) {this.style = properties.style;}
  8592. if (properties.image != undefined) {this.image = properties.image;}
  8593. if (properties.radius != undefined) {this.radius = properties.radius;}
  8594. if (properties.borderColor != undefined) {this.borderColor = properties.borderColor;}
  8595. if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;}
  8596. if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;}
  8597. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  8598. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  8599. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  8600. if (this.image != undefined) {
  8601. if (this.imagelist) {
  8602. this.imageObj = this.imagelist.load(this.image);
  8603. }
  8604. else {
  8605. throw "No imagelist provided";
  8606. }
  8607. }
  8608. this.xFixed = this.xFixed || (properties.x != undefined);
  8609. this.yFixed = this.yFixed || (properties.y != undefined);
  8610. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  8611. if (this.style == 'image') {
  8612. this.radiusMin = constants.nodes.widthMin;
  8613. this.radiusMax = constants.nodes.widthMax;
  8614. }
  8615. // choose draw method depending on the style
  8616. var style = this.style;
  8617. switch (style) {
  8618. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8619. case 'rect': this.draw = this._drawRect; this.resize = this._resizeRect; break;
  8620. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8621. // TODO: add ellipse shape
  8622. // TODO: add diamond shape
  8623. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8624. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8625. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8626. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8627. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8628. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8629. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8630. default: this.draw = this._drawRect; this.resize = this._resizeRect; break;
  8631. }
  8632. // reset the size of the node, this can be changed
  8633. this._reset();
  8634. };
  8635. /**
  8636. * select this node
  8637. */
  8638. Graph.Node.prototype.select = function() {
  8639. this.selected = true;
  8640. this._reset();
  8641. };
  8642. /**
  8643. * unselect this node
  8644. */
  8645. Graph.Node.prototype.unselect = function() {
  8646. this.selected = false;
  8647. this._reset();
  8648. };
  8649. /**
  8650. * Reset the calculated size of the node, forces it to recalculate its size
  8651. */
  8652. Graph.Node.prototype._reset = function() {
  8653. this.width = undefined;
  8654. this.height = undefined;
  8655. };
  8656. /**
  8657. * get the title of this node.
  8658. * @return {string} title The title of the node, or undefined when no title
  8659. * has been set.
  8660. */
  8661. Graph.Node.prototype.getTitle = function() {
  8662. return this.title;
  8663. };
  8664. /**
  8665. * Calculate the distance to the border of the Node
  8666. * @param {CanvasRenderingContext2D} ctx
  8667. * @param {Number} angle Angle in radians
  8668. * @returns {number} distance Distance to the border in pixels
  8669. */
  8670. Graph.Node.prototype.distanceToBorder = function (ctx, angle) {
  8671. var borderWidth = 1;
  8672. if (!this.width) {
  8673. this.resize(ctx);
  8674. }
  8675. //noinspection FallthroughInSwitchStatementJS
  8676. switch (this.style) {
  8677. case 'circle':
  8678. case 'dot':
  8679. return this.radius + borderWidth;
  8680. // TODO: implement distanceToBorder for database
  8681. // TODO: implement distanceToBorder for triangle
  8682. // TODO: implement distanceToBorder for triangleDown
  8683. case 'rect':
  8684. case 'image':
  8685. case 'text':
  8686. default:
  8687. if (this.width) {
  8688. return Math.min(
  8689. Math.abs(this.width / 2 / Math.cos(angle)),
  8690. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8691. // TODO: reckon with border radius too in case of rect
  8692. }
  8693. else {
  8694. return 0;
  8695. }
  8696. }
  8697. // TODO: implement calculation of distance to border for all shapes
  8698. };
  8699. /**
  8700. * Set forces acting on the node
  8701. * @param {number} fx Force in horizontal direction
  8702. * @param {number} fy Force in vertical direction
  8703. */
  8704. Graph.Node.prototype._setForce = function(fx, fy) {
  8705. this.fx = fx;
  8706. this.fy = fy;
  8707. };
  8708. /**
  8709. * Add forces acting on the node
  8710. * @param {number} fx Force in horizontal direction
  8711. * @param {number} fy Force in vertical direction
  8712. */
  8713. Graph.Node.prototype._addForce = function(fx, fy) {
  8714. this.fx += fx;
  8715. this.fy += fy;
  8716. };
  8717. /**
  8718. * Perform one discrete step for the node
  8719. * @param {number} interval Time interval in seconds
  8720. */
  8721. Graph.Node.prototype.discreteStep = function(interval) {
  8722. if (!this.xFixed) {
  8723. var dx = -this.damping * this.vx; // damping force
  8724. var ax = (this.fx + dx) / this.mass; // acceleration
  8725. this.vx += ax / interval; // velocity
  8726. this.x += this.vx / interval; // position
  8727. }
  8728. if (!this.yFixed) {
  8729. var dy = -this.damping * this.vy; // damping force
  8730. var ay = (this.fy + dy) / this.mass; // acceleration
  8731. this.vy += ay / interval; // velocity
  8732. this.y += this.vy / interval; // position
  8733. }
  8734. };
  8735. /**
  8736. * Check if this node has a fixed x and y position
  8737. * @return {boolean} true if fixed, false if not
  8738. */
  8739. Graph.Node.prototype.isFixed = function() {
  8740. return (this.xFixed && this.yFixed);
  8741. };
  8742. /**
  8743. * Check if this node is moving
  8744. * @param {number} vmin the minimum velocity considered as "moving"
  8745. * @return {boolean} true if moving, false if it has no velocity
  8746. */
  8747. // TODO: replace this method with calculating the kinetic energy
  8748. Graph.Node.prototype.isMoving = function(vmin) {
  8749. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  8750. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  8751. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  8752. };
  8753. /**
  8754. * check if this node is selecte
  8755. * @return {boolean} selected True if node is selected, else false
  8756. */
  8757. Graph.Node.prototype.isSelected = function() {
  8758. return this.selected;
  8759. };
  8760. /**
  8761. * Retrieve the value of the node. Can be undefined
  8762. * @return {Number} value
  8763. */
  8764. Graph.Node.prototype.getValue = function() {
  8765. return this.value;
  8766. };
  8767. /**
  8768. * Calculate the distance from the nodes location to the given location (x,y)
  8769. * @param {Number} x
  8770. * @param {Number} y
  8771. * @return {Number} value
  8772. */
  8773. Graph.Node.prototype.getDistance = function(x, y) {
  8774. var dx = this.x - x,
  8775. dy = this.y - y;
  8776. return Math.sqrt(dx * dx + dy * dy);
  8777. };
  8778. /**
  8779. * Adjust the value range of the node. The node will adjust it's radius
  8780. * based on its value.
  8781. * @param {Number} min
  8782. * @param {Number} max
  8783. */
  8784. Graph.Node.prototype.setValueRange = function(min, max) {
  8785. if (!this.radiusFixed && this.value !== undefined) {
  8786. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8787. this.radius = (this.value - min) * scale + this.radiusMin;
  8788. }
  8789. };
  8790. /**
  8791. * Draw this node in the given canvas
  8792. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8793. * @param {CanvasRenderingContext2D} ctx
  8794. */
  8795. Graph.Node.prototype.draw = function(ctx) {
  8796. throw "Draw method not initialized for node";
  8797. };
  8798. /**
  8799. * Recalculate the size of this node in the given canvas
  8800. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8801. * @param {CanvasRenderingContext2D} ctx
  8802. */
  8803. Graph.Node.prototype.resize = function(ctx) {
  8804. throw "Resize method not initialized for node";
  8805. };
  8806. /**
  8807. * Check if this object is overlapping with the provided object
  8808. * @param {Object} obj an object with parameters left, top, right, bottom
  8809. * @return {boolean} True if location is located on node
  8810. */
  8811. Graph.Node.prototype.isOverlappingWith = function(obj) {
  8812. return (this.left < obj.right &&
  8813. this.left + this.width > obj.left &&
  8814. this.top < obj.bottom &&
  8815. this.top + this.height > obj.top);
  8816. };
  8817. Graph.Node.prototype._resizeImage = function (ctx) {
  8818. // TODO: pre calculate the image size
  8819. if (!this.width) { // undefined or 0
  8820. var width, height;
  8821. if (this.value) {
  8822. var scale = this.imageObj.height / this.imageObj.width;
  8823. width = this.radius || this.imageObj.width;
  8824. height = this.radius * scale || this.imageObj.height;
  8825. }
  8826. else {
  8827. width = this.imageObj.width;
  8828. height = this.imageObj.height;
  8829. }
  8830. this.width = width;
  8831. this.height = height;
  8832. }
  8833. };
  8834. Graph.Node.prototype._drawImage = function (ctx) {
  8835. this._resizeImage(ctx);
  8836. this.left = this.x - this.width / 2;
  8837. this.top = this.y - this.height / 2;
  8838. var yText;
  8839. if (this.imageObj) {
  8840. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8841. yText = this.y + this.height / 2;
  8842. }
  8843. else {
  8844. // image still loading... just draw the text for now
  8845. yText = this.y;
  8846. }
  8847. this._text(ctx, this.text, this.x, yText, undefined, "top");
  8848. };
  8849. Graph.Node.prototype._resizeRect = function (ctx) {
  8850. if (!this.width) {
  8851. var margin = 5;
  8852. var textSize = this.getTextSize(ctx);
  8853. this.width = textSize.width + 2 * margin;
  8854. this.height = textSize.height + 2 * margin;
  8855. }
  8856. };
  8857. Graph.Node.prototype._drawRect = function (ctx) {
  8858. this._resizeRect(ctx);
  8859. this.left = this.x - this.width / 2;
  8860. this.top = this.y - this.height / 2;
  8861. ctx.strokeStyle = this.borderColor;
  8862. ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
  8863. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8864. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8865. ctx.fill();
  8866. ctx.stroke();
  8867. this._text(ctx, this.text, this.x, this.y);
  8868. };
  8869. Graph.Node.prototype._resizeDatabase = function (ctx) {
  8870. if (!this.width) {
  8871. var margin = 5;
  8872. var textSize = this.getTextSize(ctx);
  8873. var size = textSize.width + 2 * margin;
  8874. this.width = size;
  8875. this.height = size;
  8876. }
  8877. };
  8878. Graph.Node.prototype._drawDatabase = function (ctx) {
  8879. this._resizeDatabase(ctx);
  8880. this.left = this.x - this.width / 2;
  8881. this.top = this.y - this.height / 2;
  8882. ctx.strokeStyle = this.borderColor;
  8883. ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
  8884. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8885. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8886. ctx.fill();
  8887. ctx.stroke();
  8888. this._text(ctx, this.text, this.x, this.y);
  8889. };
  8890. Graph.Node.prototype._resizeCircle = function (ctx) {
  8891. if (!this.width) {
  8892. var margin = 5;
  8893. var textSize = this.getTextSize(ctx);
  8894. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8895. this.radius = diameter / 2;
  8896. this.width = diameter;
  8897. this.height = diameter;
  8898. }
  8899. };
  8900. Graph.Node.prototype._drawCircle = function (ctx) {
  8901. this._resizeCircle(ctx);
  8902. this.left = this.x - this.width / 2;
  8903. this.top = this.y - this.height / 2;
  8904. ctx.strokeStyle = this.borderColor;
  8905. ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
  8906. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8907. ctx.circle(this.x, this.y, this.radius);
  8908. ctx.fill();
  8909. ctx.stroke();
  8910. this._text(ctx, this.text, this.x, this.y);
  8911. };
  8912. Graph.Node.prototype._drawDot = function (ctx) {
  8913. this._drawShape(ctx, 'circle');
  8914. };
  8915. Graph.Node.prototype._drawTriangle = function (ctx) {
  8916. this._drawShape(ctx, 'triangle');
  8917. };
  8918. Graph.Node.prototype._drawTriangleDown = function (ctx) {
  8919. this._drawShape(ctx, 'triangleDown');
  8920. };
  8921. Graph.Node.prototype._drawSquare = function (ctx) {
  8922. this._drawShape(ctx, 'square');
  8923. };
  8924. Graph.Node.prototype._drawStar = function (ctx) {
  8925. this._drawShape(ctx, 'star');
  8926. };
  8927. Graph.Node.prototype._resizeShape = function (ctx) {
  8928. if (!this.width) {
  8929. var size = 2 * this.radius;
  8930. this.width = size;
  8931. this.height = size;
  8932. }
  8933. };
  8934. Graph.Node.prototype._drawShape = function (ctx, shape) {
  8935. this._resizeShape(ctx);
  8936. this.left = this.x - this.width / 2;
  8937. this.top = this.y - this.height / 2;
  8938. ctx.strokeStyle = this.borderColor;
  8939. ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor;
  8940. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  8941. ctx[shape](this.x, this.y, this.radius);
  8942. ctx.fill();
  8943. ctx.stroke();
  8944. if (this.text) {
  8945. this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top');
  8946. }
  8947. };
  8948. Graph.Node.prototype._resizeText = function (ctx) {
  8949. if (!this.width) {
  8950. var margin = 5;
  8951. var textSize = this.getTextSize(ctx);
  8952. this.width = textSize.width + 2 * margin;
  8953. this.height = textSize.height + 2 * margin;
  8954. }
  8955. };
  8956. Graph.Node.prototype._drawText = function (ctx) {
  8957. this._resizeText(ctx);
  8958. this.left = this.x - this.width / 2;
  8959. this.top = this.y - this.height / 2;
  8960. this._text(ctx, this.text, this.x, this.y);
  8961. };
  8962. Graph.Node.prototype._text = function (ctx, text, x, y, align, baseline) {
  8963. if (text) {
  8964. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8965. ctx.fillStyle = this.fontColor || "black";
  8966. ctx.textAlign = align || "center";
  8967. ctx.textBaseline = baseline || "middle";
  8968. var lines = text.split('\n'),
  8969. lineCount = lines.length,
  8970. fontSize = (this.fontSize + 4),
  8971. yLine = y + (1 - lineCount) / 2 * fontSize;
  8972. for (var i = 0; i < lineCount; i++) {
  8973. ctx.fillText(lines[i], x, yLine);
  8974. yLine += fontSize;
  8975. }
  8976. }
  8977. };
  8978. Graph.Node.prototype.getTextSize = function(ctx) {
  8979. if (this.text != undefined) {
  8980. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8981. var lines = this.text.split('\n'),
  8982. height = (this.fontSize + 4) * lines.length,
  8983. width = 0;
  8984. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8985. width = Math.max(width, ctx.measureText(lines[i]).width);
  8986. }
  8987. return {"width": width, "height": height};
  8988. }
  8989. else {
  8990. return {"width": 0, "height": 0};
  8991. }
  8992. };
  8993. /**--------------------------------------------------------------------------**/
  8994. /**
  8995. * @class Edge
  8996. *
  8997. * A edge connects two nodes
  8998. * @param {Object} properties Object with properties. Must contain
  8999. * At least properties from and to.
  9000. * Available properties: from (number),
  9001. * to (number), color (string),
  9002. * width (number), style (string),
  9003. * length (number), title (string)
  9004. * @param {Graph} graph A graph object, used to find and edge to
  9005. * nodes.
  9006. * @param {Object} constants An object with default values for
  9007. * example for the color
  9008. */
  9009. Graph.Edge = function (properties, graph, constants) {
  9010. if (!graph) {
  9011. throw "No graph provided";
  9012. }
  9013. this.graph = graph;
  9014. // initialize constants
  9015. this.widthMin = constants.edges.widthMin;
  9016. this.widthMax = constants.edges.widthMax;
  9017. // initialize variables
  9018. this.id = undefined;
  9019. this.style = constants.edges.style;
  9020. this.title = undefined;
  9021. this.width = constants.edges.width;
  9022. this.value = undefined;
  9023. this.length = constants.edges.length;
  9024. // Added to support dashed lines
  9025. // David Jordan
  9026. // 2012-08-08
  9027. this.dashlength = constants.edges.dashlength;
  9028. this.dashgap = constants.edges.dashgap;
  9029. this.altdashlength = constants.edges.altdashlength;
  9030. this.stiffness = undefined; // depends on the length of the edge
  9031. this.color = constants.edges.color;
  9032. this.timestamp = undefined;
  9033. this.widthFixed = false;
  9034. this.lengthFixed = false;
  9035. this.setProperties(properties, constants);
  9036. };
  9037. /**
  9038. * Set or overwrite properties for the edge
  9039. * @param {Object} properties an object with properties
  9040. * @param {Object} constants and object with default, global properties
  9041. */
  9042. Graph.Edge.prototype.setProperties = function(properties, constants) {
  9043. if (!properties) {
  9044. return;
  9045. }
  9046. if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);}
  9047. if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);}
  9048. if (properties.id != undefined) {this.id = properties.id;}
  9049. if (properties.style != undefined) {this.style = properties.style;}
  9050. if (properties.text != undefined) {this.text = properties.text;}
  9051. if (this.text) {
  9052. this.fontSize = constants.edges.fontSize;
  9053. this.fontFace = constants.edges.fontFace;
  9054. this.fontColor = constants.edges.fontColor;
  9055. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  9056. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  9057. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  9058. }
  9059. if (properties.title != undefined) {this.title = properties.title;}
  9060. if (properties.width != undefined) {this.width = properties.width;}
  9061. if (properties.value != undefined) {this.value = properties.value;}
  9062. if (properties.length != undefined) {this.length = properties.length;}
  9063. // Added to support dashed lines
  9064. // David Jordan
  9065. // 2012-08-08
  9066. if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
  9067. if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
  9068. if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
  9069. if (properties.color != undefined) {this.color = properties.color;}
  9070. if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
  9071. if (!this.from) {
  9072. throw "Node with id " + properties.from + " not found";
  9073. }
  9074. if (!this.to) {
  9075. throw "Node with id " + properties.to + " not found";
  9076. }
  9077. this.widthFixed = this.widthFixed || (properties.width != undefined);
  9078. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  9079. this.stiffness = 1 / this.length;
  9080. // initialize animation
  9081. if (this.style === 'arrow') {
  9082. this.arrows = [0.5];
  9083. this.animation = false;
  9084. }
  9085. else if (this.style === 'arrow-end') {
  9086. this.animation = false;
  9087. }
  9088. else if (this.style === 'moving-arrows') {
  9089. this.arrows = [];
  9090. var arrowCount = 3; // TODO: make customizable
  9091. for (var a = 0; a < arrowCount; a++) {
  9092. this.arrows.push(a / arrowCount);
  9093. }
  9094. this.animation = true;
  9095. }
  9096. else if (this.style === 'moving-dot') {
  9097. this.dot = 0.0;
  9098. this.animation = true;
  9099. }
  9100. else {
  9101. this.animation = false;
  9102. }
  9103. // set draw method based on style
  9104. switch (this.style) {
  9105. case 'line': this.draw = this._drawLine; break;
  9106. case 'arrow': this.draw = this._drawArrow; break;
  9107. case 'arrow-end': this.draw = this._drawArrowEnd; break;
  9108. case 'moving-arrows': this.draw = this._drawMovingArrows; break;
  9109. case 'moving-dot': this.draw = this._drawMovingDot; break;
  9110. case 'dash-line': this.draw = this._drawDashLine; break;
  9111. default: this.draw = this._drawLine; break;
  9112. }
  9113. };
  9114. /**
  9115. * Check if a node has an animating contents. If so, the graph needs to be
  9116. * redrawn regularly
  9117. * @return {boolean} true if this edge needs animation, else false
  9118. */
  9119. Graph.Edge.prototype.isMoving = function() {
  9120. // TODO: be able to set the interval somehow
  9121. return this.animation;
  9122. };
  9123. /**
  9124. * get the title of this edge.
  9125. * @return {string} title The title of the edge, or undefined when no title
  9126. * has been set.
  9127. */
  9128. Graph.Edge.prototype.getTitle = function() {
  9129. return this.title;
  9130. };
  9131. /**
  9132. * Retrieve the value of the edge. Can be undefined
  9133. * @return {Number} value
  9134. */
  9135. Graph.Edge.prototype.getValue = function() {
  9136. return this.value;
  9137. }
  9138. /**
  9139. * Adjust the value range of the edge. The edge will adjust it's width
  9140. * based on its value.
  9141. * @param {Number} min
  9142. * @param {Number} max
  9143. */
  9144. Graph.Edge.prototype.setValueRange = function(min, max) {
  9145. if (!this.widthFixed && this.value !== undefined) {
  9146. var factor = (this.widthMax - this.widthMin) / (max - min);
  9147. this.width = (this.value - min) * factor + this.widthMin;
  9148. }
  9149. };
  9150. /**
  9151. * Check if the length is fixed.
  9152. * @return {boolean} lengthFixed True if the length is fixed, else false
  9153. */
  9154. Graph.Edge.prototype.isLengthFixed = function() {
  9155. return this.lengthFixed;
  9156. };
  9157. /**
  9158. * Retrieve the length of the edge. Can be undefined
  9159. * @return {Number} length
  9160. */
  9161. Graph.Edge.prototype.getLength = function() {
  9162. return this.length;
  9163. };
  9164. /**
  9165. * Adjust the length of the edge. This can only be done when the length
  9166. * is not fixed (which is the case when the edge is created with a length property)
  9167. * @param {Number} length
  9168. */
  9169. Graph.Edge.prototype.setLength = function(length) {
  9170. if (!this.lengthFixed) {
  9171. this.length = length;
  9172. }
  9173. };
  9174. /**
  9175. * Retrieve the length of the edges dashes. Can be undefined
  9176. * @author David Jordan
  9177. * @date 2012-08-08
  9178. * @return {Number} dashlength
  9179. */
  9180. Graph.Edge.prototype.getDashLength = function() {
  9181. return this.dashlength;
  9182. };
  9183. /**
  9184. * Adjust the length of the edges dashes.
  9185. * @author David Jordan
  9186. * @date 2012-08-08
  9187. * @param {Number} dashlength
  9188. */
  9189. Graph.Edge.prototype.setDashLength = function(dashlength) {
  9190. this.dashlength = dashlength;
  9191. };
  9192. /**
  9193. * Retrieve the length of the edges dashes gaps. Can be undefined
  9194. * @author David Jordan
  9195. * @date 2012-08-08
  9196. * @return {Number} dashgap
  9197. */
  9198. Graph.Edge.prototype.getDashGap = function() {
  9199. return this.dashgap;
  9200. };
  9201. /**
  9202. * Adjust the length of the edges dashes gaps.
  9203. * @author David Jordan
  9204. * @date 2012-08-08
  9205. * @param {Number} dashgap
  9206. */
  9207. Graph.Edge.prototype.setDashGap = function(dashgap) {
  9208. this.dashgap = dashgap;
  9209. };
  9210. /**
  9211. * Retrieve the length of the edges alternate dashes. Can be undefined
  9212. * @author David Jordan
  9213. * @date 2012-08-08
  9214. * @return {Number} altdashlength
  9215. */
  9216. Graph.Edge.prototype.getAltDashLength = function() {
  9217. return this.altdashlength;
  9218. };
  9219. /**
  9220. * Adjust the length of the edges alternate dashes.
  9221. * @author David Jordan
  9222. * @date 2012-08-08
  9223. * @param {Number} altdashlength
  9224. */
  9225. Graph.Edge.prototype.setAltDashLength = function(altdashlength) {
  9226. this.altdashlength = altdashlength;
  9227. };
  9228. /**
  9229. * Redraw a edge
  9230. * Draw this edge in the given canvas
  9231. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9232. * @param {CanvasRenderingContext2D} ctx
  9233. */
  9234. Graph.Edge.prototype.draw = function(ctx) {
  9235. throw "Method draw not initialized in edge";
  9236. };
  9237. /**
  9238. * Check if this object is overlapping with the provided object
  9239. * @param {Object} obj an object with parameters left, top
  9240. * @return {boolean} True if location is located on the edge
  9241. */
  9242. Graph.Edge.prototype.isOverlappingWith = function(obj) {
  9243. var distMax = 10;
  9244. var xFrom = this.from.x;
  9245. var yFrom = this.from.y;
  9246. var xTo = this.to.x;
  9247. var yTo = this.to.y;
  9248. var xObj = obj.left;
  9249. var yObj = obj.top;
  9250. var dist = Graph._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  9251. return (dist < distMax);
  9252. };
  9253. /**
  9254. * Calculate the distance between a point (x3,y3) and a line segment from
  9255. * (x1,y1) to (x2,y2).
  9256. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9257. * @param {number} x1
  9258. * @param {number} y1
  9259. * @param {number} x2
  9260. * @param {number} y2
  9261. * @param {number} x3
  9262. * @param {number} y3
  9263. */
  9264. Graph._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9265. var px = x2-x1,
  9266. py = y2-y1,
  9267. something = px*px + py*py,
  9268. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9269. if (u > 1) {
  9270. u = 1;
  9271. }
  9272. else if (u < 0) {
  9273. u = 0;
  9274. }
  9275. var x = x1 + u * px,
  9276. y = y1 + u * py,
  9277. dx = x - x3,
  9278. dy = y - y3;
  9279. //# Note: If the actual distance does not matter,
  9280. //# if you only want to compare what this function
  9281. //# returns to other results of this function, you
  9282. //# can just return the squared distance instead
  9283. //# (i.e. remove the sqrt) to gain a little performance
  9284. return Math.sqrt(dx*dx + dy*dy);
  9285. };
  9286. /**
  9287. * Redraw a edge as a line
  9288. * Draw this edge in the given canvas
  9289. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9290. * @param {CanvasRenderingContext2D} ctx
  9291. */
  9292. Graph.Edge.prototype._drawLine = function(ctx) {
  9293. // set style
  9294. ctx.strokeStyle = this.color;
  9295. ctx.lineWidth = this._getLineWidth();
  9296. var point;
  9297. if (this.from != this.to) {
  9298. // draw line
  9299. this._line(ctx);
  9300. // draw text
  9301. if (this.text) {
  9302. point = this._pointOnLine(0.5);
  9303. this._text(ctx, this.text, point.x, point.y);
  9304. }
  9305. }
  9306. else {
  9307. var radius = this.length / 2 / Math.PI;
  9308. var x, y;
  9309. var node = this.from;
  9310. if (!node.width) {
  9311. node.resize(ctx);
  9312. }
  9313. if (node.width > node.height) {
  9314. x = node.x + node.width / 2;
  9315. y = node.y - radius;
  9316. }
  9317. else {
  9318. x = node.x + radius;
  9319. y = node.y - node.height / 2;
  9320. }
  9321. this._circle(ctx, x, y, radius);
  9322. point = this._pointOnCircle(x, y, radius, 0.5);
  9323. this._text(ctx, this.text, point.x, point.y);
  9324. }
  9325. };
  9326. /**
  9327. * Get the line width of the edge. Depends on width and whether one of the
  9328. * connected nodes is selected.
  9329. * @return {Number} width
  9330. * @private
  9331. */
  9332. Graph.Edge.prototype._getLineWidth = function() {
  9333. if (this.from.selected || this.to.selected) {
  9334. return Math.min(this.width * 2, this.widthMax);
  9335. }
  9336. else {
  9337. return this.width;
  9338. }
  9339. };
  9340. /**
  9341. * Draw a line between two nodes
  9342. * @param {CanvasRenderingContext2D} ctx
  9343. * @private
  9344. */
  9345. Graph.Edge.prototype._line = function (ctx) {
  9346. // draw a straight line
  9347. ctx.beginPath();
  9348. ctx.moveTo(this.from.x, this.from.y);
  9349. ctx.lineTo(this.to.x, this.to.y);
  9350. ctx.stroke();
  9351. };
  9352. /**
  9353. * Draw a line from a node to itself, a circle
  9354. * @param {CanvasRenderingContext2D} ctx
  9355. * @param {Number} x
  9356. * @param {Number} y
  9357. * @param {Number} radius
  9358. * @private
  9359. */
  9360. Graph.Edge.prototype._circle = function (ctx, x, y, radius) {
  9361. // draw a circle
  9362. ctx.beginPath();
  9363. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9364. ctx.stroke();
  9365. };
  9366. /**
  9367. * Draw text with white background and with the middle at (x, y)
  9368. * @param {CanvasRenderingContext2D} ctx
  9369. * @param {String} text
  9370. * @param {Number} x
  9371. * @param {Number} y
  9372. */
  9373. Graph.Edge.prototype._text = function (ctx, text, x, y) {
  9374. if (text) {
  9375. // TODO: cache the calculated size
  9376. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  9377. this.fontSize + "px " + this.fontFace;
  9378. ctx.fillStyle = 'white';
  9379. var width = ctx.measureText(this.text).width;
  9380. var height = this.fontSize;
  9381. var left = x - width / 2;
  9382. var top = y - height / 2;
  9383. ctx.fillRect(left, top, width, height);
  9384. // draw text
  9385. ctx.fillStyle = this.fontColor || "black";
  9386. ctx.textAlign = "left";
  9387. ctx.textBaseline = "top";
  9388. ctx.fillText(this.text, left, top);
  9389. }
  9390. };
  9391. /**
  9392. * Sets up the dashedLine functionality for drawing
  9393. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  9394. * @author David Jordan
  9395. * @date 2012-08-08
  9396. */
  9397. var CP = (typeof window !== 'undefined') &&
  9398. window.CanvasRenderingContext2D &&
  9399. CanvasRenderingContext2D.prototype;
  9400. if (CP && CP.lineTo){
  9401. CP.dashedLine = function(x,y,x2,y2,dashArray){
  9402. if (!dashArray) dashArray=[10,5];
  9403. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  9404. var dashCount = dashArray.length;
  9405. this.moveTo(x, y);
  9406. var dx = (x2-x), dy = (y2-y);
  9407. var slope = dy/dx;
  9408. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  9409. var dashIndex=0, draw=true;
  9410. while (distRemaining>=0.1){
  9411. var dashLength = dashArray[dashIndex++%dashCount];
  9412. if (dashLength > distRemaining) dashLength = distRemaining;
  9413. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  9414. if (dx<0) xStep = -xStep;
  9415. x += xStep
  9416. y += slope*xStep;
  9417. this[draw ? 'lineTo' : 'moveTo'](x,y);
  9418. distRemaining -= dashLength;
  9419. draw = !draw;
  9420. }
  9421. }
  9422. }
  9423. /**
  9424. * Redraw a edge as a dashed line
  9425. * Draw this edge in the given canvas
  9426. * @author David Jordan
  9427. * @date 2012-08-08
  9428. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9429. * @param {CanvasRenderingContext2D} ctx
  9430. */
  9431. Graph.Edge.prototype._drawDashLine = function(ctx) {
  9432. // set style
  9433. ctx.strokeStyle = this.color;
  9434. ctx.lineWidth = this._getLineWidth();
  9435. // draw dashed line
  9436. ctx.beginPath();
  9437. ctx.lineCap = 'round';
  9438. if (this.altdashlength != undefined) //If an alt dash value has been set add to the array this value
  9439. {
  9440. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]);
  9441. }
  9442. else if (this.dashlength != undefined && this.dashgap != undefined) //If a dash and gap value has been set add to the array this value
  9443. {
  9444. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]);
  9445. }
  9446. else //If all else fails draw a line
  9447. {
  9448. ctx.moveTo(this.from.x, this.from.y);
  9449. ctx.lineTo(this.to.x, this.to.y);
  9450. }
  9451. ctx.stroke();
  9452. // draw text
  9453. if (this.text) {
  9454. var point = this._pointOnLine(0.5);
  9455. this._text(ctx, this.text, point.x, point.y);
  9456. }
  9457. };
  9458. /**
  9459. * Get a point on a line
  9460. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9461. * @return {Object} point
  9462. * @private
  9463. */
  9464. Graph.Edge.prototype._pointOnLine = function (percentage) {
  9465. return {
  9466. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9467. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9468. }
  9469. };
  9470. /**
  9471. * Get a point on a circle
  9472. * @param {Number} x
  9473. * @param {Number} y
  9474. * @param {Number} radius
  9475. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9476. * @return {Object} point
  9477. * @private
  9478. */
  9479. Graph.Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9480. var angle = (percentage - 3/8) * 2 * Math.PI;
  9481. return {
  9482. x: x + radius * Math.cos(angle),
  9483. y: y - radius * Math.sin(angle)
  9484. }
  9485. };
  9486. /**
  9487. * Redraw a edge as a line with a moving arrow
  9488. * Draw this edge in the given canvas
  9489. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9490. * @param {CanvasRenderingContext2D} ctx
  9491. */
  9492. Graph.Edge.prototype._drawMovingArrows = function(ctx) {
  9493. this._drawArrow(ctx);
  9494. for (var a in this.arrows) {
  9495. if (this.arrows.hasOwnProperty(a)) {
  9496. this.arrows[a] += 0.02; // TODO determine speed from interval
  9497. if (this.arrows[a] > 1.0) this.arrows[a] = 0.0;
  9498. }
  9499. }
  9500. };
  9501. /**
  9502. * Redraw a edge as a line with a moving dot
  9503. * Draw this edge in the given canvas
  9504. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9505. * @param {CanvasRenderingContext2D} ctx
  9506. */
  9507. Graph.Edge.prototype._drawMovingDot = function(ctx) {
  9508. // set style
  9509. ctx.strokeStyle = this.color;
  9510. ctx.fillStyle = this.color;
  9511. ctx.lineWidth = this._getLineWidth();
  9512. // draw line
  9513. var point;
  9514. if (this.from != this.to) {
  9515. this._line(ctx);
  9516. // draw dot
  9517. var radius = 4 + this.width * 2;
  9518. point = this._pointOnLine(this.dot);
  9519. ctx.circle(point.x, point.y, radius);
  9520. ctx.fill();
  9521. // move the dot to the next position
  9522. this.dot += 0.05; // TODO determine speed from interval
  9523. if (this.dot > 1.0) this.dot = 0.0;
  9524. // draw text
  9525. if (this.text) {
  9526. point = this._pointOnLine(0.5);
  9527. this._text(ctx, this.text, point.x, point.y);
  9528. }
  9529. }
  9530. else {
  9531. // TODO: moving dot for a circular edge
  9532. }
  9533. };
  9534. /**
  9535. * Redraw a edge as a line with an arrow
  9536. * Draw this edge in the given canvas
  9537. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9538. * @param {CanvasRenderingContext2D} ctx
  9539. */
  9540. Graph.Edge.prototype._drawArrow = function(ctx) {
  9541. var point;
  9542. // set style
  9543. ctx.strokeStyle = this.color;
  9544. ctx.fillStyle = this.color;
  9545. ctx.lineWidth = this._getLineWidth();
  9546. if (this.from != this.to) {
  9547. // draw line
  9548. this._line(ctx);
  9549. // draw all arrows
  9550. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9551. var length = 10 + 5 * this.width; // TODO: make customizable?
  9552. for (var a in this.arrows) {
  9553. if (this.arrows.hasOwnProperty(a)) {
  9554. point = this._pointOnLine(this.arrows[a]);
  9555. ctx.arrow(point.x, point.y, angle, length);
  9556. ctx.fill();
  9557. ctx.stroke();
  9558. }
  9559. }
  9560. // draw text
  9561. if (this.text) {
  9562. point = this._pointOnLine(0.5);
  9563. this._text(ctx, this.text, point.x, point.y);
  9564. }
  9565. }
  9566. else {
  9567. // draw circle
  9568. var radius = this.length / 2 / Math.PI;
  9569. var x, y;
  9570. var node = this.from;
  9571. if (!node.width) {
  9572. node.resize(ctx);
  9573. }
  9574. if (node.width > node.height) {
  9575. x = node.x + node.width / 2;
  9576. y = node.y - radius;
  9577. }
  9578. else {
  9579. x = node.x + radius;
  9580. y = node.y - node.height / 2;
  9581. }
  9582. this._circle(ctx, x, y, radius);
  9583. // draw all arrows
  9584. var angle = 0.2 * Math.PI;
  9585. var length = 10 + 5 * this.width; // TODO: make customizable?
  9586. for (var a in this.arrows) {
  9587. if (this.arrows.hasOwnProperty(a)) {
  9588. point = this._pointOnCircle(x, y, radius, this.arrows[a]);
  9589. ctx.arrow(point.x, point.y, angle, length);
  9590. ctx.fill();
  9591. ctx.stroke();
  9592. }
  9593. }
  9594. // draw text
  9595. if (this.text) {
  9596. point = this._pointOnCircle(x, y, radius, 0.5);
  9597. this._text(ctx, this.text, point.x, point.y);
  9598. }
  9599. }
  9600. };
  9601. /**
  9602. * Redraw a edge as a line with an arrow
  9603. * Draw this edge in the given canvas
  9604. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9605. * @param {CanvasRenderingContext2D} ctx
  9606. */
  9607. Graph.Edge.prototype._drawArrowEnd = function(ctx) {
  9608. // set style
  9609. ctx.strokeStyle = this.color;
  9610. ctx.fillStyle = this.color;
  9611. ctx.lineWidth = this._getLineWidth();
  9612. // draw line
  9613. var angle, length;
  9614. if (this.from != this.to) {
  9615. // calculate length and angle of the line
  9616. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9617. var dx = (this.to.x - this.from.x);
  9618. var dy = (this.to.y - this.from.y);
  9619. var lEdge = Math.sqrt(dx * dx + dy * dy);
  9620. var lFrom = this.to.distanceToBorder(ctx, angle + Math.PI);
  9621. var pFrom = (lEdge - lFrom) / lEdge;
  9622. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  9623. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  9624. var lTo = this.to.distanceToBorder(ctx, angle);
  9625. var pTo = (lEdge - lTo) / lEdge;
  9626. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  9627. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  9628. ctx.beginPath();
  9629. ctx.moveTo(xFrom, yFrom);
  9630. ctx.lineTo(xTo, yTo);
  9631. ctx.stroke();
  9632. // draw arrow at the end of the line
  9633. length = 10 + 5 * this.width; // TODO: make customizable?
  9634. ctx.arrow(xTo, yTo, angle, length);
  9635. ctx.fill();
  9636. ctx.stroke();
  9637. // draw text
  9638. if (this.text) {
  9639. var point = this._pointOnLine(0.5);
  9640. this._text(ctx, this.text, point.x, point.y);
  9641. }
  9642. }
  9643. else {
  9644. // draw circle
  9645. var radius = this.length / 2 / Math.PI;
  9646. var x, y, arrow;
  9647. var node = this.from;
  9648. if (!node.width) {
  9649. node.resize(ctx);
  9650. }
  9651. if (node.width > node.height) {
  9652. x = node.x + node.width / 2;
  9653. y = node.y - radius;
  9654. arrow = {
  9655. x: x,
  9656. y: node.y,
  9657. angle: 0.9 * Math.PI
  9658. };
  9659. }
  9660. else {
  9661. x = node.x + radius;
  9662. y = node.y - node.height / 2;
  9663. arrow = {
  9664. x: node.x,
  9665. y: y,
  9666. angle: 0.6 * Math.PI
  9667. };
  9668. }
  9669. ctx.beginPath();
  9670. // TODO: do not draw a circle, but an arc
  9671. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9672. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9673. ctx.stroke();
  9674. // draw all arrows
  9675. length = 10 + 5 * this.width; // TODO: make customizable?
  9676. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9677. ctx.fill();
  9678. ctx.stroke();
  9679. // draw text
  9680. if (this.text) {
  9681. point = this._pointOnCircle(x, y, radius, 0.5);
  9682. this._text(ctx, this.text, point.x, point.y);
  9683. }
  9684. }
  9685. };
  9686. /**--------------------------------------------------------------------------**/
  9687. /**
  9688. * @class Images
  9689. * This class loades images and keeps them stored.
  9690. */
  9691. Graph.Images = function () {
  9692. this.images = {};
  9693. this.callback = undefined;
  9694. };
  9695. /**
  9696. * Set an onload callback function. This will be called each time an image
  9697. * is loaded
  9698. * @param {function} callback
  9699. */
  9700. Graph.Images.prototype.setOnloadCallback = function(callback) {
  9701. this.callback = callback;
  9702. };
  9703. /**
  9704. *
  9705. * @param {string} url Url of the image
  9706. * @return {Image} img The image object
  9707. */
  9708. Graph.Images.prototype.load = function(url) {
  9709. var img = this.images[url];
  9710. if (img == undefined) {
  9711. // create the image
  9712. var images = this;
  9713. img = new Image();
  9714. this.images[url] = img;
  9715. img.onload = function() {
  9716. if (images.callback) {
  9717. images.callback(this);
  9718. }
  9719. };
  9720. img.src = url;
  9721. }
  9722. return img;
  9723. };
  9724. /**--------------------------------------------------------------------------**/
  9725. /**
  9726. * @class Package
  9727. * This class contains one package
  9728. *
  9729. * @param {number} properties Properties for the package. Optional. Available
  9730. * properties are: id {number}, title {string},
  9731. * style {string} with available values "dot" and
  9732. * "image", radius {number}, image {string},
  9733. * color {string}, progress {number} with a value
  9734. * between 0-1, duration {number}, timestamp {number
  9735. * or Date}.
  9736. * @param {Graph} graph The graph object, used to find
  9737. * and edge to nodes.
  9738. * @param {Graph.Images} imagelist An Images object. Only needed
  9739. * when the package has style 'image'
  9740. * @param {Object} constants An object with default values for
  9741. * example for the color
  9742. */
  9743. Graph.Package = function (properties, graph, imagelist, constants) {
  9744. if (graph == undefined) {
  9745. throw "No graph provided";
  9746. }
  9747. // constants
  9748. this.radiusMin = constants.packages.radiusMin;
  9749. this.radiusMax = constants.packages.radiusMax;
  9750. this.imagelist = imagelist;
  9751. this.graph = graph;
  9752. // initialize variables
  9753. this.id = undefined;
  9754. this.from = undefined;
  9755. this.to = undefined;
  9756. this.title = undefined;
  9757. this.style = constants.packages.style;
  9758. this.radius = constants.packages.radius;
  9759. this.color = constants.packages.color;
  9760. this.image = constants.packages.image;
  9761. this.value = undefined;
  9762. this.progress = 0.0;
  9763. this.timestamp = undefined;
  9764. this.duration = constants.packages.duration;
  9765. this.autoProgress = true;
  9766. this.radiusFixed = false;
  9767. // set properties
  9768. this.setProperties(properties, constants);
  9769. };
  9770. Graph.Package.DEFAULT_DURATION = 1.0; // seconds
  9771. /**
  9772. * Set or overwrite properties for the package
  9773. * @param {Object} properties an object with properties
  9774. * @param {Object} constants and object with default, global properties
  9775. */
  9776. Graph.Package.prototype.setProperties = function(properties, constants) {
  9777. if (!properties) {
  9778. return;
  9779. }
  9780. // note that the provided properties can also be null, when they come from the Google DataTable
  9781. if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);}
  9782. if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);}
  9783. if (!this.from) {
  9784. throw "Node with id " + properties.from + " not found";
  9785. }
  9786. if (!this.to) {
  9787. throw "Node with id " + properties.to + " not found";
  9788. }
  9789. if (properties.id != undefined) {this.id = properties.id;}
  9790. if (properties.title != undefined) {this.title = properties.title;}
  9791. if (properties.style != undefined) {this.style = properties.style;}
  9792. if (properties.radius != undefined) {this.radius = properties.radius;}
  9793. if (properties.value != undefined) {this.value = properties.value;}
  9794. if (properties.image != undefined) {this.image = properties.image;}
  9795. if (properties.color != undefined) {this.color = properties.color;}
  9796. if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;}
  9797. if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;}
  9798. if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;}
  9799. if (properties.progress != undefined) {this.progress = properties.progress;}
  9800. if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;}
  9801. if (properties.duration != undefined) {this.duration = properties.duration;}
  9802. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  9803. this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false;
  9804. if (this.style == 'image') {
  9805. this.radiusMin = constants.packages.widthMin;
  9806. this.radiusMax = constants.packages.widthMax;
  9807. }
  9808. // handle progress
  9809. if (this.progress < 0.0) {this.progress = 0.0;}
  9810. if (this.progress > 1.0) {this.progress = 1.0;}
  9811. // handle image
  9812. if (this.image != undefined) {
  9813. if (this.imagelist) {
  9814. this.imageObj = this.imagelist.load(this.image);
  9815. }
  9816. else {
  9817. throw "No imagelist provided";
  9818. }
  9819. }
  9820. // choose draw method depending on the style
  9821. switch (this.style) {
  9822. // TODO: add more styles
  9823. case 'dot': this.draw = this._drawDot; break;
  9824. case 'square': this.draw = this._drawSquare; break;
  9825. case 'triangle': this.draw = this._drawTriangle; break;
  9826. case 'triangleDown':this.draw = this._drawTriangleDown; break;
  9827. case 'star': this.draw = this._drawStar; break;
  9828. case 'image': this.draw = this._drawImage; break;
  9829. default: this.draw = this._drawDot; break;
  9830. }
  9831. };
  9832. /**
  9833. * Set a new value for the progress of the package
  9834. * @param {number} progress A value between 0 and 1
  9835. */
  9836. Graph.Package.prototype.setProgress = function (progress) {
  9837. this.progress = progress;
  9838. this.autoProgress = false;
  9839. };
  9840. /**
  9841. * Check if a package is finished, if it has reached its destination.
  9842. * If so, the package can be removed.
  9843. * Only packages with automatically animated progress can be finished
  9844. * @return {boolean} true if finished, else false.
  9845. */
  9846. Graph.Package.prototype.isFinished = function () {
  9847. return (this.autoProgress == true && this.progress >= 1.0);
  9848. };
  9849. /**
  9850. * Check if this package is moving.
  9851. * A packages moves when it has automatic progress and not yet reached its
  9852. * destination.
  9853. * @return {boolean} true if moving, else false.
  9854. */
  9855. Graph.Package.prototype.isMoving = function () {
  9856. return (this.autoProgress || this.isFinished());
  9857. };
  9858. /**
  9859. * Perform one discrete step for the package. Only applicable when the
  9860. * package has no manually set, fixed progress.
  9861. * @param {number} interval Time interval in seconds
  9862. */
  9863. Graph.Package.prototype.discreteStep = function(interval) {
  9864. if (this.autoProgress == true) {
  9865. this.progress += (parseFloat(interval) / this.duration);
  9866. if (this.progress > 1.0)
  9867. this.progress = 1.0;
  9868. }
  9869. };
  9870. /**
  9871. * Draw this package in the given canvas
  9872. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9873. * @param {CanvasRenderingContext2D} ctx
  9874. */
  9875. Graph.Package.prototype.draw = function(ctx) {
  9876. throw "Draw method not initialized for package";
  9877. };
  9878. /**
  9879. * Check if this object is overlapping with the provided object
  9880. * @param {Object} obj an object with parameters left, top, right, bottom
  9881. * @return {boolean} True if location is located on node
  9882. */
  9883. Graph.Package.prototype.isOverlappingWith = function(obj) {
  9884. // radius minimum 10px else it is too hard to get your mouse at the exact right position
  9885. var radius = Math.max(this.radius, 10);
  9886. var pos = this._getPosition();
  9887. return (pos.x - radius < obj.right &&
  9888. pos.x + radius > obj.left &&
  9889. pos.y - radius < obj.bottom &&
  9890. pos.y + radius > obj.top);
  9891. };
  9892. /**
  9893. * Calculate the current position of the package
  9894. * @return {Object} position The object has parameters x and y.
  9895. */
  9896. Graph.Package.prototype._getPosition = function() {
  9897. return {
  9898. "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x,
  9899. "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y
  9900. };
  9901. };
  9902. /**
  9903. * get the title of this package.
  9904. * @return {string} title The title of the package, or undefined when no
  9905. * title has been set.
  9906. */
  9907. Graph.Package.prototype.getTitle = function() {
  9908. return this.title;
  9909. };
  9910. /**
  9911. * Retrieve the value of the package. Can be undefined
  9912. * @return {Number} value
  9913. */
  9914. Graph.Package.prototype.getValue = function() {
  9915. return this.value;
  9916. };
  9917. /**
  9918. * Calculate the distance from the packages location to the given location (x,y)
  9919. * @param {Number} x
  9920. * @param {Number} y
  9921. * @return {Number} value
  9922. */
  9923. Graph.Package.prototype.getDistance = function(x, y) {
  9924. var pos = this._getPosition(),
  9925. dx = pos.x - x,
  9926. dy = pos.y - y;
  9927. return Math.sqrt(dx * dx + dy * dy);
  9928. };
  9929. /**
  9930. * Adjust the value range of the package. The package will adjust it's radius
  9931. * based on its value.
  9932. * @param {Number} min
  9933. * @param {Number} max
  9934. */
  9935. Graph.Package.prototype.setValueRange = function(min, max) {
  9936. if (!this.radiusFixed && this.value !== undefined) {
  9937. var factor = (this.radiusMax - this.radiusMin) / (max - min);
  9938. this.radius = (this.value - min) * factor + this.radiusMin;
  9939. }
  9940. };
  9941. /**
  9942. * Redraw a package as a dot
  9943. * Draw this edge in the given canvas
  9944. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9945. * @param {CanvasRenderingContext2D} ctx
  9946. */
  9947. /* TODO: cleanup
  9948. Graph.Package.prototype._drawDot = function(ctx) {
  9949. // set style
  9950. ctx.fillStyle = this.color;
  9951. // draw dot
  9952. var pos = this._getPosition();
  9953. ctx.circle(pos.x, pos.y, this.radius);
  9954. ctx.fill();
  9955. }
  9956. */
  9957. Graph.Package.prototype._drawDot = function (ctx) {
  9958. this._drawShape(ctx, 'circle');
  9959. };
  9960. Graph.Package.prototype._drawTriangle = function (ctx) {
  9961. this._drawShape(ctx, 'triangle');
  9962. };
  9963. Graph.Package.prototype._drawTriangleDown = function (ctx) {
  9964. this._drawShape(ctx, 'triangleDown');
  9965. };
  9966. Graph.Package.prototype._drawSquare = function (ctx) {
  9967. this._drawShape(ctx, 'square');
  9968. };
  9969. Graph.Package.prototype._drawStar = function (ctx) {
  9970. this._drawShape(ctx, 'star');
  9971. };
  9972. Graph.Package.prototype._drawShape = function (ctx, shape) {
  9973. // set style
  9974. ctx.fillStyle = this.color;
  9975. // draw shape
  9976. var pos = this._getPosition();
  9977. ctx[shape](pos.x, pos.y, this.radius);
  9978. ctx.fill();
  9979. };
  9980. /**
  9981. * Redraw a package as an image
  9982. * Draw this edge in the given canvas
  9983. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9984. * @param {CanvasRenderingContext2D} ctx
  9985. */
  9986. Graph.Package.prototype._drawImage = function (ctx) {
  9987. if (this.imageObj) {
  9988. var width, height;
  9989. if (this.value) {
  9990. var scale = this.imageObj.height / this.imageObj.width;
  9991. width = this.radius || this.imageObj.width;
  9992. height = this.radius * scale || this.imageObj.height;
  9993. }
  9994. else {
  9995. width = this.imageObj.width;
  9996. height = this.imageObj.height;
  9997. }
  9998. var pos = this._getPosition();
  9999. ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height);
  10000. }
  10001. else {
  10002. console.log("image still loading...");
  10003. }
  10004. };
  10005. /**--------------------------------------------------------------------------**/
  10006. /**
  10007. * @class Groups
  10008. * This class can store groups and properties specific for groups.
  10009. */
  10010. Graph.Groups = function () {
  10011. this.clear();
  10012. this.defaultIndex = 0;
  10013. };
  10014. /**
  10015. * default constants for group colors
  10016. */
  10017. Graph.Groups.DEFAULT = [
  10018. {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue
  10019. {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow
  10020. {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red
  10021. {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green
  10022. {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta
  10023. {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple
  10024. {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange
  10025. {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue
  10026. {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink
  10027. {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"} // mint
  10028. ];
  10029. /**
  10030. * Clear all groups
  10031. */
  10032. Graph.Groups.prototype.clear = function () {
  10033. this.groups = {};
  10034. this.groups.length = function()
  10035. {
  10036. var i = 0;
  10037. for ( var p in this ) {
  10038. if (this.hasOwnProperty(p)) {
  10039. i++;
  10040. }
  10041. }
  10042. return i;
  10043. }
  10044. };
  10045. /**
  10046. * get group properties of a groupname. If groupname is not found, a new group
  10047. * is added.
  10048. * @param {*} groupname Can be a number, string, Date, etc.
  10049. * @return {Object} group The created group, containing all group properties
  10050. */
  10051. Graph.Groups.prototype.get = function (groupname) {
  10052. var group = this.groups[groupname];
  10053. if (group == undefined) {
  10054. // create new group
  10055. var index = this.defaultIndex % Graph.Groups.DEFAULT.length;
  10056. this.defaultIndex++;
  10057. group = {};
  10058. group.borderColor = Graph.Groups.DEFAULT[index].borderColor;
  10059. group.backgroundColor = Graph.Groups.DEFAULT[index].backgroundColor;
  10060. group.highlightColor = Graph.Groups.DEFAULT[index].highlightColor;
  10061. this.groups[groupname] = group;
  10062. }
  10063. return group;
  10064. };
  10065. /**
  10066. * Add a custom group style
  10067. * @param {String} groupname
  10068. * @param {Object} style An object containing borderColor,
  10069. * backgroundColor, etc.
  10070. * @return {Object} group The created group object
  10071. */
  10072. Graph.Groups.prototype.add = function (groupname, style) {
  10073. this.groups[groupname] = style;
  10074. return style;
  10075. };
  10076. /**
  10077. * Check if given object is a Javascript Array
  10078. * @param {*} obj
  10079. * @return {Boolean} isArray true if the given object is an array
  10080. */
  10081. // See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
  10082. Graph.isArray = function (obj) {
  10083. if (obj instanceof Array) {
  10084. return true;
  10085. }
  10086. return (Object.prototype.toString.call(obj) === '[object Array]');
  10087. };
  10088. /**--------------------------------------------------------------------------**/
  10089. /**
  10090. * @class Slider
  10091. *
  10092. * An html slider control with start/stop/prev/next buttons
  10093. * @param {Element} container The element where the slider will be created
  10094. */
  10095. Graph.Slider = function(container) {
  10096. if (container === undefined) throw "Error: No container element defined";
  10097. this.container = container;
  10098. this.frame = document.createElement("DIV");
  10099. //this.frame.style.backgroundColor = "#E5E5E5";
  10100. this.frame.style.width = "100%";
  10101. this.frame.style.position = "relative";
  10102. this.title = document.createElement("DIV");
  10103. this.title.style.margin = "2px";
  10104. this.title.style.marginBottom = "5px";
  10105. this.title.innerHTML = "";
  10106. this.container.appendChild(this.title);
  10107. this.frame.prev = document.createElement("INPUT");
  10108. this.frame.prev.type = "BUTTON";
  10109. this.frame.prev.value = "Prev";
  10110. this.frame.appendChild(this.frame.prev);
  10111. this.frame.play = document.createElement("INPUT");
  10112. this.frame.play.type = "BUTTON";
  10113. this.frame.play.value = "Play";
  10114. this.frame.appendChild(this.frame.play);
  10115. this.frame.next = document.createElement("INPUT");
  10116. this.frame.next.type = "BUTTON";
  10117. this.frame.next.value = "Next";
  10118. this.frame.appendChild(this.frame.next);
  10119. this.frame.bar = document.createElement("INPUT");
  10120. this.frame.bar.type = "BUTTON";
  10121. this.frame.bar.style.position = "absolute";
  10122. this.frame.bar.style.border = "1px solid red";
  10123. this.frame.bar.style.width = "100px";
  10124. this.frame.bar.style.height = "6px";
  10125. this.frame.bar.style.borderRadius = "2px";
  10126. this.frame.bar.style.MozBorderRadius = "2px";
  10127. this.frame.bar.style.border = "1px solid #7F7F7F";
  10128. this.frame.bar.style.backgroundColor = "#E5E5E5";
  10129. this.frame.appendChild(this.frame.bar);
  10130. this.frame.slide = document.createElement("INPUT");
  10131. this.frame.slide.type = "BUTTON";
  10132. this.frame.slide.style.margin = "0px";
  10133. this.frame.slide.value = " ";
  10134. this.frame.slide.style.position = "relative";
  10135. this.frame.slide.style.left = "-100px";
  10136. this.frame.appendChild(this.frame.slide);
  10137. // create events
  10138. var me = this;
  10139. this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
  10140. this.frame.prev.onclick = function (event) {me.prev(event);};
  10141. this.frame.play.onclick = function (event) {me.togglePlay(event);};
  10142. this.frame.next.onclick = function (event) {me.next(event);};
  10143. this.container.appendChild(this.frame);
  10144. this.onChangeCallback = undefined;
  10145. this.playTimeout = undefined;
  10146. this.framerate = 20; // frames per second
  10147. this.duration = 10; // seconds
  10148. this.doLoop = true;
  10149. this.start = 0;
  10150. this.end = 0;
  10151. this.value = 0;
  10152. this.step = 0;
  10153. this.rangeIsDate = false;
  10154. this.redraw();
  10155. };
  10156. /**
  10157. * Retrieve the step size, depending on the range, framerate, and duration
  10158. */
  10159. Graph.Slider.prototype._updateStep = function() {
  10160. var range = (this.end - this.start);
  10161. var frameCount = this.duration * this.framerate;
  10162. this.step = range / frameCount;
  10163. };
  10164. /**
  10165. * Select the previous index
  10166. */
  10167. Graph.Slider.prototype.prev = function() {
  10168. this._setValue(this.value - this.step);
  10169. };
  10170. /**
  10171. * Select the next index
  10172. */
  10173. Graph.Slider.prototype.next = function() {
  10174. this._setValue(this.value + this.step);
  10175. };
  10176. /**
  10177. * Select the next index
  10178. */
  10179. Graph.Slider.prototype.playNext = function() {
  10180. var start = new Date();
  10181. if (!this.leftButtonDown) {
  10182. if (this.value + this.step < this.end) {
  10183. this._setValue(this.value + this.step);
  10184. }
  10185. else {
  10186. if (this.doLoop) {
  10187. this._setValue(this.start);
  10188. }
  10189. else {
  10190. this._setValue(this.end);
  10191. this.stop();
  10192. return;
  10193. }
  10194. }
  10195. }
  10196. var end = new Date();
  10197. var diff = (end - start);
  10198. // calculate how much time it to to set the index and to execute the callback
  10199. // function.
  10200. var interval = Math.max(1000 / this.framerate - diff, 0);
  10201. var me = this;
  10202. this.playTimeout = setTimeout(function() {me.playNext();}, interval);
  10203. };
  10204. /**
  10205. * Toggle start or stop playing
  10206. */
  10207. Graph.Slider.prototype.togglePlay = function() {
  10208. if (this.playTimeout === undefined) {
  10209. this.play();
  10210. } else {
  10211. this.stop();
  10212. }
  10213. };
  10214. /**
  10215. * Start playing
  10216. */
  10217. Graph.Slider.prototype.play = function() {
  10218. this.frame.play.value = "Stop";
  10219. this.playNext();
  10220. };
  10221. /**
  10222. * Stop playing
  10223. */
  10224. Graph.Slider.prototype.stop = function() {
  10225. this.frame.play.value = "Play";
  10226. clearInterval(this.playTimeout);
  10227. this.playTimeout = undefined;
  10228. };
  10229. /**
  10230. * Set a callback function which will be triggered when the value of the
  10231. * slider bar has changed.
  10232. */
  10233. Graph.Slider.prototype.setOnChangeCallback = function(callback) {
  10234. this.onChangeCallback = callback;
  10235. };
  10236. /**
  10237. * Set the interval for playing the list
  10238. * @param {number} framerate Framerate in frames per second
  10239. */
  10240. Graph.Slider.prototype.setFramerate = function(framerate) {
  10241. this.framerate = framerate;
  10242. this._updateStep();
  10243. };
  10244. /**
  10245. * Retrieve the current framerate
  10246. * @return {number} framerate in frames per second
  10247. */
  10248. Graph.Slider.prototype.getFramerate = function() {
  10249. return this.framerate;
  10250. };
  10251. /**
  10252. * Set the duration for playing
  10253. * @param {number} duration Duration in seconds
  10254. */
  10255. Graph.Slider.prototype.setDuration = function(duration) {
  10256. this.duration = duration;
  10257. this._updateStep();
  10258. };
  10259. /**
  10260. * Set the time acceleration for playing the history. Only applicable when
  10261. * the values are of type Date.
  10262. * @param {number} acceleration Acceleration, for example 10 means play
  10263. * ten times as fast as real time. A value
  10264. * of 1 will play the history in real time.
  10265. */
  10266. Graph.Slider.prototype.setAcceleration = function(acceleration) {
  10267. var durationRealtime = (this.end - this.start) / 1000; // in seconds
  10268. this.duration = durationRealtime / acceleration;
  10269. this._updateStep();
  10270. };
  10271. /**
  10272. * Set looping on or off
  10273. * @param {boolean} doLoop If true, the slider will jump to the start when
  10274. * the end is passed, and will jump to the end
  10275. * when the start is passed.
  10276. */
  10277. Graph.Slider.prototype.setLoop = function(doLoop) {
  10278. this.doLoop = doLoop;
  10279. };
  10280. /**
  10281. * Retrieve the current value of loop
  10282. * @return {boolean} doLoop If true, the slider will jump to the start when
  10283. * the end is passed, and will jump to the end
  10284. * when the start is passed.
  10285. */
  10286. Graph.Slider.prototype.getLoop = function() {
  10287. return this.doLoop;
  10288. };
  10289. /**
  10290. * Execute the onchange callback function
  10291. */
  10292. Graph.Slider.prototype.onChange = function() {
  10293. if (this.onChangeCallback !== undefined) {
  10294. this.onChangeCallback();
  10295. }
  10296. };
  10297. /**
  10298. * redraw the slider on the correct place
  10299. */
  10300. Graph.Slider.prototype.redraw = function() {
  10301. // resize the bar
  10302. var barTop = (this.frame.clientHeight/2 -
  10303. this.frame.bar.offsetHeight/2);
  10304. var barWidth = (this.frame.clientWidth -
  10305. this.frame.prev.clientWidth -
  10306. this.frame.play.clientWidth -
  10307. this.frame.next.clientWidth - 30);
  10308. this.frame.bar.style.top = barTop + "px";
  10309. this.frame.bar.style.width = barWidth + "px";
  10310. // position the slider button
  10311. this.frame.slide.title = this.getValue();
  10312. this.frame.slide.style.left = this._valueToLeft(this.value) + "px";
  10313. // set the title
  10314. this.title.innerHTML = this.getValue();
  10315. };
  10316. /**
  10317. * Set the range for the slider
  10318. * @param {Date | Number} start Start of the range
  10319. * @param {Date | Number} end End of the range
  10320. */
  10321. Graph.Slider.prototype.setRange = function(start, end) {
  10322. if (start === undefined || start === null || start === NaN) {
  10323. this.start = 0;
  10324. this.rangeIsDate = false;
  10325. }
  10326. else if (start instanceof Date) {
  10327. this.start = start.getTime();
  10328. this.rangeIsDate = true;
  10329. }
  10330. else {
  10331. this.start = start;
  10332. this.rangeIsDate = false;
  10333. }
  10334. if (end === undefined || end === null || end === NaN) {
  10335. if (this.start instanceof Date) {
  10336. this.end = new Date(this.start);
  10337. }
  10338. else {
  10339. this.end = this.start;
  10340. }
  10341. }
  10342. else if (end instanceof Date) {
  10343. this.end = end.getTime();
  10344. }
  10345. else {
  10346. this.end = end;
  10347. }
  10348. this.value = this.start;
  10349. this._updateStep();
  10350. this.redraw();
  10351. };
  10352. /**
  10353. * Set a value for the slider. The value must be between start and end
  10354. * When the range are Dates, the value will be translated to a date
  10355. * @param {Number} value
  10356. */
  10357. Graph.Slider.prototype._setValue = function(value) {
  10358. this.value = this._limitValue(value);
  10359. this.redraw();
  10360. this.onChange();
  10361. };
  10362. /**
  10363. * retrieve the current value in the correct type, Number or Date
  10364. * @return {Date | Number} value
  10365. */
  10366. Graph.Slider.prototype.getValue = function() {
  10367. if (this.rangeIsDate) {
  10368. return new Date(this.value);
  10369. }
  10370. else {
  10371. return this.value;
  10372. }
  10373. };
  10374. Graph.Slider.prototype.offset = 3;
  10375. Graph.Slider.prototype._leftToValue = function (left) {
  10376. var width = parseFloat(this.frame.bar.style.width) -
  10377. this.frame.slide.clientWidth - 10;
  10378. var x = left - this.offset;
  10379. var range = this.end - this.start;
  10380. var value = this._limitValue(x / width * range + this.start);
  10381. return value;
  10382. };
  10383. Graph.Slider.prototype._valueToLeft = function (value) {
  10384. var width = parseFloat(this.frame.bar.style.width) -
  10385. this.frame.slide.clientWidth - 10;
  10386. var x;
  10387. if (this.end > this.start) {
  10388. x = (value - this.start) / (this.end - this.start) * width;
  10389. }
  10390. else {
  10391. x = 0;
  10392. }
  10393. var left = x + this.offset;
  10394. return left;
  10395. };
  10396. Graph.Slider.prototype._limitValue = function(value) {
  10397. if (value < this.start) {
  10398. value = this.start
  10399. }
  10400. if (value > this.end) {
  10401. value = this.end;
  10402. }
  10403. return value;
  10404. };
  10405. Graph.Slider.prototype._onMouseDown = function(event) {
  10406. // only react on left mouse button down
  10407. this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  10408. if (!this.leftButtonDown) return;
  10409. this.startClientX = event.clientX;
  10410. this.startSlideX = parseFloat(this.frame.slide.style.left);
  10411. this.frame.style.cursor = 'move';
  10412. // add event listeners to handle moving the contents
  10413. // we store the function onmousemove and onmouseup in the graph, so we can
  10414. // remove the eventlisteners lateron in the function mouseUp()
  10415. var me = this;
  10416. this.onmousemove = function (event) {me._onMouseMove(event);};
  10417. this.onmouseup = function (event) {me._onMouseUp(event);};
  10418. Graph.addEventListener(document, "mousemove", this.onmousemove);
  10419. Graph.addEventListener(document, "mouseup", this.onmouseup);
  10420. Graph.preventDefault(event);
  10421. };
  10422. Graph.Slider.prototype._onMouseMove = function (event) {
  10423. var diff = event.clientX - this.startClientX;
  10424. var x = this.startSlideX + diff;
  10425. var value = this._leftToValue(x);
  10426. this._setValue(value);
  10427. Graph.preventDefault(event);
  10428. };
  10429. Graph.Slider.prototype._onMouseUp = function (event) {
  10430. this.frame.style.cursor = 'auto';
  10431. this.leftButtonDown = false;
  10432. // remove event listeners
  10433. Graph.removeEventListener(document, "mousemove", this.onmousemove);
  10434. Graph.removeEventListener(document, "mouseup", this.onmouseup);
  10435. Graph.preventDefault(event);
  10436. };
  10437. /**--------------------------------------------------------------------------**/
  10438. /**
  10439. * Popup is a class to create a popup window with some text
  10440. * @param {Element} container The container object.
  10441. * @param {Number} x
  10442. * @param {Number} y
  10443. * @param {String} text
  10444. */
  10445. Graph.Popup = function (container, x, y, text) {
  10446. if (container) {
  10447. this.container = container;
  10448. }
  10449. else {
  10450. this.container = document.body;
  10451. }
  10452. this.x = 0;
  10453. this.y = 0;
  10454. this.padding = 5;
  10455. if (x !== undefined && y !== undefined ) {
  10456. this.setPosition(x, y);
  10457. }
  10458. if (text !== undefined) {
  10459. this.setText(text);
  10460. }
  10461. // create the frame
  10462. this.frame = document.createElement("div");
  10463. var style = this.frame.style;
  10464. style.position = "absolute";
  10465. style.visibility = "hidden";
  10466. style.border = "1px solid #666";
  10467. style.color = "black";
  10468. style.padding = this.padding + "px";
  10469. style.backgroundColor = "#FFFFC6";
  10470. style.borderRadius = "3px";
  10471. style.MozBorderRadius = "3px";
  10472. style.WebkitBorderRadius = "3px";
  10473. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  10474. style.whiteSpace = "nowrap";
  10475. this.container.appendChild(this.frame);
  10476. };
  10477. /**
  10478. * @param {number} x Horizontal position of the popup window
  10479. * @param {number} y Vertical position of the popup window
  10480. */
  10481. Graph.Popup.prototype.setPosition = function(x, y) {
  10482. this.x = parseInt(x);
  10483. this.y = parseInt(y);
  10484. };
  10485. /**
  10486. * Set the text for the popup window. This can be HTML code
  10487. * @param {string} text
  10488. */
  10489. Graph.Popup.prototype.setText = function(text) {
  10490. this.frame.innerHTML = text;
  10491. };
  10492. /**
  10493. * Show the popup window
  10494. * @param {boolean} show Optional. Show or hide the window
  10495. */
  10496. Graph.Popup.prototype.show = function (show) {
  10497. if (show === undefined) {
  10498. show = true;
  10499. }
  10500. if (show) {
  10501. var height = this.frame.clientHeight;
  10502. var width = this.frame.clientWidth;
  10503. var maxHeight = this.frame.parentNode.clientHeight;
  10504. var maxWidth = this.frame.parentNode.clientWidth;
  10505. var top = (this.y - height);
  10506. if (top + height + this.padding > maxHeight) {
  10507. top = maxHeight - height - this.padding;
  10508. }
  10509. if (top < this.padding) {
  10510. top = this.padding;
  10511. }
  10512. var left = this.x;
  10513. if (left + width + this.padding > maxWidth) {
  10514. left = maxWidth - width - this.padding;
  10515. }
  10516. if (left < this.padding) {
  10517. left = this.padding;
  10518. }
  10519. this.frame.style.left = left + "px";
  10520. this.frame.style.top = top + "px";
  10521. this.frame.style.visibility = "visible";
  10522. }
  10523. else {
  10524. this.hide();
  10525. }
  10526. };
  10527. /**
  10528. * Hide the popup window
  10529. */
  10530. Graph.Popup.prototype.hide = function () {
  10531. this.frame.style.visibility = "hidden";
  10532. };
  10533. /**--------------------------------------------------------------------------**/
  10534. if (typeof CanvasRenderingContext2D !== 'undefined') {
  10535. /**
  10536. * Draw a circle shape
  10537. */
  10538. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  10539. this.beginPath();
  10540. this.arc(x, y, r, 0, 2*Math.PI, false);
  10541. };
  10542. /**
  10543. * Draw a square shape
  10544. * @param {Number} x horizontal center
  10545. * @param {Number} y vertical center
  10546. * @param {Number} r size, width and height of the square
  10547. */
  10548. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  10549. this.beginPath();
  10550. this.rect(x - r, y - r, r * 2, r * 2);
  10551. };
  10552. /**
  10553. * Draw a triangle shape
  10554. * @param {Number} x horizontal center
  10555. * @param {Number} y vertical center
  10556. * @param {Number} r radius, half the length of the sides of the triangle
  10557. */
  10558. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  10559. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10560. this.beginPath();
  10561. var s = r * 2;
  10562. var s2 = s / 2;
  10563. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10564. var h = Math.sqrt(s * s - s2 * s2); // height
  10565. this.moveTo(x, y - (h - ir));
  10566. this.lineTo(x + s2, y + ir);
  10567. this.lineTo(x - s2, y + ir);
  10568. this.lineTo(x, y - (h - ir));
  10569. this.closePath();
  10570. };
  10571. /**
  10572. * Draw a triangle shape in downward orientation
  10573. * @param {Number} x horizontal center
  10574. * @param {Number} y vertical center
  10575. * @param {Number} r radius
  10576. */
  10577. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  10578. // http://en.wikipedia.org/wiki/Equilateral_triangle
  10579. this.beginPath();
  10580. var s = r * 2;
  10581. var s2 = s / 2;
  10582. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  10583. var h = Math.sqrt(s * s - s2 * s2); // height
  10584. this.moveTo(x, y + (h - ir));
  10585. this.lineTo(x + s2, y - ir);
  10586. this.lineTo(x - s2, y - ir);
  10587. this.lineTo(x, y + (h - ir));
  10588. this.closePath();
  10589. };
  10590. /**
  10591. * Draw a star shape, a star with 5 points
  10592. * @param {Number} x horizontal center
  10593. * @param {Number} y vertical center
  10594. * @param {Number} r radius, half the length of the sides of the triangle
  10595. */
  10596. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  10597. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  10598. this.beginPath();
  10599. for (var n = 0; n < 10; n++) {
  10600. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  10601. this.lineTo(
  10602. x + radius * Math.sin(n * 2 * Math.PI / 10),
  10603. y - radius * Math.cos(n * 2 * Math.PI / 10)
  10604. );
  10605. }
  10606. this.closePath();
  10607. };
  10608. /**
  10609. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  10610. */
  10611. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  10612. var r2d = Math.PI/180;
  10613. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  10614. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  10615. this.beginPath();
  10616. this.moveTo(x+r,y);
  10617. this.lineTo(x+w-r,y);
  10618. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  10619. this.lineTo(x+w,y+h-r);
  10620. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  10621. this.lineTo(x+r,y+h);
  10622. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  10623. this.lineTo(x,y+r);
  10624. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  10625. };
  10626. /**
  10627. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10628. */
  10629. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  10630. var kappa = .5522848,
  10631. ox = (w / 2) * kappa, // control point offset horizontal
  10632. oy = (h / 2) * kappa, // control point offset vertical
  10633. xe = x + w, // x-end
  10634. ye = y + h, // y-end
  10635. xm = x + w / 2, // x-middle
  10636. ym = y + h / 2; // y-middle
  10637. this.beginPath();
  10638. this.moveTo(x, ym);
  10639. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10640. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10641. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10642. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10643. };
  10644. /**
  10645. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  10646. */
  10647. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  10648. var f = 1/3;
  10649. var wEllipse = w;
  10650. var hEllipse = h * f;
  10651. var kappa = .5522848,
  10652. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  10653. oy = (hEllipse / 2) * kappa, // control point offset vertical
  10654. xe = x + wEllipse, // x-end
  10655. ye = y + hEllipse, // y-end
  10656. xm = x + wEllipse / 2, // x-middle
  10657. ym = y + hEllipse / 2, // y-middle
  10658. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  10659. yeb = y + h; // y-end, bottom ellipse
  10660. this.beginPath();
  10661. this.moveTo(xe, ym);
  10662. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  10663. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  10664. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  10665. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  10666. this.lineTo(xe, ymb);
  10667. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  10668. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  10669. this.lineTo(x, ym);
  10670. };
  10671. /**
  10672. * Draw an arrow point (no line)
  10673. */
  10674. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  10675. // tail
  10676. var xt = x - length * Math.cos(angle);
  10677. var yt = y - length * Math.sin(angle);
  10678. // inner tail
  10679. // TODO: allow to customize different shapes
  10680. var xi = x - length * 0.9 * Math.cos(angle);
  10681. var yi = y - length * 0.9 * Math.sin(angle);
  10682. // left
  10683. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  10684. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  10685. // right
  10686. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  10687. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  10688. this.beginPath();
  10689. this.moveTo(x, y);
  10690. this.lineTo(xl, yl);
  10691. this.lineTo(xi, yi);
  10692. this.lineTo(xr, yr);
  10693. this.closePath();
  10694. };
  10695. // TODO: add diamond shape
  10696. }
  10697. /*----------------------------------------------------------------------------*/
  10698. // utility methods
  10699. Graph.util = {};
  10700. /**
  10701. * Parse a text source containing data in DOT language into a JSON object.
  10702. * The object contains two lists: one with nodes and one with edges.
  10703. * @param {String} data Text containing a graph in DOT-notation
  10704. * @return {Object} json An object containing two parameters:
  10705. * {Object[]} nodes
  10706. * {Object[]} edges
  10707. */
  10708. Graph.util.parseDOT = function (data) {
  10709. /**
  10710. * Test whether given character is a whitespace character
  10711. * @param {String} c
  10712. * @return {Boolean} isWhitespace
  10713. */
  10714. function isWhitespace(c) {
  10715. return c == ' ' || c == '\t' || c == '\n' || c == '\r';
  10716. }
  10717. /**
  10718. * Test whether given character is a delimeter
  10719. * @param {String} c
  10720. * @return {Boolean} isDelimeter
  10721. */
  10722. function isDelimeter(c) {
  10723. return '[]{}();,=->'.indexOf(c) != -1;
  10724. }
  10725. var i = -1; // current index in the data
  10726. var c = ''; // current character in the data
  10727. /**
  10728. * Read the next character from the data
  10729. */
  10730. function next() {
  10731. i++;
  10732. c = data[i];
  10733. }
  10734. /**
  10735. * Preview the next character in the data
  10736. * @returns {String} nextChar
  10737. */
  10738. function previewNext () {
  10739. return data[i + 1];
  10740. }
  10741. /**
  10742. * Preview the next character in the data
  10743. * @returns {String} nextChar
  10744. */
  10745. function previewPrevious () {
  10746. return data[i + 1];
  10747. }
  10748. /**
  10749. * Get a text description of the the current index in the data
  10750. * @return {String} desc
  10751. */
  10752. function pos() {
  10753. return '(char ' + i + ')';
  10754. }
  10755. /**
  10756. * Skip whitespace and comments
  10757. */
  10758. function parseWhitespace() {
  10759. // skip whitespace
  10760. while (c && isWhitespace(c)) {
  10761. next();
  10762. }
  10763. // test for comment
  10764. var cNext = data[i + 1];
  10765. var cPrev = data[i - 1];
  10766. var c2 = c + cNext;
  10767. if (c2 == '/*') {
  10768. // block comment. skip until the block is closed
  10769. while (c && !(c == '*' && data[i + 1] == '/')) {
  10770. next();
  10771. }
  10772. next();
  10773. next();
  10774. parseWhitespace();
  10775. }
  10776. else if (c2 == '//' || (c == '#' && cPrev == '\n')) {
  10777. // line comment. skip until the next return
  10778. while (c && c != '\n') {
  10779. next();
  10780. }
  10781. next();
  10782. parseWhitespace();
  10783. }
  10784. }
  10785. /**
  10786. * Parse a string
  10787. * The string may be enclosed by double quotes
  10788. * @return {String | undefined} value
  10789. */
  10790. function parseString() {
  10791. parseWhitespace();
  10792. var name = '';
  10793. if (c == '"') {
  10794. next();
  10795. while (c && c != '"') {
  10796. name += c;
  10797. next();
  10798. }
  10799. next(); // skip the closing quote
  10800. }
  10801. else {
  10802. while (c && !isWhitespace(c) && !isDelimeter(c)) {
  10803. name += c;
  10804. next();
  10805. }
  10806. // cast string to number or boolean
  10807. var number = Number(name);
  10808. if (!isNaN(number)) {
  10809. name = number;
  10810. }
  10811. else if (name == 'true') {
  10812. name = true;
  10813. }
  10814. else if (name == 'false') {
  10815. name = false;
  10816. }
  10817. else if (name == 'null') {
  10818. name = null;
  10819. }
  10820. }
  10821. return name;
  10822. }
  10823. /**
  10824. * Parse a value, can be a string, number, or boolean.
  10825. * The value may be enclosed by double quotes
  10826. * @return {String | Number | Boolean | undefined} value
  10827. */
  10828. function parseValue() {
  10829. parseWhitespace();
  10830. if (c == '"') {
  10831. return parseString();
  10832. }
  10833. else {
  10834. var value = parseString();
  10835. if (value != undefined) {
  10836. // cast string to number or boolean
  10837. var number = Number(value);
  10838. if (!isNaN(number)) {
  10839. value = number;
  10840. }
  10841. else if (value == 'true') {
  10842. value = true;
  10843. }
  10844. else if (value == 'false') {
  10845. value = false;
  10846. }
  10847. else if (value == 'null') {
  10848. value = null;
  10849. }
  10850. }
  10851. return value;
  10852. }
  10853. }
  10854. /**
  10855. * Parse a set with attributes,
  10856. * for example [label="1.000", style=solid]
  10857. * @return {Object | undefined} attr
  10858. */
  10859. function parseAttributes() {
  10860. parseWhitespace();
  10861. if (c == '[') {
  10862. next();
  10863. var attr = {};
  10864. while (c && c != ']') {
  10865. parseWhitespace();
  10866. var name = parseString();
  10867. if (!name) {
  10868. throw new SyntaxError('Attribute name expected ' + pos());
  10869. }
  10870. parseWhitespace();
  10871. if (c != '=') {
  10872. throw new SyntaxError('Equal sign = expected ' + pos());
  10873. }
  10874. next();
  10875. var value = parseValue();
  10876. if (!value) {
  10877. throw new SyntaxError('Attribute value expected ' + pos());
  10878. }
  10879. attr[name] = value;
  10880. parseWhitespace();
  10881. if (c ==',') {
  10882. next();
  10883. }
  10884. }
  10885. next();
  10886. return attr;
  10887. }
  10888. else {
  10889. return undefined;
  10890. }
  10891. }
  10892. /**
  10893. * Parse a directed or undirected arrow '->' or '--'
  10894. * @return {String | undefined} arrow
  10895. */
  10896. function parseArrow() {
  10897. parseWhitespace();
  10898. if (c == '-') {
  10899. next();
  10900. if (c == '>' || c == '-') {
  10901. var arrow = '-' + c;
  10902. next();
  10903. return arrow;
  10904. }
  10905. else {
  10906. throw new SyntaxError('Arrow "->" or "--" expected ' + pos());
  10907. }
  10908. }
  10909. return undefined;
  10910. }
  10911. /**
  10912. * Parse a line separator ';'
  10913. * @return {String | undefined} separator
  10914. */
  10915. function parseSeparator() {
  10916. parseWhitespace();
  10917. if (c == ';') {
  10918. next();
  10919. return ';';
  10920. }
  10921. return undefined;
  10922. }
  10923. /**
  10924. * Merge all properties of object b into object b
  10925. * @param {Object} a
  10926. * @param {Object} b
  10927. */
  10928. function merge (a, b) {
  10929. if (a && b) {
  10930. for (var name in b) {
  10931. if (b.hasOwnProperty(name)) {
  10932. a[name] = b[name];
  10933. }
  10934. }
  10935. }
  10936. }
  10937. var nodeMap = {};
  10938. var edgeList = [];
  10939. /**
  10940. * Register a node with attributes
  10941. * @param {String} id
  10942. * @param {Object} [attr]
  10943. */
  10944. function addNode(id, attr) {
  10945. var node = {
  10946. id: String(id),
  10947. attr: attr || {}
  10948. };
  10949. if (!nodeMap[id]) {
  10950. nodeMap[id] = node;
  10951. }
  10952. else {
  10953. merge(nodeMap[id].attr, node.attr);
  10954. }
  10955. }
  10956. /**
  10957. * Register an edge
  10958. * @param {String} from
  10959. * @param {String} to
  10960. * @param {String} type A string "->" or "--"
  10961. * @param {Object} [attr]
  10962. */
  10963. function addEdge(from, to, type, attr) {
  10964. edgeList.push({
  10965. from: String(from),
  10966. to: String(to),
  10967. type: type,
  10968. attr: attr || {}
  10969. });
  10970. }
  10971. // find the opening curly bracket
  10972. next();
  10973. while (c && c != '{') {
  10974. next();
  10975. }
  10976. if (c != '{') {
  10977. throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos())
  10978. }
  10979. next();
  10980. // parse all data until a closing curly bracket is encountered
  10981. while (c && c != '}') {
  10982. // parse node id and optional node attributes
  10983. var id = parseString();
  10984. if (id == undefined) {
  10985. throw new SyntaxError('String with id expected ' + pos());
  10986. }
  10987. var attr = parseAttributes();
  10988. addNode(id, attr);
  10989. // TODO: parse global attributes "graph", "node", "edge"
  10990. // parse arrow
  10991. var type = parseArrow();
  10992. while (type) {
  10993. // parse node id
  10994. var prevId = id;
  10995. id = parseString();
  10996. if (id == undefined) {
  10997. throw new SyntaxError('String with id expected ' + pos());
  10998. }
  10999. addNode(id);
  11000. // parse edge attributes and register edge
  11001. attr = parseAttributes();
  11002. addEdge(prevId, id, type, attr);
  11003. // parse next arrow (optional)
  11004. type = parseArrow();
  11005. }
  11006. // parse separator (optional)
  11007. parseSeparator();
  11008. parseWhitespace();
  11009. }
  11010. if (c != '}') {
  11011. throw new SyntaxError('Invalid data. Curly bracket } expected');
  11012. }
  11013. // crop data between the curly brackets
  11014. var start = data.indexOf('{');
  11015. var end = data.indexOf('}', start);
  11016. var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined;
  11017. if (!text) {
  11018. throw new Error('Invalid data. no curly brackets containing data found');
  11019. }
  11020. // return the results
  11021. var nodeList = [];
  11022. for (id in nodeMap) {
  11023. if (nodeMap.hasOwnProperty(id)) {
  11024. nodeList.push(nodeMap[id]);
  11025. }
  11026. }
  11027. return {
  11028. nodes: nodeList,
  11029. edges: edgeList
  11030. }
  11031. };
  11032. /**
  11033. * Convert a string containing a graph in DOT language into a map containing
  11034. * with nodes and edges in the format of graph.
  11035. * @param {String} data Text containing a graph in DOT-notation
  11036. * @return {Object} graphData
  11037. */
  11038. Graph.util.DOTToGraph = function (data) {
  11039. // parse the DOT file
  11040. var dotData = Graph.util.parseDOT(data);
  11041. var graphData = {
  11042. nodes: [],
  11043. edges: [],
  11044. options: {
  11045. nodes: {},
  11046. edges: {}
  11047. }
  11048. };
  11049. /**
  11050. * Merge the properties of object b into object a, and adjust properties
  11051. * not supported by Graph (for example replace "shape" with "style"
  11052. * @param {Object} a
  11053. * @param {Object} b
  11054. * @param {Array} [ignore] Optional array with property names to be ignored
  11055. */
  11056. function merge (a, b, ignore) {
  11057. for (var prop in b) {
  11058. if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) {
  11059. a[prop] = b[prop];
  11060. }
  11061. }
  11062. // Convert aliases to configuration settings supported by Graph
  11063. if (a.label) {
  11064. a.text = a.label;
  11065. delete a.label;
  11066. }
  11067. if (a.shape) {
  11068. a.style = a.shape;
  11069. delete a.shape;
  11070. }
  11071. }
  11072. dotData.nodes.forEach(function (node) {
  11073. if (node.id.toLowerCase() == 'graph') {
  11074. merge(graphData.options, node.attr);
  11075. }
  11076. else if (node.id.toLowerCase() == 'node') {
  11077. merge(graphData.options.nodes, node.attr);
  11078. }
  11079. else if (node.id.toLowerCase() == 'edge') {
  11080. merge(graphData.options.edges, node.attr);
  11081. }
  11082. else {
  11083. var graphNode = {};
  11084. graphNode.id = node.id;
  11085. graphNode.text = node.id;
  11086. merge(graphNode, node.attr);
  11087. graphData.nodes.push(graphNode);
  11088. }
  11089. });
  11090. dotData.edges.forEach(function (edge) {
  11091. var graphEdge = {};
  11092. graphEdge.from = edge.from;
  11093. graphEdge.to = edge.to;
  11094. graphEdge.text = edge.id;
  11095. graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line';
  11096. merge(graphEdge, edge.attr);
  11097. graphData.edges.push(graphEdge);
  11098. });
  11099. return graphData;
  11100. };
  11101. /**
  11102. * vis.js module exports
  11103. */
  11104. var vis = {
  11105. util: util,
  11106. events: events,
  11107. Controller: Controller,
  11108. DataSet: DataSet,
  11109. DataView: DataView,
  11110. Range: Range,
  11111. Stack: Stack,
  11112. TimeStep: TimeStep,
  11113. EventBus: EventBus,
  11114. components: {
  11115. items: {
  11116. Item: Item,
  11117. ItemBox: ItemBox,
  11118. ItemPoint: ItemPoint,
  11119. ItemRange: ItemRange
  11120. },
  11121. Component: Component,
  11122. Panel: Panel,
  11123. RootPanel: RootPanel,
  11124. ItemSet: ItemSet,
  11125. TimeAxis: TimeAxis
  11126. },
  11127. Timeline: Timeline,
  11128. Graph: Graph
  11129. };
  11130. /**
  11131. * CommonJS module exports
  11132. */
  11133. if (typeof exports !== 'undefined') {
  11134. exports = vis;
  11135. }
  11136. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  11137. module.exports = vis;
  11138. }
  11139. /**
  11140. * AMD module exports
  11141. */
  11142. if (typeof(define) === 'function') {
  11143. define(function () {
  11144. return vis;
  11145. });
  11146. }
  11147. /**
  11148. * Window exports
  11149. */
  11150. if (typeof window !== 'undefined') {
  11151. // attach the module to the window, load as a regular javascript file
  11152. window['vis'] = vis;
  11153. }
  11154. // inject css
  11155. util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.point {\n background: none;\n}\n\n.graph .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  11156. })()
  11157. },{"moment":2}],2:[function(require,module,exports){
  11158. (function(){// moment.js
  11159. // version : 2.0.0
  11160. // author : Tim Wood
  11161. // license : MIT
  11162. // momentjs.com
  11163. (function (undefined) {
  11164. /************************************
  11165. Constants
  11166. ************************************/
  11167. var moment,
  11168. VERSION = "2.0.0",
  11169. round = Math.round, i,
  11170. // internal storage for language config files
  11171. languages = {},
  11172. // check for nodeJS
  11173. hasModule = (typeof module !== 'undefined' && module.exports),
  11174. // ASP.NET json date format regex
  11175. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  11176. // format tokens
  11177. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
  11178. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  11179. // parsing tokens
  11180. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  11181. // parsing token regexes
  11182. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  11183. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  11184. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  11185. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  11186. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  11187. parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
  11188. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  11189. parseTokenT = /T/i, // T (ISO seperator)
  11190. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  11191. // preliminary iso regex
  11192. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  11193. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  11194. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  11195. // iso time formats and regexes
  11196. isoTimes = [
  11197. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  11198. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  11199. ['HH:mm', /(T| )\d\d:\d\d/],
  11200. ['HH', /(T| )\d\d/]
  11201. ],
  11202. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  11203. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  11204. // getter and setter names
  11205. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  11206. unitMillisecondFactors = {
  11207. 'Milliseconds' : 1,
  11208. 'Seconds' : 1e3,
  11209. 'Minutes' : 6e4,
  11210. 'Hours' : 36e5,
  11211. 'Days' : 864e5,
  11212. 'Months' : 2592e6,
  11213. 'Years' : 31536e6
  11214. },
  11215. // format function strings
  11216. formatFunctions = {},
  11217. // tokens to ordinalize and pad
  11218. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  11219. paddedTokens = 'M D H h m s w W'.split(' '),
  11220. formatTokenFunctions = {
  11221. M : function () {
  11222. return this.month() + 1;
  11223. },
  11224. MMM : function (format) {
  11225. return this.lang().monthsShort(this, format);
  11226. },
  11227. MMMM : function (format) {
  11228. return this.lang().months(this, format);
  11229. },
  11230. D : function () {
  11231. return this.date();
  11232. },
  11233. DDD : function () {
  11234. return this.dayOfYear();
  11235. },
  11236. d : function () {
  11237. return this.day();
  11238. },
  11239. dd : function (format) {
  11240. return this.lang().weekdaysMin(this, format);
  11241. },
  11242. ddd : function (format) {
  11243. return this.lang().weekdaysShort(this, format);
  11244. },
  11245. dddd : function (format) {
  11246. return this.lang().weekdays(this, format);
  11247. },
  11248. w : function () {
  11249. return this.week();
  11250. },
  11251. W : function () {
  11252. return this.isoWeek();
  11253. },
  11254. YY : function () {
  11255. return leftZeroFill(this.year() % 100, 2);
  11256. },
  11257. YYYY : function () {
  11258. return leftZeroFill(this.year(), 4);
  11259. },
  11260. YYYYY : function () {
  11261. return leftZeroFill(this.year(), 5);
  11262. },
  11263. a : function () {
  11264. return this.lang().meridiem(this.hours(), this.minutes(), true);
  11265. },
  11266. A : function () {
  11267. return this.lang().meridiem(this.hours(), this.minutes(), false);
  11268. },
  11269. H : function () {
  11270. return this.hours();
  11271. },
  11272. h : function () {
  11273. return this.hours() % 12 || 12;
  11274. },
  11275. m : function () {
  11276. return this.minutes();
  11277. },
  11278. s : function () {
  11279. return this.seconds();
  11280. },
  11281. S : function () {
  11282. return ~~(this.milliseconds() / 100);
  11283. },
  11284. SS : function () {
  11285. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  11286. },
  11287. SSS : function () {
  11288. return leftZeroFill(this.milliseconds(), 3);
  11289. },
  11290. Z : function () {
  11291. var a = -this.zone(),
  11292. b = "+";
  11293. if (a < 0) {
  11294. a = -a;
  11295. b = "-";
  11296. }
  11297. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  11298. },
  11299. ZZ : function () {
  11300. var a = -this.zone(),
  11301. b = "+";
  11302. if (a < 0) {
  11303. a = -a;
  11304. b = "-";
  11305. }
  11306. return b + leftZeroFill(~~(10 * a / 6), 4);
  11307. },
  11308. X : function () {
  11309. return this.unix();
  11310. }
  11311. };
  11312. function padToken(func, count) {
  11313. return function (a) {
  11314. return leftZeroFill(func.call(this, a), count);
  11315. };
  11316. }
  11317. function ordinalizeToken(func) {
  11318. return function (a) {
  11319. return this.lang().ordinal(func.call(this, a));
  11320. };
  11321. }
  11322. while (ordinalizeTokens.length) {
  11323. i = ordinalizeTokens.pop();
  11324. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  11325. }
  11326. while (paddedTokens.length) {
  11327. i = paddedTokens.pop();
  11328. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  11329. }
  11330. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  11331. /************************************
  11332. Constructors
  11333. ************************************/
  11334. function Language() {
  11335. }
  11336. // Moment prototype object
  11337. function Moment(config) {
  11338. extend(this, config);
  11339. }
  11340. // Duration Constructor
  11341. function Duration(duration) {
  11342. var data = this._data = {},
  11343. years = duration.years || duration.year || duration.y || 0,
  11344. months = duration.months || duration.month || duration.M || 0,
  11345. weeks = duration.weeks || duration.week || duration.w || 0,
  11346. days = duration.days || duration.day || duration.d || 0,
  11347. hours = duration.hours || duration.hour || duration.h || 0,
  11348. minutes = duration.minutes || duration.minute || duration.m || 0,
  11349. seconds = duration.seconds || duration.second || duration.s || 0,
  11350. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  11351. // representation for dateAddRemove
  11352. this._milliseconds = milliseconds +
  11353. seconds * 1e3 + // 1000
  11354. minutes * 6e4 + // 1000 * 60
  11355. hours * 36e5; // 1000 * 60 * 60
  11356. // Because of dateAddRemove treats 24 hours as different from a
  11357. // day when working around DST, we need to store them separately
  11358. this._days = days +
  11359. weeks * 7;
  11360. // It is impossible translate months into days without knowing
  11361. // which months you are are talking about, so we have to store
  11362. // it separately.
  11363. this._months = months +
  11364. years * 12;
  11365. // The following code bubbles up values, see the tests for
  11366. // examples of what that means.
  11367. data.milliseconds = milliseconds % 1000;
  11368. seconds += absRound(milliseconds / 1000);
  11369. data.seconds = seconds % 60;
  11370. minutes += absRound(seconds / 60);
  11371. data.minutes = minutes % 60;
  11372. hours += absRound(minutes / 60);
  11373. data.hours = hours % 24;
  11374. days += absRound(hours / 24);
  11375. days += weeks * 7;
  11376. data.days = days % 30;
  11377. months += absRound(days / 30);
  11378. data.months = months % 12;
  11379. years += absRound(months / 12);
  11380. data.years = years;
  11381. }
  11382. /************************************
  11383. Helpers
  11384. ************************************/
  11385. function extend(a, b) {
  11386. for (var i in b) {
  11387. if (b.hasOwnProperty(i)) {
  11388. a[i] = b[i];
  11389. }
  11390. }
  11391. return a;
  11392. }
  11393. function absRound(number) {
  11394. if (number < 0) {
  11395. return Math.ceil(number);
  11396. } else {
  11397. return Math.floor(number);
  11398. }
  11399. }
  11400. // left zero fill a number
  11401. // see http://jsperf.com/left-zero-filling for performance comparison
  11402. function leftZeroFill(number, targetLength) {
  11403. var output = number + '';
  11404. while (output.length < targetLength) {
  11405. output = '0' + output;
  11406. }
  11407. return output;
  11408. }
  11409. // helper function for _.addTime and _.subtractTime
  11410. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  11411. var ms = duration._milliseconds,
  11412. d = duration._days,
  11413. M = duration._months,
  11414. currentDate;
  11415. if (ms) {
  11416. mom._d.setTime(+mom + ms * isAdding);
  11417. }
  11418. if (d) {
  11419. mom.date(mom.date() + d * isAdding);
  11420. }
  11421. if (M) {
  11422. currentDate = mom.date();
  11423. mom.date(1)
  11424. .month(mom.month() + M * isAdding)
  11425. .date(Math.min(currentDate, mom.daysInMonth()));
  11426. }
  11427. }
  11428. // check if is an array
  11429. function isArray(input) {
  11430. return Object.prototype.toString.call(input) === '[object Array]';
  11431. }
  11432. // compare two arrays, return the number of differences
  11433. function compareArrays(array1, array2) {
  11434. var len = Math.min(array1.length, array2.length),
  11435. lengthDiff = Math.abs(array1.length - array2.length),
  11436. diffs = 0,
  11437. i;
  11438. for (i = 0; i < len; i++) {
  11439. if (~~array1[i] !== ~~array2[i]) {
  11440. diffs++;
  11441. }
  11442. }
  11443. return diffs + lengthDiff;
  11444. }
  11445. /************************************
  11446. Languages
  11447. ************************************/
  11448. Language.prototype = {
  11449. set : function (config) {
  11450. var prop, i;
  11451. for (i in config) {
  11452. prop = config[i];
  11453. if (typeof prop === 'function') {
  11454. this[i] = prop;
  11455. } else {
  11456. this['_' + i] = prop;
  11457. }
  11458. }
  11459. },
  11460. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  11461. months : function (m) {
  11462. return this._months[m.month()];
  11463. },
  11464. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  11465. monthsShort : function (m) {
  11466. return this._monthsShort[m.month()];
  11467. },
  11468. monthsParse : function (monthName) {
  11469. var i, mom, regex, output;
  11470. if (!this._monthsParse) {
  11471. this._monthsParse = [];
  11472. }
  11473. for (i = 0; i < 12; i++) {
  11474. // make the regex if we don't have it already
  11475. if (!this._monthsParse[i]) {
  11476. mom = moment([2000, i]);
  11477. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  11478. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  11479. }
  11480. // test the regex
  11481. if (this._monthsParse[i].test(monthName)) {
  11482. return i;
  11483. }
  11484. }
  11485. },
  11486. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  11487. weekdays : function (m) {
  11488. return this._weekdays[m.day()];
  11489. },
  11490. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  11491. weekdaysShort : function (m) {
  11492. return this._weekdaysShort[m.day()];
  11493. },
  11494. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  11495. weekdaysMin : function (m) {
  11496. return this._weekdaysMin[m.day()];
  11497. },
  11498. _longDateFormat : {
  11499. LT : "h:mm A",
  11500. L : "MM/DD/YYYY",
  11501. LL : "MMMM D YYYY",
  11502. LLL : "MMMM D YYYY LT",
  11503. LLLL : "dddd, MMMM D YYYY LT"
  11504. },
  11505. longDateFormat : function (key) {
  11506. var output = this._longDateFormat[key];
  11507. if (!output && this._longDateFormat[key.toUpperCase()]) {
  11508. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  11509. return val.slice(1);
  11510. });
  11511. this._longDateFormat[key] = output;
  11512. }
  11513. return output;
  11514. },
  11515. meridiem : function (hours, minutes, isLower) {
  11516. if (hours > 11) {
  11517. return isLower ? 'pm' : 'PM';
  11518. } else {
  11519. return isLower ? 'am' : 'AM';
  11520. }
  11521. },
  11522. _calendar : {
  11523. sameDay : '[Today at] LT',
  11524. nextDay : '[Tomorrow at] LT',
  11525. nextWeek : 'dddd [at] LT',
  11526. lastDay : '[Yesterday at] LT',
  11527. lastWeek : '[last] dddd [at] LT',
  11528. sameElse : 'L'
  11529. },
  11530. calendar : function (key, mom) {
  11531. var output = this._calendar[key];
  11532. return typeof output === 'function' ? output.apply(mom) : output;
  11533. },
  11534. _relativeTime : {
  11535. future : "in %s",
  11536. past : "%s ago",
  11537. s : "a few seconds",
  11538. m : "a minute",
  11539. mm : "%d minutes",
  11540. h : "an hour",
  11541. hh : "%d hours",
  11542. d : "a day",
  11543. dd : "%d days",
  11544. M : "a month",
  11545. MM : "%d months",
  11546. y : "a year",
  11547. yy : "%d years"
  11548. },
  11549. relativeTime : function (number, withoutSuffix, string, isFuture) {
  11550. var output = this._relativeTime[string];
  11551. return (typeof output === 'function') ?
  11552. output(number, withoutSuffix, string, isFuture) :
  11553. output.replace(/%d/i, number);
  11554. },
  11555. pastFuture : function (diff, output) {
  11556. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  11557. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  11558. },
  11559. ordinal : function (number) {
  11560. return this._ordinal.replace("%d", number);
  11561. },
  11562. _ordinal : "%d",
  11563. preparse : function (string) {
  11564. return string;
  11565. },
  11566. postformat : function (string) {
  11567. return string;
  11568. },
  11569. week : function (mom) {
  11570. return weekOfYear(mom, this._week.dow, this._week.doy);
  11571. },
  11572. _week : {
  11573. dow : 0, // Sunday is the first day of the week.
  11574. doy : 6 // The week that contains Jan 1st is the first week of the year.
  11575. }
  11576. };
  11577. // Loads a language definition into the `languages` cache. The function
  11578. // takes a key and optionally values. If not in the browser and no values
  11579. // are provided, it will load the language file module. As a convenience,
  11580. // this function also returns the language values.
  11581. function loadLang(key, values) {
  11582. values.abbr = key;
  11583. if (!languages[key]) {
  11584. languages[key] = new Language();
  11585. }
  11586. languages[key].set(values);
  11587. return languages[key];
  11588. }
  11589. // Determines which language definition to use and returns it.
  11590. //
  11591. // With no parameters, it will return the global language. If you
  11592. // pass in a language key, such as 'en', it will return the
  11593. // definition for 'en', so long as 'en' has already been loaded using
  11594. // moment.lang.
  11595. function getLangDefinition(key) {
  11596. if (!key) {
  11597. return moment.fn._lang;
  11598. }
  11599. if (!languages[key] && hasModule) {
  11600. require('./lang/' + key);
  11601. }
  11602. return languages[key];
  11603. }
  11604. /************************************
  11605. Formatting
  11606. ************************************/
  11607. function removeFormattingTokens(input) {
  11608. if (input.match(/\[.*\]/)) {
  11609. return input.replace(/^\[|\]$/g, "");
  11610. }
  11611. return input.replace(/\\/g, "");
  11612. }
  11613. function makeFormatFunction(format) {
  11614. var array = format.match(formattingTokens), i, length;
  11615. for (i = 0, length = array.length; i < length; i++) {
  11616. if (formatTokenFunctions[array[i]]) {
  11617. array[i] = formatTokenFunctions[array[i]];
  11618. } else {
  11619. array[i] = removeFormattingTokens(array[i]);
  11620. }
  11621. }
  11622. return function (mom) {
  11623. var output = "";
  11624. for (i = 0; i < length; i++) {
  11625. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  11626. }
  11627. return output;
  11628. };
  11629. }
  11630. // format date using native date object
  11631. function formatMoment(m, format) {
  11632. var i = 5;
  11633. function replaceLongDateFormatTokens(input) {
  11634. return m.lang().longDateFormat(input) || input;
  11635. }
  11636. while (i-- && localFormattingTokens.test(format)) {
  11637. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  11638. }
  11639. if (!formatFunctions[format]) {
  11640. formatFunctions[format] = makeFormatFunction(format);
  11641. }
  11642. return formatFunctions[format](m);
  11643. }
  11644. /************************************
  11645. Parsing
  11646. ************************************/
  11647. // get the regex to find the next token
  11648. function getParseRegexForToken(token) {
  11649. switch (token) {
  11650. case 'DDDD':
  11651. return parseTokenThreeDigits;
  11652. case 'YYYY':
  11653. return parseTokenFourDigits;
  11654. case 'YYYYY':
  11655. return parseTokenSixDigits;
  11656. case 'S':
  11657. case 'SS':
  11658. case 'SSS':
  11659. case 'DDD':
  11660. return parseTokenOneToThreeDigits;
  11661. case 'MMM':
  11662. case 'MMMM':
  11663. case 'dd':
  11664. case 'ddd':
  11665. case 'dddd':
  11666. case 'a':
  11667. case 'A':
  11668. return parseTokenWord;
  11669. case 'X':
  11670. return parseTokenTimestampMs;
  11671. case 'Z':
  11672. case 'ZZ':
  11673. return parseTokenTimezone;
  11674. case 'T':
  11675. return parseTokenT;
  11676. case 'MM':
  11677. case 'DD':
  11678. case 'YY':
  11679. case 'HH':
  11680. case 'hh':
  11681. case 'mm':
  11682. case 'ss':
  11683. case 'M':
  11684. case 'D':
  11685. case 'd':
  11686. case 'H':
  11687. case 'h':
  11688. case 'm':
  11689. case 's':
  11690. return parseTokenOneOrTwoDigits;
  11691. default :
  11692. return new RegExp(token.replace('\\', ''));
  11693. }
  11694. }
  11695. // function to convert string input to date
  11696. function addTimeToArrayFromToken(token, input, config) {
  11697. var a, b,
  11698. datePartArray = config._a;
  11699. switch (token) {
  11700. // MONTH
  11701. case 'M' : // fall through to MM
  11702. case 'MM' :
  11703. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  11704. break;
  11705. case 'MMM' : // fall through to MMMM
  11706. case 'MMMM' :
  11707. a = getLangDefinition(config._l).monthsParse(input);
  11708. // if we didn't find a month name, mark the date as invalid.
  11709. if (a != null) {
  11710. datePartArray[1] = a;
  11711. } else {
  11712. config._isValid = false;
  11713. }
  11714. break;
  11715. // DAY OF MONTH
  11716. case 'D' : // fall through to DDDD
  11717. case 'DD' : // fall through to DDDD
  11718. case 'DDD' : // fall through to DDDD
  11719. case 'DDDD' :
  11720. if (input != null) {
  11721. datePartArray[2] = ~~input;
  11722. }
  11723. break;
  11724. // YEAR
  11725. case 'YY' :
  11726. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  11727. break;
  11728. case 'YYYY' :
  11729. case 'YYYYY' :
  11730. datePartArray[0] = ~~input;
  11731. break;
  11732. // AM / PM
  11733. case 'a' : // fall through to A
  11734. case 'A' :
  11735. config._isPm = ((input + '').toLowerCase() === 'pm');
  11736. break;
  11737. // 24 HOUR
  11738. case 'H' : // fall through to hh
  11739. case 'HH' : // fall through to hh
  11740. case 'h' : // fall through to hh
  11741. case 'hh' :
  11742. datePartArray[3] = ~~input;
  11743. break;
  11744. // MINUTE
  11745. case 'm' : // fall through to mm
  11746. case 'mm' :
  11747. datePartArray[4] = ~~input;
  11748. break;
  11749. // SECOND
  11750. case 's' : // fall through to ss
  11751. case 'ss' :
  11752. datePartArray[5] = ~~input;
  11753. break;
  11754. // MILLISECOND
  11755. case 'S' :
  11756. case 'SS' :
  11757. case 'SSS' :
  11758. datePartArray[6] = ~~ (('0.' + input) * 1000);
  11759. break;
  11760. // UNIX TIMESTAMP WITH MS
  11761. case 'X':
  11762. config._d = new Date(parseFloat(input) * 1000);
  11763. break;
  11764. // TIMEZONE
  11765. case 'Z' : // fall through to ZZ
  11766. case 'ZZ' :
  11767. config._useUTC = true;
  11768. a = (input + '').match(parseTimezoneChunker);
  11769. if (a && a[1]) {
  11770. config._tzh = ~~a[1];
  11771. }
  11772. if (a && a[2]) {
  11773. config._tzm = ~~a[2];
  11774. }
  11775. // reverse offsets
  11776. if (a && a[0] === '+') {
  11777. config._tzh = -config._tzh;
  11778. config._tzm = -config._tzm;
  11779. }
  11780. break;
  11781. }
  11782. // if the input is null, the date is not valid
  11783. if (input == null) {
  11784. config._isValid = false;
  11785. }
  11786. }
  11787. // convert an array to a date.
  11788. // the array should mirror the parameters below
  11789. // note: all values past the year are optional and will default to the lowest possible value.
  11790. // [year, month, day , hour, minute, second, millisecond]
  11791. function dateFromArray(config) {
  11792. var i, date, input = [];
  11793. if (config._d) {
  11794. return;
  11795. }
  11796. for (i = 0; i < 7; i++) {
  11797. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  11798. }
  11799. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  11800. input[3] += config._tzh || 0;
  11801. input[4] += config._tzm || 0;
  11802. date = new Date(0);
  11803. if (config._useUTC) {
  11804. date.setUTCFullYear(input[0], input[1], input[2]);
  11805. date.setUTCHours(input[3], input[4], input[5], input[6]);
  11806. } else {
  11807. date.setFullYear(input[0], input[1], input[2]);
  11808. date.setHours(input[3], input[4], input[5], input[6]);
  11809. }
  11810. config._d = date;
  11811. }
  11812. // date from string and format string
  11813. function makeDateFromStringAndFormat(config) {
  11814. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  11815. var tokens = config._f.match(formattingTokens),
  11816. string = config._i,
  11817. i, parsedInput;
  11818. config._a = [];
  11819. for (i = 0; i < tokens.length; i++) {
  11820. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  11821. if (parsedInput) {
  11822. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  11823. }
  11824. // don't parse if its not a known token
  11825. if (formatTokenFunctions[tokens[i]]) {
  11826. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  11827. }
  11828. }
  11829. // handle am pm
  11830. if (config._isPm && config._a[3] < 12) {
  11831. config._a[3] += 12;
  11832. }
  11833. // if is 12 am, change hours to 0
  11834. if (config._isPm === false && config._a[3] === 12) {
  11835. config._a[3] = 0;
  11836. }
  11837. // return
  11838. dateFromArray(config);
  11839. }
  11840. // date from string and array of format strings
  11841. function makeDateFromStringAndArray(config) {
  11842. var tempConfig,
  11843. tempMoment,
  11844. bestMoment,
  11845. scoreToBeat = 99,
  11846. i,
  11847. currentDate,
  11848. currentScore;
  11849. while (config._f.length) {
  11850. tempConfig = extend({}, config);
  11851. tempConfig._f = config._f.pop();
  11852. makeDateFromStringAndFormat(tempConfig);
  11853. tempMoment = new Moment(tempConfig);
  11854. if (tempMoment.isValid()) {
  11855. bestMoment = tempMoment;
  11856. break;
  11857. }
  11858. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  11859. if (currentScore < scoreToBeat) {
  11860. scoreToBeat = currentScore;
  11861. bestMoment = tempMoment;
  11862. }
  11863. }
  11864. extend(config, bestMoment);
  11865. }
  11866. // date from iso format
  11867. function makeDateFromString(config) {
  11868. var i,
  11869. string = config._i;
  11870. if (isoRegex.exec(string)) {
  11871. config._f = 'YYYY-MM-DDT';
  11872. for (i = 0; i < 4; i++) {
  11873. if (isoTimes[i][1].exec(string)) {
  11874. config._f += isoTimes[i][0];
  11875. break;
  11876. }
  11877. }
  11878. if (parseTokenTimezone.exec(string)) {
  11879. config._f += " Z";
  11880. }
  11881. makeDateFromStringAndFormat(config);
  11882. } else {
  11883. config._d = new Date(string);
  11884. }
  11885. }
  11886. function makeDateFromInput(config) {
  11887. var input = config._i,
  11888. matched = aspNetJsonRegex.exec(input);
  11889. if (input === undefined) {
  11890. config._d = new Date();
  11891. } else if (matched) {
  11892. config._d = new Date(+matched[1]);
  11893. } else if (typeof input === 'string') {
  11894. makeDateFromString(config);
  11895. } else if (isArray(input)) {
  11896. config._a = input.slice(0);
  11897. dateFromArray(config);
  11898. } else {
  11899. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  11900. }
  11901. }
  11902. /************************************
  11903. Relative Time
  11904. ************************************/
  11905. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  11906. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  11907. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  11908. }
  11909. function relativeTime(milliseconds, withoutSuffix, lang) {
  11910. var seconds = round(Math.abs(milliseconds) / 1000),
  11911. minutes = round(seconds / 60),
  11912. hours = round(minutes / 60),
  11913. days = round(hours / 24),
  11914. years = round(days / 365),
  11915. args = seconds < 45 && ['s', seconds] ||
  11916. minutes === 1 && ['m'] ||
  11917. minutes < 45 && ['mm', minutes] ||
  11918. hours === 1 && ['h'] ||
  11919. hours < 22 && ['hh', hours] ||
  11920. days === 1 && ['d'] ||
  11921. days <= 25 && ['dd', days] ||
  11922. days <= 45 && ['M'] ||
  11923. days < 345 && ['MM', round(days / 30)] ||
  11924. years === 1 && ['y'] || ['yy', years];
  11925. args[2] = withoutSuffix;
  11926. args[3] = milliseconds > 0;
  11927. args[4] = lang;
  11928. return substituteTimeAgo.apply({}, args);
  11929. }
  11930. /************************************
  11931. Week of Year
  11932. ************************************/
  11933. // firstDayOfWeek 0 = sun, 6 = sat
  11934. // the day of the week that starts the week
  11935. // (usually sunday or monday)
  11936. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  11937. // the first week is the week that contains the first
  11938. // of this day of the week
  11939. // (eg. ISO weeks use thursday (4))
  11940. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  11941. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  11942. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  11943. if (daysToDayOfWeek > end) {
  11944. daysToDayOfWeek -= 7;
  11945. }
  11946. if (daysToDayOfWeek < end - 7) {
  11947. daysToDayOfWeek += 7;
  11948. }
  11949. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  11950. }
  11951. /************************************
  11952. Top Level Functions
  11953. ************************************/
  11954. function makeMoment(config) {
  11955. var input = config._i,
  11956. format = config._f;
  11957. if (input === null || input === '') {
  11958. return null;
  11959. }
  11960. if (typeof input === 'string') {
  11961. config._i = input = getLangDefinition().preparse(input);
  11962. }
  11963. if (moment.isMoment(input)) {
  11964. config = extend({}, input);
  11965. config._d = new Date(+input._d);
  11966. } else if (format) {
  11967. if (isArray(format)) {
  11968. makeDateFromStringAndArray(config);
  11969. } else {
  11970. makeDateFromStringAndFormat(config);
  11971. }
  11972. } else {
  11973. makeDateFromInput(config);
  11974. }
  11975. return new Moment(config);
  11976. }
  11977. moment = function (input, format, lang) {
  11978. return makeMoment({
  11979. _i : input,
  11980. _f : format,
  11981. _l : lang,
  11982. _isUTC : false
  11983. });
  11984. };
  11985. // creating with utc
  11986. moment.utc = function (input, format, lang) {
  11987. return makeMoment({
  11988. _useUTC : true,
  11989. _isUTC : true,
  11990. _l : lang,
  11991. _i : input,
  11992. _f : format
  11993. });
  11994. };
  11995. // creating with unix timestamp (in seconds)
  11996. moment.unix = function (input) {
  11997. return moment(input * 1000);
  11998. };
  11999. // duration
  12000. moment.duration = function (input, key) {
  12001. var isDuration = moment.isDuration(input),
  12002. isNumber = (typeof input === 'number'),
  12003. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  12004. ret;
  12005. if (isNumber) {
  12006. if (key) {
  12007. duration[key] = input;
  12008. } else {
  12009. duration.milliseconds = input;
  12010. }
  12011. }
  12012. ret = new Duration(duration);
  12013. if (isDuration && input.hasOwnProperty('_lang')) {
  12014. ret._lang = input._lang;
  12015. }
  12016. return ret;
  12017. };
  12018. // version number
  12019. moment.version = VERSION;
  12020. // default format
  12021. moment.defaultFormat = isoFormat;
  12022. // This function will load languages and then set the global language. If
  12023. // no arguments are passed in, it will simply return the current global
  12024. // language key.
  12025. moment.lang = function (key, values) {
  12026. var i;
  12027. if (!key) {
  12028. return moment.fn._lang._abbr;
  12029. }
  12030. if (values) {
  12031. loadLang(key, values);
  12032. } else if (!languages[key]) {
  12033. getLangDefinition(key);
  12034. }
  12035. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  12036. };
  12037. // returns language data
  12038. moment.langData = function (key) {
  12039. if (key && key._lang && key._lang._abbr) {
  12040. key = key._lang._abbr;
  12041. }
  12042. return getLangDefinition(key);
  12043. };
  12044. // compare moment object
  12045. moment.isMoment = function (obj) {
  12046. return obj instanceof Moment;
  12047. };
  12048. // for typechecking Duration objects
  12049. moment.isDuration = function (obj) {
  12050. return obj instanceof Duration;
  12051. };
  12052. /************************************
  12053. Moment Prototype
  12054. ************************************/
  12055. moment.fn = Moment.prototype = {
  12056. clone : function () {
  12057. return moment(this);
  12058. },
  12059. valueOf : function () {
  12060. return +this._d;
  12061. },
  12062. unix : function () {
  12063. return Math.floor(+this._d / 1000);
  12064. },
  12065. toString : function () {
  12066. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  12067. },
  12068. toDate : function () {
  12069. return this._d;
  12070. },
  12071. toJSON : function () {
  12072. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  12073. },
  12074. toArray : function () {
  12075. var m = this;
  12076. return [
  12077. m.year(),
  12078. m.month(),
  12079. m.date(),
  12080. m.hours(),
  12081. m.minutes(),
  12082. m.seconds(),
  12083. m.milliseconds()
  12084. ];
  12085. },
  12086. isValid : function () {
  12087. if (this._isValid == null) {
  12088. if (this._a) {
  12089. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  12090. } else {
  12091. this._isValid = !isNaN(this._d.getTime());
  12092. }
  12093. }
  12094. return !!this._isValid;
  12095. },
  12096. utc : function () {
  12097. this._isUTC = true;
  12098. return this;
  12099. },
  12100. local : function () {
  12101. this._isUTC = false;
  12102. return this;
  12103. },
  12104. format : function (inputString) {
  12105. var output = formatMoment(this, inputString || moment.defaultFormat);
  12106. return this.lang().postformat(output);
  12107. },
  12108. add : function (input, val) {
  12109. var dur;
  12110. // switch args to support add('s', 1) and add(1, 's')
  12111. if (typeof input === 'string') {
  12112. dur = moment.duration(+val, input);
  12113. } else {
  12114. dur = moment.duration(input, val);
  12115. }
  12116. addOrSubtractDurationFromMoment(this, dur, 1);
  12117. return this;
  12118. },
  12119. subtract : function (input, val) {
  12120. var dur;
  12121. // switch args to support subtract('s', 1) and subtract(1, 's')
  12122. if (typeof input === 'string') {
  12123. dur = moment.duration(+val, input);
  12124. } else {
  12125. dur = moment.duration(input, val);
  12126. }
  12127. addOrSubtractDurationFromMoment(this, dur, -1);
  12128. return this;
  12129. },
  12130. diff : function (input, units, asFloat) {
  12131. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  12132. zoneDiff = (this.zone() - that.zone()) * 6e4,
  12133. diff, output;
  12134. if (units) {
  12135. // standardize on singular form
  12136. units = units.replace(/s$/, '');
  12137. }
  12138. if (units === 'year' || units === 'month') {
  12139. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  12140. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  12141. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  12142. if (units === 'year') {
  12143. output = output / 12;
  12144. }
  12145. } else {
  12146. diff = (this - that) - zoneDiff;
  12147. output = units === 'second' ? diff / 1e3 : // 1000
  12148. units === 'minute' ? diff / 6e4 : // 1000 * 60
  12149. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  12150. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  12151. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  12152. diff;
  12153. }
  12154. return asFloat ? output : absRound(output);
  12155. },
  12156. from : function (time, withoutSuffix) {
  12157. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  12158. },
  12159. fromNow : function (withoutSuffix) {
  12160. return this.from(moment(), withoutSuffix);
  12161. },
  12162. calendar : function () {
  12163. var diff = this.diff(moment().startOf('day'), 'days', true),
  12164. format = diff < -6 ? 'sameElse' :
  12165. diff < -1 ? 'lastWeek' :
  12166. diff < 0 ? 'lastDay' :
  12167. diff < 1 ? 'sameDay' :
  12168. diff < 2 ? 'nextDay' :
  12169. diff < 7 ? 'nextWeek' : 'sameElse';
  12170. return this.format(this.lang().calendar(format, this));
  12171. },
  12172. isLeapYear : function () {
  12173. var year = this.year();
  12174. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  12175. },
  12176. isDST : function () {
  12177. return (this.zone() < moment([this.year()]).zone() ||
  12178. this.zone() < moment([this.year(), 5]).zone());
  12179. },
  12180. day : function (input) {
  12181. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  12182. return input == null ? day :
  12183. this.add({ d : input - day });
  12184. },
  12185. startOf: function (units) {
  12186. units = units.replace(/s$/, '');
  12187. // the following switch intentionally omits break keywords
  12188. // to utilize falling through the cases.
  12189. switch (units) {
  12190. case 'year':
  12191. this.month(0);
  12192. /* falls through */
  12193. case 'month':
  12194. this.date(1);
  12195. /* falls through */
  12196. case 'week':
  12197. case 'day':
  12198. this.hours(0);
  12199. /* falls through */
  12200. case 'hour':
  12201. this.minutes(0);
  12202. /* falls through */
  12203. case 'minute':
  12204. this.seconds(0);
  12205. /* falls through */
  12206. case 'second':
  12207. this.milliseconds(0);
  12208. /* falls through */
  12209. }
  12210. // weeks are a special case
  12211. if (units === 'week') {
  12212. this.day(0);
  12213. }
  12214. return this;
  12215. },
  12216. endOf: function (units) {
  12217. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  12218. },
  12219. isAfter: function (input, units) {
  12220. units = typeof units !== 'undefined' ? units : 'millisecond';
  12221. return +this.clone().startOf(units) > +moment(input).startOf(units);
  12222. },
  12223. isBefore: function (input, units) {
  12224. units = typeof units !== 'undefined' ? units : 'millisecond';
  12225. return +this.clone().startOf(units) < +moment(input).startOf(units);
  12226. },
  12227. isSame: function (input, units) {
  12228. units = typeof units !== 'undefined' ? units : 'millisecond';
  12229. return +this.clone().startOf(units) === +moment(input).startOf(units);
  12230. },
  12231. zone : function () {
  12232. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  12233. },
  12234. daysInMonth : function () {
  12235. return moment.utc([this.year(), this.month() + 1, 0]).date();
  12236. },
  12237. dayOfYear : function (input) {
  12238. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  12239. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  12240. },
  12241. isoWeek : function (input) {
  12242. var week = weekOfYear(this, 1, 4);
  12243. return input == null ? week : this.add("d", (input - week) * 7);
  12244. },
  12245. week : function (input) {
  12246. var week = this.lang().week(this);
  12247. return input == null ? week : this.add("d", (input - week) * 7);
  12248. },
  12249. // If passed a language key, it will set the language for this
  12250. // instance. Otherwise, it will return the language configuration
  12251. // variables for this instance.
  12252. lang : function (key) {
  12253. if (key === undefined) {
  12254. return this._lang;
  12255. } else {
  12256. this._lang = getLangDefinition(key);
  12257. return this;
  12258. }
  12259. }
  12260. };
  12261. // helper for adding shortcuts
  12262. function makeGetterAndSetter(name, key) {
  12263. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  12264. var utc = this._isUTC ? 'UTC' : '';
  12265. if (input != null) {
  12266. this._d['set' + utc + key](input);
  12267. return this;
  12268. } else {
  12269. return this._d['get' + utc + key]();
  12270. }
  12271. };
  12272. }
  12273. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  12274. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  12275. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  12276. }
  12277. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  12278. makeGetterAndSetter('year', 'FullYear');
  12279. // add plural methods
  12280. moment.fn.days = moment.fn.day;
  12281. moment.fn.weeks = moment.fn.week;
  12282. moment.fn.isoWeeks = moment.fn.isoWeek;
  12283. /************************************
  12284. Duration Prototype
  12285. ************************************/
  12286. moment.duration.fn = Duration.prototype = {
  12287. weeks : function () {
  12288. return absRound(this.days() / 7);
  12289. },
  12290. valueOf : function () {
  12291. return this._milliseconds +
  12292. this._days * 864e5 +
  12293. this._months * 2592e6;
  12294. },
  12295. humanize : function (withSuffix) {
  12296. var difference = +this,
  12297. output = relativeTime(difference, !withSuffix, this.lang());
  12298. if (withSuffix) {
  12299. output = this.lang().pastFuture(difference, output);
  12300. }
  12301. return this.lang().postformat(output);
  12302. },
  12303. lang : moment.fn.lang
  12304. };
  12305. function makeDurationGetter(name) {
  12306. moment.duration.fn[name] = function () {
  12307. return this._data[name];
  12308. };
  12309. }
  12310. function makeDurationAsGetter(name, factor) {
  12311. moment.duration.fn['as' + name] = function () {
  12312. return +this / factor;
  12313. };
  12314. }
  12315. for (i in unitMillisecondFactors) {
  12316. if (unitMillisecondFactors.hasOwnProperty(i)) {
  12317. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  12318. makeDurationGetter(i.toLowerCase());
  12319. }
  12320. }
  12321. makeDurationAsGetter('Weeks', 6048e5);
  12322. /************************************
  12323. Default Lang
  12324. ************************************/
  12325. // Set default language, other languages will inherit from English.
  12326. moment.lang('en', {
  12327. ordinal : function (number) {
  12328. var b = number % 10,
  12329. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  12330. (b === 1) ? 'st' :
  12331. (b === 2) ? 'nd' :
  12332. (b === 3) ? 'rd' : 'th';
  12333. return number + output;
  12334. }
  12335. });
  12336. /************************************
  12337. Exposing Moment
  12338. ************************************/
  12339. // CommonJS module is defined
  12340. if (hasModule) {
  12341. module.exports = moment;
  12342. }
  12343. /*global ender:false */
  12344. if (typeof ender === 'undefined') {
  12345. // here, `this` means `window` in the browser, or `global` on the server
  12346. // add `moment` as a global object via a string identifier,
  12347. // for Closure Compiler "advanced" mode
  12348. this['moment'] = moment;
  12349. }
  12350. /*global define:false */
  12351. if (typeof define === "function" && define.amd) {
  12352. define("moment", [], function () {
  12353. return moment;
  12354. });
  12355. }
  12356. }).call(this);
  12357. })()
  12358. },{}]},{},[1])(1)
  12359. });
  12360. ;