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.

12378 lines
372 KiB

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.9
  8. * @date 2013-06-07
  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. * An event bus can be used to emit events, and to subscribe to events
  922. * @constructor EventBus
  923. */
  924. function EventBus() {
  925. this.subscriptions = [];
  926. }
  927. /**
  928. * Subscribe to an event
  929. * @param {String | RegExp} event The event can be a regular expression, or
  930. * a string with wildcards, like 'server.*'.
  931. * @param {function} callback. Callback are called with three parameters:
  932. * {String} event, {*} [data], {*} [source]
  933. * @param {*} [target]
  934. * @returns {String} id A subscription id
  935. */
  936. EventBus.prototype.on = function (event, callback, target) {
  937. var regexp = (event instanceof RegExp) ?
  938. event :
  939. new RegExp(event.replace('*', '\\w+'));
  940. var subscription = {
  941. id: util.randomUUID(),
  942. event: event,
  943. regexp: regexp,
  944. callback: (typeof callback === 'function') ? callback : null,
  945. target: target
  946. };
  947. this.subscriptions.push(subscription);
  948. return subscription.id;
  949. };
  950. /**
  951. * Unsubscribe from an event
  952. * @param {String | Object} filter Filter for subscriptions to be removed
  953. * Filter can be a string containing a
  954. * subscription id, or an object containing
  955. * one or more of the fields id, event,
  956. * callback, and target.
  957. */
  958. EventBus.prototype.off = function (filter) {
  959. var i = 0;
  960. while (i < this.subscriptions.length) {
  961. var subscription = this.subscriptions[i];
  962. var match = true;
  963. if (filter instanceof Object) {
  964. // filter is an object. All fields must match
  965. for (var prop in filter) {
  966. if (filter.hasOwnProperty(prop)) {
  967. if (filter[prop] !== subscription[prop]) {
  968. match = false;
  969. }
  970. }
  971. }
  972. }
  973. else {
  974. // filter is a string, filter on id
  975. match = (subscription.id == filter);
  976. }
  977. if (match) {
  978. this.subscriptions.splice(i, 1);
  979. }
  980. else {
  981. i++;
  982. }
  983. }
  984. };
  985. /**
  986. * Emit an event
  987. * @param {String} event
  988. * @param {*} [data]
  989. * @param {*} [source]
  990. */
  991. EventBus.prototype.emit = function (event, data, source) {
  992. for (var i =0; i < this.subscriptions.length; i++) {
  993. var subscription = this.subscriptions[i];
  994. if (subscription.regexp.test(event)) {
  995. if (subscription.callback) {
  996. subscription.callback(event, data, source);
  997. }
  998. }
  999. }
  1000. };
  1001. /**
  1002. * DataSet
  1003. *
  1004. * Usage:
  1005. * var dataSet = new DataSet({
  1006. * fieldId: '_id',
  1007. * fieldTypes: {
  1008. * // ...
  1009. * }
  1010. * });
  1011. *
  1012. * dataSet.add(item);
  1013. * dataSet.add(data);
  1014. * dataSet.update(item);
  1015. * dataSet.update(data);
  1016. * dataSet.remove(id);
  1017. * dataSet.remove(ids);
  1018. * var data = dataSet.get();
  1019. * var data = dataSet.get(id);
  1020. * var data = dataSet.get(ids);
  1021. * var data = dataSet.get(ids, options, data);
  1022. * dataSet.clear();
  1023. *
  1024. * A data set can:
  1025. * - add/remove/update data
  1026. * - gives triggers upon changes in the data
  1027. * - can import/export data in various data formats
  1028. *
  1029. * @param {Object} [options] Available options:
  1030. * {String} fieldId Field name of the id in the
  1031. * items, 'id' by default.
  1032. * {Object.<String, String} fieldTypes
  1033. * A map with field names as key,
  1034. * and the field type as value.
  1035. * @constructor DataSet
  1036. */
  1037. function DataSet (options) {
  1038. this.id = util.randomUUID();
  1039. this.options = options || {};
  1040. this.data = {}; // map with data indexed by id
  1041. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1042. this.fieldTypes = {}; // field types by field name
  1043. if (this.options.fieldTypes) {
  1044. for (var field in this.options.fieldTypes) {
  1045. if (this.options.fieldTypes.hasOwnProperty(field)) {
  1046. var value = this.options.fieldTypes[field];
  1047. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1048. this.fieldTypes[field] = 'Date';
  1049. }
  1050. else {
  1051. this.fieldTypes[field] = value;
  1052. }
  1053. }
  1054. }
  1055. }
  1056. // event subscribers
  1057. this.subscribers = {};
  1058. this.internalIds = {}; // internally generated id's
  1059. }
  1060. /**
  1061. * Subscribe to an event, add an event listener
  1062. * @param {String} event Event name. Available events: 'put', 'update',
  1063. * 'remove'
  1064. * @param {function} callback Callback method. Called with three parameters:
  1065. * {String} event
  1066. * {Object | null} params
  1067. * {String} senderId
  1068. * @param {String} [id] Optional id for the sender, used to filter
  1069. * events triggered by the sender itself.
  1070. */
  1071. DataSet.prototype.subscribe = function (event, callback, id) {
  1072. var subscribers = this.subscribers[event];
  1073. if (!subscribers) {
  1074. subscribers = [];
  1075. this.subscribers[event] = subscribers;
  1076. }
  1077. subscribers.push({
  1078. id: id ? String(id) : null,
  1079. callback: callback
  1080. });
  1081. };
  1082. /**
  1083. * Unsubscribe from an event, remove an event listener
  1084. * @param {String} event
  1085. * @param {function} callback
  1086. */
  1087. DataSet.prototype.unsubscribe = function (event, callback) {
  1088. var subscribers = this.subscribers[event];
  1089. if (subscribers) {
  1090. this.subscribers[event] = subscribers.filter(function (listener) {
  1091. return (listener.callback != callback);
  1092. });
  1093. }
  1094. };
  1095. /**
  1096. * Trigger an event
  1097. * @param {String} event
  1098. * @param {Object | null} params
  1099. * @param {String} [senderId] Optional id of the sender.
  1100. * @private
  1101. */
  1102. DataSet.prototype._trigger = function (event, params, senderId) {
  1103. if (event == '*') {
  1104. throw new Error('Cannot trigger event *');
  1105. }
  1106. var subscribers = [];
  1107. if (event in this.subscribers) {
  1108. subscribers = subscribers.concat(this.subscribers[event]);
  1109. }
  1110. if ('*' in this.subscribers) {
  1111. subscribers = subscribers.concat(this.subscribers['*']);
  1112. }
  1113. for (var i = 0; i < subscribers.length; i++) {
  1114. var subscriber = subscribers[i];
  1115. if (subscriber.callback) {
  1116. subscriber.callback(event, params, senderId || null);
  1117. }
  1118. }
  1119. };
  1120. /**
  1121. * Add data.
  1122. * Adding an item will fail when there already is an item with the same id.
  1123. * @param {Object | Array | DataTable} data
  1124. * @param {String} [senderId] Optional sender id
  1125. */
  1126. DataSet.prototype.add = function (data, senderId) {
  1127. var addedItems = [],
  1128. id,
  1129. me = this;
  1130. if (data instanceof Array) {
  1131. // Array
  1132. for (var i = 0, len = data.length; i < len; i++) {
  1133. id = me._addItem(data[i]);
  1134. addedItems.push(id);
  1135. }
  1136. }
  1137. else if (util.isDataTable(data)) {
  1138. // Google DataTable
  1139. var columns = this._getColumnNames(data);
  1140. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1141. var item = {};
  1142. for (var col = 0, cols = columns.length; col < cols; col++) {
  1143. var field = columns[col];
  1144. item[field] = data.getValue(row, col);
  1145. }
  1146. id = me._addItem(item);
  1147. addedItems.push(id);
  1148. }
  1149. }
  1150. else if (data instanceof Object) {
  1151. // Single item
  1152. id = me._addItem(data);
  1153. addedItems.push(id);
  1154. }
  1155. else {
  1156. throw new Error('Unknown dataType');
  1157. }
  1158. if (addedItems.length) {
  1159. this._trigger('add', {items: addedItems}, senderId);
  1160. }
  1161. };
  1162. /**
  1163. * Update existing items. When an item does not exist, it will be created
  1164. * @param {Object | Array | DataTable} data
  1165. * @param {String} [senderId] Optional sender id
  1166. */
  1167. DataSet.prototype.update = function (data, senderId) {
  1168. var addedItems = [],
  1169. updatedItems = [],
  1170. me = this,
  1171. fieldId = me.fieldId;
  1172. var addOrUpdate = function (item) {
  1173. var id = item[fieldId];
  1174. if (me.data[id]) {
  1175. // update item
  1176. id = me._updateItem(item);
  1177. updatedItems.push(id);
  1178. }
  1179. else {
  1180. // add new item
  1181. id = me._addItem(item);
  1182. addedItems.push(id);
  1183. }
  1184. };
  1185. if (data instanceof Array) {
  1186. // Array
  1187. for (var i = 0, len = data.length; i < len; i++) {
  1188. addOrUpdate(data[i]);
  1189. }
  1190. }
  1191. else if (util.isDataTable(data)) {
  1192. // Google DataTable
  1193. var columns = this._getColumnNames(data);
  1194. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1195. var item = {};
  1196. for (var col = 0, cols = columns.length; col < cols; col++) {
  1197. var field = columns[col];
  1198. item[field] = data.getValue(row, col);
  1199. }
  1200. addOrUpdate(item);
  1201. }
  1202. }
  1203. else if (data instanceof Object) {
  1204. // Single item
  1205. addOrUpdate(data);
  1206. }
  1207. else {
  1208. throw new Error('Unknown dataType');
  1209. }
  1210. if (addedItems.length) {
  1211. this._trigger('add', {items: addedItems}, senderId);
  1212. }
  1213. if (updatedItems.length) {
  1214. this._trigger('update', {items: updatedItems}, senderId);
  1215. }
  1216. };
  1217. /**
  1218. * Get a data item or multiple items.
  1219. *
  1220. * Usage:
  1221. *
  1222. * get()
  1223. * get(options: Object)
  1224. * get(options: Object, data: Array | DataTable)
  1225. *
  1226. * get(id: Number | String)
  1227. * get(id: Number | String, options: Object)
  1228. * get(id: Number | String, options: Object, data: Array | DataTable)
  1229. *
  1230. * get(ids: Number[] | String[])
  1231. * get(ids: Number[] | String[], options: Object)
  1232. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1233. *
  1234. * Where:
  1235. *
  1236. * {Number | String} id The id of an item
  1237. * {Number[] | String{}} ids An array with ids of items
  1238. * {Object} options An Object with options. Available options:
  1239. * {String} [type] Type of data to be returned. Can
  1240. * be 'DataTable' or 'Array' (default)
  1241. * {Object.<String, String>} [fieldTypes]
  1242. * {String[]} [fields] field names to be returned
  1243. * {function} [filter] filter items
  1244. * {String | function} [order] Order the items by
  1245. * a field name or custom sort function.
  1246. * {Array | DataTable} [data] If provided, items will be appended to this
  1247. * array or table. Required in case of Google
  1248. * DataTable.
  1249. *
  1250. * @throws Error
  1251. */
  1252. DataSet.prototype.get = function (args) {
  1253. var me = this;
  1254. // parse the arguments
  1255. var id, ids, options, data;
  1256. var firstType = util.getType(arguments[0]);
  1257. if (firstType == 'String' || firstType == 'Number') {
  1258. // get(id [, options] [, data])
  1259. id = arguments[0];
  1260. options = arguments[1];
  1261. data = arguments[2];
  1262. }
  1263. else if (firstType == 'Array') {
  1264. // get(ids [, options] [, data])
  1265. ids = arguments[0];
  1266. options = arguments[1];
  1267. data = arguments[2];
  1268. }
  1269. else {
  1270. // get([, options] [, data])
  1271. options = arguments[0];
  1272. data = arguments[1];
  1273. }
  1274. // determine the return type
  1275. var type;
  1276. if (options && options.type) {
  1277. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1278. if (data && (type != util.getType(data))) {
  1279. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1280. 'does not correspond with specified options.type (' + options.type + ')');
  1281. }
  1282. if (type == 'DataTable' && !util.isDataTable(data)) {
  1283. throw new Error('Parameter "data" must be a DataTable ' +
  1284. 'when options.type is "DataTable"');
  1285. }
  1286. }
  1287. else if (data) {
  1288. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1289. }
  1290. else {
  1291. type = 'Array';
  1292. }
  1293. // build options
  1294. var fieldTypes = options && options.fieldTypes || this.options.fieldTypes;
  1295. var filter = options && options.filter;
  1296. var items = [], item, itemId, i, len;
  1297. // cast items
  1298. if (id != undefined) {
  1299. // return a single item
  1300. item = me._getItem(id, fieldTypes);
  1301. if (filter && !filter(item)) {
  1302. item = null;
  1303. }
  1304. }
  1305. else if (ids != undefined) {
  1306. // return a subset of items
  1307. for (i = 0, len = ids.length; i < len; i++) {
  1308. item = me._getItem(ids[i], fieldTypes);
  1309. if (!filter || filter(item)) {
  1310. items.push(item);
  1311. }
  1312. }
  1313. }
  1314. else {
  1315. // return all items
  1316. for (itemId in this.data) {
  1317. if (this.data.hasOwnProperty(itemId)) {
  1318. item = me._getItem(itemId, fieldTypes);
  1319. if (!filter || filter(item)) {
  1320. items.push(item);
  1321. }
  1322. }
  1323. }
  1324. }
  1325. // order the results
  1326. if (options && options.order && id == undefined) {
  1327. this._sort(items, options.order);
  1328. }
  1329. // filter fields of the items
  1330. if (options && options.fields) {
  1331. var fields = options.fields;
  1332. if (id != undefined) {
  1333. item = this._filterFields(item, fields);
  1334. }
  1335. else {
  1336. for (i = 0, len = items.length; i < len; i++) {
  1337. items[i] = this._filterFields(items[i], fields);
  1338. }
  1339. }
  1340. }
  1341. // return the results
  1342. if (type == 'DataTable') {
  1343. var columns = this._getColumnNames(data);
  1344. if (id != undefined) {
  1345. // append a single item to the data table
  1346. me._appendRow(data, columns, item);
  1347. }
  1348. else {
  1349. // copy the items to the provided data table
  1350. for (i = 0, len = items.length; i < len; i++) {
  1351. me._appendRow(data, columns, items[i]);
  1352. }
  1353. }
  1354. return data;
  1355. }
  1356. else {
  1357. // return an array
  1358. if (id != undefined) {
  1359. // a single item
  1360. return item;
  1361. }
  1362. else {
  1363. // multiple items
  1364. if (data) {
  1365. // copy the items to the provided array
  1366. for (i = 0, len = items.length; i < len; i++) {
  1367. data.push(items[i]);
  1368. }
  1369. return data;
  1370. }
  1371. else {
  1372. // just return our array
  1373. return items;
  1374. }
  1375. }
  1376. }
  1377. };
  1378. /**
  1379. * Get ids of all items or from a filtered set of items.
  1380. * @param {Object} [options] An Object with options. Available options:
  1381. * {function} [filter] filter items
  1382. * {String | function} [order] Order the items by
  1383. * a field name or custom sort function.
  1384. * @return {Array} ids
  1385. */
  1386. DataSet.prototype.getIds = function (options) {
  1387. var data = this.data,
  1388. filter = options && options.filter,
  1389. order = options && options.order,
  1390. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1391. i,
  1392. len,
  1393. id,
  1394. item,
  1395. items,
  1396. ids = [];
  1397. if (filter) {
  1398. // get filtered items
  1399. if (order) {
  1400. // create ordered list
  1401. items = [];
  1402. for (id in data) {
  1403. if (data.hasOwnProperty(id)) {
  1404. item = this._getItem(id, fieldTypes);
  1405. if (filter(item)) {
  1406. items.push(item);
  1407. }
  1408. }
  1409. }
  1410. this._sort(items, order);
  1411. for (i = 0, len = items.length; i < len; i++) {
  1412. ids[i] = items[i][this.fieldId];
  1413. }
  1414. }
  1415. else {
  1416. // create unordered list
  1417. for (id in data) {
  1418. if (data.hasOwnProperty(id)) {
  1419. item = this._getItem(id, fieldTypes);
  1420. if (filter(item)) {
  1421. ids.push(item[this.fieldId]);
  1422. }
  1423. }
  1424. }
  1425. }
  1426. }
  1427. else {
  1428. // get all items
  1429. if (order) {
  1430. // create an ordered list
  1431. items = [];
  1432. for (id in data) {
  1433. if (data.hasOwnProperty(id)) {
  1434. items.push(data[id]);
  1435. }
  1436. }
  1437. this._sort(items, order);
  1438. for (i = 0, len = items.length; i < len; i++) {
  1439. ids[i] = items[i][this.fieldId];
  1440. }
  1441. }
  1442. else {
  1443. // create unordered list
  1444. for (id in data) {
  1445. if (data.hasOwnProperty(id)) {
  1446. item = data[id];
  1447. ids.push(item[this.fieldId]);
  1448. }
  1449. }
  1450. }
  1451. }
  1452. return ids;
  1453. };
  1454. /**
  1455. * Execute a callback function for every item in the dataset.
  1456. * The order of the items is not determined.
  1457. * @param {function} callback
  1458. * @param {Object} [options] Available options:
  1459. * {Object.<String, String>} [fieldTypes]
  1460. * {String[]} [fields] filter fields
  1461. * {function} [filter] filter items
  1462. * {String | function} [order] Order the items by
  1463. * a field name or custom sort function.
  1464. */
  1465. DataSet.prototype.forEach = function (callback, options) {
  1466. var filter = options && options.filter,
  1467. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1468. data = this.data,
  1469. item,
  1470. id;
  1471. if (options && options.order) {
  1472. // execute forEach on ordered list
  1473. var items = this.get(options);
  1474. for (var i = 0, len = items.length; i < len; i++) {
  1475. item = items[i];
  1476. id = item[this.fieldId];
  1477. callback(item, id);
  1478. }
  1479. }
  1480. else {
  1481. // unordered
  1482. for (id in data) {
  1483. if (data.hasOwnProperty(id)) {
  1484. item = this._getItem(id, fieldTypes);
  1485. if (!filter || filter(item)) {
  1486. callback(item, id);
  1487. }
  1488. }
  1489. }
  1490. }
  1491. };
  1492. /**
  1493. * Map every item in the dataset.
  1494. * @param {function} callback
  1495. * @param {Object} [options] Available options:
  1496. * {Object.<String, String>} [fieldTypes]
  1497. * {String[]} [fields] filter fields
  1498. * {function} [filter] filter items
  1499. * {String | function} [order] Order the items by
  1500. * a field name or custom sort function.
  1501. * @return {Object[]} mappedItems
  1502. */
  1503. DataSet.prototype.map = function (callback, options) {
  1504. var filter = options && options.filter,
  1505. fieldTypes = options && options.fieldTypes || this.options.fieldTypes,
  1506. mappedItems = [],
  1507. data = this.data,
  1508. item;
  1509. // cast and filter items
  1510. for (var id in data) {
  1511. if (data.hasOwnProperty(id)) {
  1512. item = this._getItem(id, fieldTypes);
  1513. if (!filter || filter(item)) {
  1514. mappedItems.push(callback(item, id));
  1515. }
  1516. }
  1517. }
  1518. // order items
  1519. if (options && options.order) {
  1520. this._sort(mappedItems, options.order);
  1521. }
  1522. return mappedItems;
  1523. };
  1524. /**
  1525. * Filter the fields of an item
  1526. * @param {Object} item
  1527. * @param {String[]} fields Field names
  1528. * @return {Object} filteredItem
  1529. * @private
  1530. */
  1531. DataSet.prototype._filterFields = function (item, fields) {
  1532. var filteredItem = {};
  1533. for (var field in item) {
  1534. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1535. filteredItem[field] = item[field];
  1536. }
  1537. }
  1538. return filteredItem;
  1539. };
  1540. /**
  1541. * Sort the provided array with items
  1542. * @param {Object[]} items
  1543. * @param {String | function} order A field name or custom sort function.
  1544. * @private
  1545. */
  1546. DataSet.prototype._sort = function (items, order) {
  1547. if (util.isString(order)) {
  1548. // order by provided field name
  1549. var name = order; // field name
  1550. items.sort(function (a, b) {
  1551. var av = a[name];
  1552. var bv = b[name];
  1553. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1554. });
  1555. }
  1556. else if (typeof order === 'function') {
  1557. // order by sort function
  1558. items.sort(order);
  1559. }
  1560. // TODO: extend order by an Object {field:String, direction:String}
  1561. // where direction can be 'asc' or 'desc'
  1562. else {
  1563. throw new TypeError('Order must be a function or a string');
  1564. }
  1565. };
  1566. /**
  1567. * Remove an object by pointer or by id
  1568. * @param {String | Number | Object | Array} id Object or id, or an array with
  1569. * objects or ids to be removed
  1570. * @param {String} [senderId] Optional sender id
  1571. */
  1572. DataSet.prototype.remove = function (id, senderId) {
  1573. var removedItems = [],
  1574. i, len;
  1575. if (util.isNumber(id) || util.isString(id)) {
  1576. delete this.data[id];
  1577. delete this.internalIds[id];
  1578. removedItems.push(id);
  1579. }
  1580. else if (id instanceof Array) {
  1581. for (i = 0, len = id.length; i < len; i++) {
  1582. this.remove(id[i]);
  1583. }
  1584. removedItems = items.concat(id);
  1585. }
  1586. else if (id instanceof Object) {
  1587. // search for the object
  1588. for (i in this.data) {
  1589. if (this.data.hasOwnProperty(i)) {
  1590. if (this.data[i] == id) {
  1591. delete this.data[i];
  1592. delete this.internalIds[i];
  1593. removedItems.push(i);
  1594. }
  1595. }
  1596. }
  1597. }
  1598. if (removedItems.length) {
  1599. this._trigger('remove', {items: removedItems}, senderId);
  1600. }
  1601. };
  1602. /**
  1603. * Clear the data
  1604. * @param {String} [senderId] Optional sender id
  1605. */
  1606. DataSet.prototype.clear = function (senderId) {
  1607. var ids = Object.keys(this.data);
  1608. this.data = {};
  1609. this.internalIds = {};
  1610. this._trigger('remove', {items: ids}, senderId);
  1611. };
  1612. /**
  1613. * Find the item with maximum value of a specified field
  1614. * @param {String} field
  1615. * @return {Object | null} item Item containing max value, or null if no items
  1616. */
  1617. DataSet.prototype.max = function (field) {
  1618. var data = this.data,
  1619. max = null,
  1620. maxField = null;
  1621. for (var id in data) {
  1622. if (data.hasOwnProperty(id)) {
  1623. var item = data[id];
  1624. var itemField = item[field];
  1625. if (itemField != null && (!max || itemField > maxField)) {
  1626. max = item;
  1627. maxField = itemField;
  1628. }
  1629. }
  1630. }
  1631. return max;
  1632. };
  1633. /**
  1634. * Find the item with minimum value of a specified field
  1635. * @param {String} field
  1636. * @return {Object | null} item Item containing max value, or null if no items
  1637. */
  1638. DataSet.prototype.min = function (field) {
  1639. var data = this.data,
  1640. min = null,
  1641. minField = null;
  1642. for (var id in data) {
  1643. if (data.hasOwnProperty(id)) {
  1644. var item = data[id];
  1645. var itemField = item[field];
  1646. if (itemField != null && (!min || itemField < minField)) {
  1647. min = item;
  1648. minField = itemField;
  1649. }
  1650. }
  1651. }
  1652. return min;
  1653. };
  1654. /**
  1655. * Find all distinct values of a specified field
  1656. * @param {String} field
  1657. * @return {Array} values Array containing all distinct values. If the data
  1658. * items do not contain the specified field, an array
  1659. * containing a single value undefined is returned.
  1660. * The returned array is unordered.
  1661. */
  1662. DataSet.prototype.distinct = function (field) {
  1663. var data = this.data,
  1664. values = [],
  1665. fieldType = this.options.fieldTypes[field],
  1666. count = 0;
  1667. for (var prop in data) {
  1668. if (data.hasOwnProperty(prop)) {
  1669. var item = data[prop];
  1670. var value = util.cast(item[field], fieldType);
  1671. var exists = false;
  1672. for (var i = 0; i < count; i++) {
  1673. if (values[i] == value) {
  1674. exists = true;
  1675. break;
  1676. }
  1677. }
  1678. if (!exists) {
  1679. values[count] = value;
  1680. count++;
  1681. }
  1682. }
  1683. }
  1684. return values;
  1685. };
  1686. /**
  1687. * Add a single item. Will fail when an item with the same id already exists.
  1688. * @param {Object} item
  1689. * @return {String} id
  1690. * @private
  1691. */
  1692. DataSet.prototype._addItem = function (item) {
  1693. var id = item[this.fieldId];
  1694. if (id != undefined) {
  1695. // check whether this id is already taken
  1696. if (this.data[id]) {
  1697. // item already exists
  1698. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1699. }
  1700. }
  1701. else {
  1702. // generate an id
  1703. id = util.randomUUID();
  1704. item[this.fieldId] = id;
  1705. this.internalIds[id] = item;
  1706. }
  1707. var d = {};
  1708. for (var field in item) {
  1709. if (item.hasOwnProperty(field)) {
  1710. var type = this.fieldTypes[field]; // type may be undefined
  1711. d[field] = util.cast(item[field], type);
  1712. }
  1713. }
  1714. this.data[id] = d;
  1715. return id;
  1716. };
  1717. /**
  1718. * Get an item. Fields can be casted to a specific type
  1719. * @param {String} id
  1720. * @param {Object.<String, String>} [fieldTypes] Cast field types
  1721. * @return {Object | null} item
  1722. * @private
  1723. */
  1724. DataSet.prototype._getItem = function (id, fieldTypes) {
  1725. var field, value;
  1726. // get the item from the dataset
  1727. var raw = this.data[id];
  1728. if (!raw) {
  1729. return null;
  1730. }
  1731. // cast the items field types
  1732. var casted = {},
  1733. fieldId = this.fieldId,
  1734. internalIds = this.internalIds;
  1735. if (fieldTypes) {
  1736. for (field in raw) {
  1737. if (raw.hasOwnProperty(field)) {
  1738. value = raw[field];
  1739. // output all fields, except internal ids
  1740. if ((field != fieldId) || !(value in internalIds)) {
  1741. casted[field] = util.cast(value, fieldTypes[field]);
  1742. }
  1743. }
  1744. }
  1745. }
  1746. else {
  1747. // no field types specified, no casting needed
  1748. for (field in raw) {
  1749. if (raw.hasOwnProperty(field)) {
  1750. value = raw[field];
  1751. // output all fields, except internal ids
  1752. if ((field != fieldId) || !(value in internalIds)) {
  1753. casted[field] = value;
  1754. }
  1755. }
  1756. }
  1757. }
  1758. return casted;
  1759. };
  1760. /**
  1761. * Update a single item: merge with existing item.
  1762. * Will fail when the item has no id, or when there does not exist an item
  1763. * with the same id.
  1764. * @param {Object} item
  1765. * @return {String} id
  1766. * @private
  1767. */
  1768. DataSet.prototype._updateItem = function (item) {
  1769. var id = item[this.fieldId];
  1770. if (id == undefined) {
  1771. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1772. }
  1773. var d = this.data[id];
  1774. if (!d) {
  1775. // item doesn't exist
  1776. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1777. }
  1778. // merge with current item
  1779. for (var field in item) {
  1780. if (item.hasOwnProperty(field)) {
  1781. var type = this.fieldTypes[field]; // type may be undefined
  1782. d[field] = util.cast(item[field], type);
  1783. }
  1784. }
  1785. return id;
  1786. };
  1787. /**
  1788. * Get an array with the column names of a Google DataTable
  1789. * @param {DataTable} dataTable
  1790. * @return {String[]} columnNames
  1791. * @private
  1792. */
  1793. DataSet.prototype._getColumnNames = function (dataTable) {
  1794. var columns = [];
  1795. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1796. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1797. }
  1798. return columns;
  1799. };
  1800. /**
  1801. * Append an item as a row to the dataTable
  1802. * @param dataTable
  1803. * @param columns
  1804. * @param item
  1805. * @private
  1806. */
  1807. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1808. var row = dataTable.addRow();
  1809. for (var col = 0, cols = columns.length; col < cols; col++) {
  1810. var field = columns[col];
  1811. dataTable.setValue(row, col, item[field]);
  1812. }
  1813. };
  1814. /**
  1815. * DataView
  1816. *
  1817. * a dataview offers a filtered view on a dataset or an other dataview.
  1818. *
  1819. * @param {DataSet | DataView} data
  1820. * @param {Object} [options] Available options: see method get
  1821. *
  1822. * @constructor DataView
  1823. */
  1824. function DataView (data, options) {
  1825. this.id = util.randomUUID();
  1826. this.data = null;
  1827. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1828. this.options = options || {};
  1829. this.fieldId = 'id'; // name of the field containing id
  1830. this.subscribers = {}; // event subscribers
  1831. var me = this;
  1832. this.listener = function () {
  1833. me._onEvent.apply(me, arguments);
  1834. };
  1835. this.setData(data);
  1836. }
  1837. /**
  1838. * Set a data source for the view
  1839. * @param {DataSet | DataView} data
  1840. */
  1841. DataView.prototype.setData = function (data) {
  1842. var ids, dataItems, i, len;
  1843. if (this.data) {
  1844. // unsubscribe from current dataset
  1845. if (this.data.unsubscribe) {
  1846. this.data.unsubscribe('*', this.listener);
  1847. }
  1848. // trigger a remove of all items in memory
  1849. ids = [];
  1850. for (var id in this.ids) {
  1851. if (this.ids.hasOwnProperty(id)) {
  1852. ids.push(id);
  1853. }
  1854. }
  1855. this.ids = {};
  1856. this._trigger('remove', {items: ids});
  1857. }
  1858. this.data = data;
  1859. if (this.data) {
  1860. // update fieldId
  1861. this.fieldId = this.options.fieldId ||
  1862. (this.data && this.data.options && this.data.options.fieldId) ||
  1863. 'id';
  1864. // trigger an add of all added items
  1865. ids = this.data.getIds({filter: this.options && this.options.filter});
  1866. for (i = 0, len = ids.length; i < len; i++) {
  1867. id = ids[i];
  1868. this.ids[id] = true;
  1869. }
  1870. this._trigger('add', {items: ids});
  1871. // subscribe to new dataset
  1872. if (this.data.subscribe) {
  1873. this.data.subscribe('*', this.listener);
  1874. }
  1875. }
  1876. };
  1877. /**
  1878. * Get data from the data view
  1879. *
  1880. * Usage:
  1881. *
  1882. * get()
  1883. * get(options: Object)
  1884. * get(options: Object, data: Array | DataTable)
  1885. *
  1886. * get(id: Number)
  1887. * get(id: Number, options: Object)
  1888. * get(id: Number, options: Object, data: Array | DataTable)
  1889. *
  1890. * get(ids: Number[])
  1891. * get(ids: Number[], options: Object)
  1892. * get(ids: Number[], options: Object, data: Array | DataTable)
  1893. *
  1894. * Where:
  1895. *
  1896. * {Number | String} id The id of an item
  1897. * {Number[] | String{}} ids An array with ids of items
  1898. * {Object} options An Object with options. Available options:
  1899. * {String} [type] Type of data to be returned. Can
  1900. * be 'DataTable' or 'Array' (default)
  1901. * {Object.<String, String>} [fieldTypes]
  1902. * {String[]} [fields] field names to be returned
  1903. * {function} [filter] filter items
  1904. * {String | function} [order] Order the items by
  1905. * a field name or custom sort function.
  1906. * {Array | DataTable} [data] If provided, items will be appended to this
  1907. * array or table. Required in case of Google
  1908. * DataTable.
  1909. * @param args
  1910. */
  1911. DataView.prototype.get = function (args) {
  1912. var me = this;
  1913. // parse the arguments
  1914. var ids, options, data;
  1915. var firstType = util.getType(arguments[0]);
  1916. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1917. // get(id(s) [, options] [, data])
  1918. ids = arguments[0]; // can be a single id or an array with ids
  1919. options = arguments[1];
  1920. data = arguments[2];
  1921. }
  1922. else {
  1923. // get([, options] [, data])
  1924. options = arguments[0];
  1925. data = arguments[1];
  1926. }
  1927. // extend the options with the default options and provided options
  1928. var viewOptions = util.extend({}, this.options, options);
  1929. // create a combined filter method when needed
  1930. if (this.options.filter && options && options.filter) {
  1931. viewOptions.filter = function (item) {
  1932. return me.options.filter(item) && options.filter(item);
  1933. }
  1934. }
  1935. // build up the call to the linked data set
  1936. var getArguments = [];
  1937. if (ids != undefined) {
  1938. getArguments.push(ids);
  1939. }
  1940. getArguments.push(viewOptions);
  1941. getArguments.push(data);
  1942. return this.data && this.data.get.apply(this.data, getArguments);
  1943. };
  1944. /**
  1945. * Get ids of all items or from a filtered set of items.
  1946. * @param {Object} [options] An Object with options. Available options:
  1947. * {function} [filter] filter items
  1948. * {String | function} [order] Order the items by
  1949. * a field name or custom sort function.
  1950. * @return {Array} ids
  1951. */
  1952. DataView.prototype.getIds = function (options) {
  1953. var ids;
  1954. if (this.data) {
  1955. var defaultFilter = this.options.filter;
  1956. var filter;
  1957. if (options && options.filter) {
  1958. if (defaultFilter) {
  1959. filter = function (item) {
  1960. return defaultFilter(item) && options.filter(item);
  1961. }
  1962. }
  1963. else {
  1964. filter = options.filter;
  1965. }
  1966. }
  1967. else {
  1968. filter = defaultFilter;
  1969. }
  1970. ids = this.data.getIds({
  1971. filter: filter,
  1972. order: options && options.order
  1973. });
  1974. }
  1975. else {
  1976. ids = [];
  1977. }
  1978. return ids;
  1979. };
  1980. /**
  1981. * Event listener. Will propagate all events from the connected data set to
  1982. * the subscribers of the DataView, but will filter the items and only trigger
  1983. * when there are changes in the filtered data set.
  1984. * @param {String} event
  1985. * @param {Object | null} params
  1986. * @param {String} senderId
  1987. * @private
  1988. */
  1989. DataView.prototype._onEvent = function (event, params, senderId) {
  1990. var i, len, id, item,
  1991. ids = params && params.items,
  1992. data = this.data,
  1993. added = [],
  1994. updated = [],
  1995. removed = [];
  1996. if (ids && data) {
  1997. switch (event) {
  1998. case 'add':
  1999. // filter the ids of the added items
  2000. for (i = 0, len = ids.length; i < len; i++) {
  2001. id = ids[i];
  2002. item = this.get(id);
  2003. if (item) {
  2004. this.ids[id] = true;
  2005. added.push(id);
  2006. }
  2007. }
  2008. break;
  2009. case 'update':
  2010. // determine the event from the views viewpoint: an updated
  2011. // item can be added, updated, or removed from this view.
  2012. for (i = 0, len = ids.length; i < len; i++) {
  2013. id = ids[i];
  2014. item = this.get(id);
  2015. if (item) {
  2016. if (this.ids[id]) {
  2017. updated.push(id);
  2018. }
  2019. else {
  2020. this.ids[id] = true;
  2021. added.push(id);
  2022. }
  2023. }
  2024. else {
  2025. if (this.ids[id]) {
  2026. delete this.ids[id];
  2027. removed.push(id);
  2028. }
  2029. else {
  2030. // nothing interesting for me :-(
  2031. }
  2032. }
  2033. }
  2034. break;
  2035. case 'remove':
  2036. // filter the ids of the removed items
  2037. for (i = 0, len = ids.length; i < len; i++) {
  2038. id = ids[i];
  2039. if (this.ids[id]) {
  2040. delete this.ids[id];
  2041. removed.push(id);
  2042. }
  2043. }
  2044. break;
  2045. }
  2046. if (added.length) {
  2047. this._trigger('add', {items: added}, senderId);
  2048. }
  2049. if (updated.length) {
  2050. this._trigger('update', {items: updated}, senderId);
  2051. }
  2052. if (removed.length) {
  2053. this._trigger('remove', {items: removed}, senderId);
  2054. }
  2055. }
  2056. };
  2057. // copy subscription functionality from DataSet
  2058. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2059. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2060. DataView.prototype._trigger = DataSet.prototype._trigger;
  2061. /**
  2062. * @constructor TimeStep
  2063. * The class TimeStep is an iterator for dates. You provide a start date and an
  2064. * end date. The class itself determines the best scale (step size) based on the
  2065. * provided start Date, end Date, and minimumStep.
  2066. *
  2067. * If minimumStep is provided, the step size is chosen as close as possible
  2068. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2069. * provided, the scale is set to 1 DAY.
  2070. * The minimumStep should correspond with the onscreen size of about 6 characters
  2071. *
  2072. * Alternatively, you can set a scale by hand.
  2073. * After creation, you can initialize the class by executing first(). Then you
  2074. * can iterate from the start date to the end date via next(). You can check if
  2075. * the end date is reached with the function hasNext(). After each step, you can
  2076. * retrieve the current date via getCurrent().
  2077. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2078. * days, to years.
  2079. *
  2080. * Version: 1.2
  2081. *
  2082. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2083. * or new Date(2010, 9, 21, 23, 45, 00)
  2084. * @param {Date} [end] The end date
  2085. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2086. */
  2087. TimeStep = function(start, end, minimumStep) {
  2088. // variables
  2089. this.current = new Date();
  2090. this._start = new Date();
  2091. this._end = new Date();
  2092. this.autoScale = true;
  2093. this.scale = TimeStep.SCALE.DAY;
  2094. this.step = 1;
  2095. // initialize the range
  2096. this.setRange(start, end, minimumStep);
  2097. };
  2098. /// enum scale
  2099. TimeStep.SCALE = {
  2100. MILLISECOND: 1,
  2101. SECOND: 2,
  2102. MINUTE: 3,
  2103. HOUR: 4,
  2104. DAY: 5,
  2105. WEEKDAY: 6,
  2106. MONTH: 7,
  2107. YEAR: 8
  2108. };
  2109. /**
  2110. * Set a new range
  2111. * If minimumStep is provided, the step size is chosen as close as possible
  2112. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2113. * provided, the scale is set to 1 DAY.
  2114. * The minimumStep should correspond with the onscreen size of about 6 characters
  2115. * @param {Date} [start] The start date and time.
  2116. * @param {Date} [end] The end date and time.
  2117. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2118. */
  2119. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2120. if (!(start instanceof Date) || !(end instanceof Date)) {
  2121. //throw "No legal start or end date in method setRange";
  2122. return;
  2123. }
  2124. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2125. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2126. if (this.autoScale) {
  2127. this.setMinimumStep(minimumStep);
  2128. }
  2129. };
  2130. /**
  2131. * Set the range iterator to the start date.
  2132. */
  2133. TimeStep.prototype.first = function() {
  2134. this.current = new Date(this._start.valueOf());
  2135. this.roundToMinor();
  2136. };
  2137. /**
  2138. * Round the current date to the first minor date value
  2139. * This must be executed once when the current date is set to start Date
  2140. */
  2141. TimeStep.prototype.roundToMinor = function() {
  2142. // round to floor
  2143. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2144. //noinspection FallthroughInSwitchStatementJS
  2145. switch (this.scale) {
  2146. case TimeStep.SCALE.YEAR:
  2147. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2148. this.current.setMonth(0);
  2149. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2150. case TimeStep.SCALE.DAY: // intentional fall through
  2151. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2152. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2153. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2154. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2155. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2156. }
  2157. if (this.step != 1) {
  2158. // round down to the first minor value that is a multiple of the current step size
  2159. switch (this.scale) {
  2160. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2161. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2162. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2163. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2164. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2165. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2166. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2167. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2168. default: break;
  2169. }
  2170. }
  2171. };
  2172. /**
  2173. * Check if the there is a next step
  2174. * @return {boolean} true if the current date has not passed the end date
  2175. */
  2176. TimeStep.prototype.hasNext = function () {
  2177. return (this.current.valueOf() <= this._end.valueOf());
  2178. };
  2179. /**
  2180. * Do the next step
  2181. */
  2182. TimeStep.prototype.next = function() {
  2183. var prev = this.current.valueOf();
  2184. // Two cases, needed to prevent issues with switching daylight savings
  2185. // (end of March and end of October)
  2186. if (this.current.getMonth() < 6) {
  2187. switch (this.scale) {
  2188. case TimeStep.SCALE.MILLISECOND:
  2189. this.current = new Date(this.current.valueOf() + this.step); break;
  2190. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2191. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2192. case TimeStep.SCALE.HOUR:
  2193. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2194. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2195. var h = this.current.getHours();
  2196. this.current.setHours(h - (h % this.step));
  2197. break;
  2198. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2199. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2200. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2201. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2202. default: break;
  2203. }
  2204. }
  2205. else {
  2206. switch (this.scale) {
  2207. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2208. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2209. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2210. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2211. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2212. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2213. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2214. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2215. default: break;
  2216. }
  2217. }
  2218. if (this.step != 1) {
  2219. // round down to the correct major value
  2220. switch (this.scale) {
  2221. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2222. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2223. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2224. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2225. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2226. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2227. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2228. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2229. default: break;
  2230. }
  2231. }
  2232. // safety mechanism: if current time is still unchanged, move to the end
  2233. if (this.current.valueOf() == prev) {
  2234. this.current = new Date(this._end.valueOf());
  2235. }
  2236. };
  2237. /**
  2238. * Get the current datetime
  2239. * @return {Date} current The current date
  2240. */
  2241. TimeStep.prototype.getCurrent = function() {
  2242. return this.current;
  2243. };
  2244. /**
  2245. * Set a custom scale. Autoscaling will be disabled.
  2246. * For example setScale(SCALE.MINUTES, 5) will result
  2247. * in minor steps of 5 minutes, and major steps of an hour.
  2248. *
  2249. * @param {TimeStep.SCALE} newScale
  2250. * A scale. Choose from SCALE.MILLISECOND,
  2251. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2252. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2253. * SCALE.YEAR.
  2254. * @param {Number} newStep A step size, by default 1. Choose for
  2255. * example 1, 2, 5, or 10.
  2256. */
  2257. TimeStep.prototype.setScale = function(newScale, newStep) {
  2258. this.scale = newScale;
  2259. if (newStep > 0) {
  2260. this.step = newStep;
  2261. }
  2262. this.autoScale = false;
  2263. };
  2264. /**
  2265. * Enable or disable autoscaling
  2266. * @param {boolean} enable If true, autoascaling is set true
  2267. */
  2268. TimeStep.prototype.setAutoScale = function (enable) {
  2269. this.autoScale = enable;
  2270. };
  2271. /**
  2272. * Automatically determine the scale that bests fits the provided minimum step
  2273. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2274. */
  2275. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2276. if (minimumStep == undefined) {
  2277. return;
  2278. }
  2279. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2280. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2281. var stepDay = (1000 * 60 * 60 * 24);
  2282. var stepHour = (1000 * 60 * 60);
  2283. var stepMinute = (1000 * 60);
  2284. var stepSecond = (1000);
  2285. var stepMillisecond= (1);
  2286. // find the smallest step that is larger than the provided minimumStep
  2287. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2288. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2289. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2290. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2291. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2292. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2293. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2294. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2295. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2296. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2297. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2298. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2299. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2300. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2301. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2302. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2303. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2304. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2305. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2306. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2307. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2308. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2309. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2310. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2311. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2312. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2313. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2314. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2315. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2316. };
  2317. /**
  2318. * Snap a date to a rounded value. The snap intervals are dependent on the
  2319. * current scale and step.
  2320. * @param {Date} date the date to be snapped
  2321. */
  2322. TimeStep.prototype.snap = function(date) {
  2323. if (this.scale == TimeStep.SCALE.YEAR) {
  2324. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2325. date.setFullYear(Math.round(year / this.step) * this.step);
  2326. date.setMonth(0);
  2327. date.setDate(0);
  2328. date.setHours(0);
  2329. date.setMinutes(0);
  2330. date.setSeconds(0);
  2331. date.setMilliseconds(0);
  2332. }
  2333. else if (this.scale == TimeStep.SCALE.MONTH) {
  2334. if (date.getDate() > 15) {
  2335. date.setDate(1);
  2336. date.setMonth(date.getMonth() + 1);
  2337. // important: first set Date to 1, after that change the month.
  2338. }
  2339. else {
  2340. date.setDate(1);
  2341. }
  2342. date.setHours(0);
  2343. date.setMinutes(0);
  2344. date.setSeconds(0);
  2345. date.setMilliseconds(0);
  2346. }
  2347. else if (this.scale == TimeStep.SCALE.DAY ||
  2348. this.scale == TimeStep.SCALE.WEEKDAY) {
  2349. //noinspection FallthroughInSwitchStatementJS
  2350. switch (this.step) {
  2351. case 5:
  2352. case 2:
  2353. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2354. default:
  2355. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2356. }
  2357. date.setMinutes(0);
  2358. date.setSeconds(0);
  2359. date.setMilliseconds(0);
  2360. }
  2361. else if (this.scale == TimeStep.SCALE.HOUR) {
  2362. switch (this.step) {
  2363. case 4:
  2364. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2365. default:
  2366. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2367. }
  2368. date.setSeconds(0);
  2369. date.setMilliseconds(0);
  2370. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2371. //noinspection FallthroughInSwitchStatementJS
  2372. switch (this.step) {
  2373. case 15:
  2374. case 10:
  2375. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2376. date.setSeconds(0);
  2377. break;
  2378. case 5:
  2379. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2380. default:
  2381. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2382. }
  2383. date.setMilliseconds(0);
  2384. }
  2385. else if (this.scale == TimeStep.SCALE.SECOND) {
  2386. //noinspection FallthroughInSwitchStatementJS
  2387. switch (this.step) {
  2388. case 15:
  2389. case 10:
  2390. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2391. date.setMilliseconds(0);
  2392. break;
  2393. case 5:
  2394. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2395. default:
  2396. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2397. }
  2398. }
  2399. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2400. var step = this.step > 5 ? this.step / 2 : 1;
  2401. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2402. }
  2403. };
  2404. /**
  2405. * Check if the current value is a major value (for example when the step
  2406. * is DAY, a major value is each first day of the MONTH)
  2407. * @return {boolean} true if current date is major, else false.
  2408. */
  2409. TimeStep.prototype.isMajor = function() {
  2410. switch (this.scale) {
  2411. case TimeStep.SCALE.MILLISECOND:
  2412. return (this.current.getMilliseconds() == 0);
  2413. case TimeStep.SCALE.SECOND:
  2414. return (this.current.getSeconds() == 0);
  2415. case TimeStep.SCALE.MINUTE:
  2416. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2417. // Note: this is no bug. Major label is equal for both minute and hour scale
  2418. case TimeStep.SCALE.HOUR:
  2419. return (this.current.getHours() == 0);
  2420. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2421. case TimeStep.SCALE.DAY:
  2422. return (this.current.getDate() == 1);
  2423. case TimeStep.SCALE.MONTH:
  2424. return (this.current.getMonth() == 0);
  2425. case TimeStep.SCALE.YEAR:
  2426. return false;
  2427. default:
  2428. return false;
  2429. }
  2430. };
  2431. /**
  2432. * Returns formatted text for the minor axislabel, depending on the current
  2433. * date and the scale. For example when scale is MINUTE, the current time is
  2434. * formatted as "hh:mm".
  2435. * @param {Date} [date] custom date. if not provided, current date is taken
  2436. */
  2437. TimeStep.prototype.getLabelMinor = function(date) {
  2438. if (date == undefined) {
  2439. date = this.current;
  2440. }
  2441. switch (this.scale) {
  2442. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2443. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2444. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2445. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2446. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2447. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2448. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2449. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2450. default: return '';
  2451. }
  2452. };
  2453. /**
  2454. * Returns formatted text for the major axis label, depending on the current
  2455. * date and the scale. For example when scale is MINUTE, the major scale is
  2456. * hours, and the hour will be formatted as "hh".
  2457. * @param {Date} [date] custom date. if not provided, current date is taken
  2458. */
  2459. TimeStep.prototype.getLabelMajor = function(date) {
  2460. if (date == undefined) {
  2461. date = this.current;
  2462. }
  2463. //noinspection FallthroughInSwitchStatementJS
  2464. switch (this.scale) {
  2465. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2466. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2467. case TimeStep.SCALE.MINUTE:
  2468. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2469. case TimeStep.SCALE.WEEKDAY:
  2470. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2471. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2472. case TimeStep.SCALE.YEAR: return '';
  2473. default: return '';
  2474. }
  2475. };
  2476. /**
  2477. * @constructor Stack
  2478. * Stacks items on top of each other.
  2479. * @param {ItemSet} parent
  2480. * @param {Object} [options]
  2481. */
  2482. function Stack (parent, options) {
  2483. this.parent = parent;
  2484. this.options = options || {};
  2485. this.defaultOptions = {
  2486. order: function (a, b) {
  2487. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2488. // Order: ranges over non-ranges, ranged ordered by width, and
  2489. // lastly ordered by start.
  2490. if (a instanceof ItemRange) {
  2491. if (b instanceof ItemRange) {
  2492. var aInt = (a.data.end - a.data.start);
  2493. var bInt = (b.data.end - b.data.start);
  2494. return (aInt - bInt) || (a.data.start - b.data.start);
  2495. }
  2496. else {
  2497. return -1;
  2498. }
  2499. }
  2500. else {
  2501. if (b instanceof ItemRange) {
  2502. return 1;
  2503. }
  2504. else {
  2505. return (a.data.start - b.data.start);
  2506. }
  2507. }
  2508. },
  2509. margin: {
  2510. item: 10
  2511. }
  2512. };
  2513. this.ordered = []; // ordered items
  2514. }
  2515. /**
  2516. * Set options for the stack
  2517. * @param {Object} options Available options:
  2518. * {ItemSet} parent
  2519. * {Number} margin
  2520. * {function} order Stacking order
  2521. */
  2522. Stack.prototype.setOptions = function setOptions (options) {
  2523. util.extend(this.options, options);
  2524. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2525. };
  2526. /**
  2527. * Stack the items such that they don't overlap. The items will have a minimal
  2528. * distance equal to options.margin.item.
  2529. */
  2530. Stack.prototype.update = function update() {
  2531. this._order();
  2532. this._stack();
  2533. };
  2534. /**
  2535. * Order the items. The items are ordered by width first, and by left position
  2536. * second.
  2537. * If a custom order function has been provided via the options, then this will
  2538. * be used.
  2539. * @private
  2540. */
  2541. Stack.prototype._order = function _order () {
  2542. var items = this.parent.items;
  2543. if (!items) {
  2544. throw new Error('Cannot stack items: parent does not contain items');
  2545. }
  2546. // TODO: store the sorted items, to have less work later on
  2547. var ordered = [];
  2548. var index = 0;
  2549. // items is a map (no array)
  2550. util.forEach(items, function (item) {
  2551. if (item.visible) {
  2552. ordered[index] = item;
  2553. index++;
  2554. }
  2555. });
  2556. //if a customer stack order function exists, use it.
  2557. var order = this.options.order || this.defaultOptions.order;
  2558. if (!(typeof order === 'function')) {
  2559. throw new Error('Option order must be a function');
  2560. }
  2561. ordered.sort(order);
  2562. this.ordered = ordered;
  2563. };
  2564. /**
  2565. * Adjust vertical positions of the events such that they don't overlap each
  2566. * other.
  2567. * @private
  2568. */
  2569. Stack.prototype._stack = function _stack () {
  2570. var i,
  2571. iMax,
  2572. ordered = this.ordered,
  2573. options = this.options,
  2574. orientation = options.orientation || this.defaultOptions.orientation,
  2575. axisOnTop = (orientation == 'top'),
  2576. margin;
  2577. if (options.margin && options.margin.item !== undefined) {
  2578. margin = options.margin.item;
  2579. }
  2580. else {
  2581. margin = this.defaultOptions.margin.item
  2582. }
  2583. // calculate new, non-overlapping positions
  2584. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2585. var item = ordered[i];
  2586. var collidingItem = null;
  2587. do {
  2588. // TODO: optimize checking for overlap. when there is a gap without items,
  2589. // you only need to check for items from the next item on, not from zero
  2590. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2591. if (collidingItem != null) {
  2592. // There is a collision. Reposition the event above the colliding element
  2593. if (axisOnTop) {
  2594. item.top = collidingItem.top + collidingItem.height + margin;
  2595. }
  2596. else {
  2597. item.top = collidingItem.top - item.height - margin;
  2598. }
  2599. }
  2600. } while (collidingItem);
  2601. }
  2602. };
  2603. /**
  2604. * Check if the destiny position of given item overlaps with any
  2605. * of the other items from index itemStart to itemEnd.
  2606. * @param {Array} items Array with items
  2607. * @param {int} itemIndex Number of the item to be checked for overlap
  2608. * @param {int} itemStart First item to be checked.
  2609. * @param {int} itemEnd Last item to be checked.
  2610. * @return {Object | null} colliding item, or undefined when no collisions
  2611. * @param {Number} margin A minimum required margin.
  2612. * If margin is provided, the two items will be
  2613. * marked colliding when they overlap or
  2614. * when the margin between the two is smaller than
  2615. * the requested margin.
  2616. */
  2617. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2618. itemStart, itemEnd, margin) {
  2619. var collision = this.collision;
  2620. // we loop from end to start, as we suppose that the chance of a
  2621. // collision is larger for items at the end, so check these first.
  2622. var a = items[itemIndex];
  2623. for (var i = itemEnd; i >= itemStart; i--) {
  2624. var b = items[i];
  2625. if (collision(a, b, margin)) {
  2626. if (i != itemIndex) {
  2627. return b;
  2628. }
  2629. }
  2630. }
  2631. return null;
  2632. };
  2633. /**
  2634. * Test if the two provided items collide
  2635. * The items must have parameters left, width, top, and height.
  2636. * @param {Component} a The first item
  2637. * @param {Component} b The second item
  2638. * @param {Number} margin A minimum required margin.
  2639. * If margin is provided, the two items will be
  2640. * marked colliding when they overlap or
  2641. * when the margin between the two is smaller than
  2642. * the requested margin.
  2643. * @return {boolean} true if a and b collide, else false
  2644. */
  2645. Stack.prototype.collision = function collision (a, b, margin) {
  2646. return ((a.left - margin) < (b.left + b.width) &&
  2647. (a.left + a.width + margin) > b.left &&
  2648. (a.top - margin) < (b.top + b.height) &&
  2649. (a.top + a.height + margin) > b.top);
  2650. };
  2651. /**
  2652. * @constructor Range
  2653. * A Range controls a numeric range with a start and end value.
  2654. * The Range adjusts the range based on mouse events or programmatic changes,
  2655. * and triggers events when the range is changing or has been changed.
  2656. * @param {Object} [options] See description at Range.setOptions
  2657. * @extends Controller
  2658. */
  2659. function Range(options) {
  2660. this.id = util.randomUUID();
  2661. this.start = 0; // Number
  2662. this.end = 0; // Number
  2663. this.options = {
  2664. min: null,
  2665. max: null,
  2666. zoomMin: null,
  2667. zoomMax: null
  2668. };
  2669. this.listeners = [];
  2670. this.setOptions(options);
  2671. }
  2672. /**
  2673. * Set options for the range controller
  2674. * @param {Object} options Available options:
  2675. * {Number} start Set start value of the range
  2676. * {Number} end Set end value of the range
  2677. * {Number} min Minimum value for start
  2678. * {Number} max Maximum value for end
  2679. * {Number} zoomMin Set a minimum value for
  2680. * (end - start).
  2681. * {Number} zoomMax Set a maximum value for
  2682. * (end - start).
  2683. */
  2684. Range.prototype.setOptions = function (options) {
  2685. util.extend(this.options, options);
  2686. if (options.start != null || options.end != null) {
  2687. this.setRange(options.start, options.end);
  2688. }
  2689. };
  2690. /**
  2691. * Add listeners for mouse and touch events to the component
  2692. * @param {Component} component
  2693. * @param {String} event Available events: 'move', 'zoom'
  2694. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2695. */
  2696. Range.prototype.subscribe = function (component, event, direction) {
  2697. var me = this;
  2698. var listener;
  2699. if (direction != 'horizontal' && direction != 'vertical') {
  2700. throw new TypeError('Unknown direction "' + direction + '". ' +
  2701. 'Choose "horizontal" or "vertical".');
  2702. }
  2703. //noinspection FallthroughInSwitchStatementJS
  2704. if (event == 'move') {
  2705. listener = {
  2706. component: component,
  2707. event: event,
  2708. direction: direction,
  2709. callback: function (event) {
  2710. me._onMouseDown(event, listener);
  2711. },
  2712. params: {}
  2713. };
  2714. component.on('mousedown', listener.callback);
  2715. me.listeners.push(listener);
  2716. }
  2717. else if (event == 'zoom') {
  2718. listener = {
  2719. component: component,
  2720. event: event,
  2721. direction: direction,
  2722. callback: function (event) {
  2723. me._onMouseWheel(event, listener);
  2724. },
  2725. params: {}
  2726. };
  2727. component.on('mousewheel', listener.callback);
  2728. me.listeners.push(listener);
  2729. }
  2730. else {
  2731. throw new TypeError('Unknown event "' + event + '". ' +
  2732. 'Choose "move" or "zoom".');
  2733. }
  2734. };
  2735. /**
  2736. * Event handler
  2737. * @param {String} event name of the event, for example 'click', 'mousemove'
  2738. * @param {function} callback callback handler, invoked with the raw HTML Event
  2739. * as parameter.
  2740. */
  2741. Range.prototype.on = function (event, callback) {
  2742. events.addListener(this, event, callback);
  2743. };
  2744. /**
  2745. * Trigger an event
  2746. * @param {String} event name of the event, available events: 'rangechange',
  2747. * 'rangechanged'
  2748. * @private
  2749. */
  2750. Range.prototype._trigger = function (event) {
  2751. events.trigger(this, event, {
  2752. start: this.start,
  2753. end: this.end
  2754. });
  2755. };
  2756. /**
  2757. * Set a new start and end range
  2758. * @param {Number} start
  2759. * @param {Number} end
  2760. */
  2761. Range.prototype.setRange = function(start, end) {
  2762. var changed = this._applyRange(start, end);
  2763. if (changed) {
  2764. this._trigger('rangechange');
  2765. this._trigger('rangechanged');
  2766. }
  2767. };
  2768. /**
  2769. * Set a new start and end range. This method is the same as setRange, but
  2770. * does not trigger a range change and range changed event, and it returns
  2771. * true when the range is changed
  2772. * @param {Number} start
  2773. * @param {Number} end
  2774. * @return {Boolean} changed
  2775. * @private
  2776. */
  2777. Range.prototype._applyRange = function(start, end) {
  2778. var newStart = (start != null) ? util.cast(start, 'Number') : this.start;
  2779. var newEnd = (end != null) ? util.cast(end, 'Number') : this.end;
  2780. var diff;
  2781. // check for valid number
  2782. if (isNaN(newStart)) {
  2783. throw new Error('Invalid start "' + start + '"');
  2784. }
  2785. if (isNaN(newEnd)) {
  2786. throw new Error('Invalid end "' + end + '"');
  2787. }
  2788. // prevent start < end
  2789. if (newEnd < newStart) {
  2790. newEnd = newStart;
  2791. }
  2792. // prevent start < min
  2793. if (this.options.min != null) {
  2794. var min = this.options.min.valueOf();
  2795. if (newStart < min) {
  2796. diff = (min - newStart);
  2797. newStart += diff;
  2798. newEnd += diff;
  2799. }
  2800. }
  2801. // prevent end > max
  2802. if (this.options.max != null) {
  2803. var max = this.options.max.valueOf();
  2804. if (newEnd > max) {
  2805. diff = (newEnd - max);
  2806. newStart -= diff;
  2807. newEnd -= diff;
  2808. }
  2809. }
  2810. // prevent (end-start) > zoomMin
  2811. if (this.options.zoomMin != null) {
  2812. var zoomMin = this.options.zoomMin.valueOf();
  2813. if (zoomMin < 0) {
  2814. zoomMin = 0;
  2815. }
  2816. if ((newEnd - newStart) < zoomMin) {
  2817. if ((this.end - this.start) > zoomMin) {
  2818. // zoom to the minimum
  2819. diff = (zoomMin - (newEnd - newStart));
  2820. newStart -= diff / 2;
  2821. newEnd += diff / 2;
  2822. }
  2823. else {
  2824. // ingore this action, we are already zoomed to the minimum
  2825. newStart = this.start;
  2826. newEnd = this.end;
  2827. }
  2828. }
  2829. }
  2830. // prevent (end-start) > zoomMin
  2831. if (this.options.zoomMax != null) {
  2832. var zoomMax = this.options.zoomMax.valueOf();
  2833. if (zoomMax < 0) {
  2834. zoomMax = 0;
  2835. }
  2836. if ((newEnd - newStart) > zoomMax) {
  2837. if ((this.end - this.start) < zoomMax) {
  2838. // zoom to the maximum
  2839. diff = ((newEnd - newStart) - zoomMax);
  2840. newStart += diff / 2;
  2841. newEnd -= diff / 2;
  2842. }
  2843. else {
  2844. // ingore this action, we are already zoomed to the maximum
  2845. newStart = this.start;
  2846. newEnd = this.end;
  2847. }
  2848. }
  2849. }
  2850. var changed = (this.start != newStart || this.end != newEnd);
  2851. this.start = newStart;
  2852. this.end = newEnd;
  2853. return changed;
  2854. };
  2855. /**
  2856. * Retrieve the current range.
  2857. * @return {Object} An object with start and end properties
  2858. */
  2859. Range.prototype.getRange = function() {
  2860. return {
  2861. start: this.start,
  2862. end: this.end
  2863. };
  2864. };
  2865. /**
  2866. * Calculate the conversion offset and factor for current range, based on
  2867. * the provided width
  2868. * @param {Number} width
  2869. * @returns {{offset: number, factor: number}} conversion
  2870. */
  2871. Range.prototype.conversion = function (width) {
  2872. var start = this.start;
  2873. var end = this.end;
  2874. return Range.conversion(this.start, this.end, width);
  2875. };
  2876. /**
  2877. * Static method to calculate the conversion offset and factor for a range,
  2878. * based on the provided start, end, and width
  2879. * @param {Number} start
  2880. * @param {Number} end
  2881. * @param {Number} width
  2882. * @returns {{offset: number, factor: number}} conversion
  2883. */
  2884. Range.conversion = function (start, end, width) {
  2885. if (width != 0 && (end - start != 0)) {
  2886. return {
  2887. offset: start,
  2888. factor: width / (end - start)
  2889. }
  2890. }
  2891. else {
  2892. return {
  2893. offset: 0,
  2894. factor: 1
  2895. };
  2896. }
  2897. };
  2898. /**
  2899. * Start moving horizontally or vertically
  2900. * @param {Event} event
  2901. * @param {Object} listener Listener containing the component and params
  2902. * @private
  2903. */
  2904. Range.prototype._onMouseDown = function(event, listener) {
  2905. event = event || window.event;
  2906. var params = listener.params;
  2907. // only react on left mouse button down
  2908. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2909. if (!leftButtonDown) {
  2910. return;
  2911. }
  2912. // get mouse position
  2913. params.mouseX = util.getPageX(event);
  2914. params.mouseY = util.getPageY(event);
  2915. params.previousLeft = 0;
  2916. params.previousOffset = 0;
  2917. params.moved = false;
  2918. params.start = this.start;
  2919. params.end = this.end;
  2920. var frame = listener.component.frame;
  2921. if (frame) {
  2922. frame.style.cursor = 'move';
  2923. }
  2924. // add event listeners to handle moving the contents
  2925. // we store the function onmousemove and onmouseup in the timeaxis,
  2926. // so we can remove the eventlisteners lateron in the function onmouseup
  2927. var me = this;
  2928. if (!params.onMouseMove) {
  2929. params.onMouseMove = function (event) {
  2930. me._onMouseMove(event, listener);
  2931. };
  2932. util.addEventListener(document, "mousemove", params.onMouseMove);
  2933. }
  2934. if (!params.onMouseUp) {
  2935. params.onMouseUp = function (event) {
  2936. me._onMouseUp(event, listener);
  2937. };
  2938. util.addEventListener(document, "mouseup", params.onMouseUp);
  2939. }
  2940. util.preventDefault(event);
  2941. };
  2942. /**
  2943. * Perform moving operating.
  2944. * This function activated from within the funcion TimeAxis._onMouseDown().
  2945. * @param {Event} event
  2946. * @param {Object} listener
  2947. * @private
  2948. */
  2949. Range.prototype._onMouseMove = function (event, listener) {
  2950. event = event || window.event;
  2951. var params = listener.params;
  2952. // calculate change in mouse position
  2953. var mouseX = util.getPageX(event);
  2954. var mouseY = util.getPageY(event);
  2955. if (params.mouseX == undefined) {
  2956. params.mouseX = mouseX;
  2957. }
  2958. if (params.mouseY == undefined) {
  2959. params.mouseY = mouseY;
  2960. }
  2961. var diffX = mouseX - params.mouseX;
  2962. var diffY = mouseY - params.mouseY;
  2963. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  2964. // if mouse movement is big enough, register it as a "moved" event
  2965. if (Math.abs(diff) >= 1) {
  2966. params.moved = true;
  2967. }
  2968. var interval = (params.end - params.start);
  2969. var width = (listener.direction == 'horizontal') ?
  2970. listener.component.width : listener.component.height;
  2971. var diffRange = -diff / width * interval;
  2972. this._applyRange(params.start + diffRange, params.end + diffRange);
  2973. // fire a rangechange event
  2974. this._trigger('rangechange');
  2975. util.preventDefault(event);
  2976. };
  2977. /**
  2978. * Stop moving operating.
  2979. * This function activated from within the function Range._onMouseDown().
  2980. * @param {event} event
  2981. * @param {Object} listener
  2982. * @private
  2983. */
  2984. Range.prototype._onMouseUp = function (event, listener) {
  2985. event = event || window.event;
  2986. var params = listener.params;
  2987. if (listener.component.frame) {
  2988. listener.component.frame.style.cursor = 'auto';
  2989. }
  2990. // remove event listeners here, important for Safari
  2991. if (params.onMouseMove) {
  2992. util.removeEventListener(document, "mousemove", params.onMouseMove);
  2993. params.onMouseMove = null;
  2994. }
  2995. if (params.onMouseUp) {
  2996. util.removeEventListener(document, "mouseup", params.onMouseUp);
  2997. params.onMouseUp = null;
  2998. }
  2999. //util.preventDefault(event);
  3000. if (params.moved) {
  3001. // fire a rangechanged event
  3002. this._trigger('rangechanged');
  3003. }
  3004. };
  3005. /**
  3006. * Event handler for mouse wheel event, used to zoom
  3007. * Code from http://adomas.org/javascript-mouse-wheel/
  3008. * @param {Event} event
  3009. * @param {Object} listener
  3010. * @private
  3011. */
  3012. Range.prototype._onMouseWheel = function(event, listener) {
  3013. event = event || window.event;
  3014. // retrieve delta
  3015. var delta = 0;
  3016. if (event.wheelDelta) { /* IE/Opera. */
  3017. delta = event.wheelDelta / 120;
  3018. } else if (event.detail) { /* Mozilla case. */
  3019. // In Mozilla, sign of delta is different than in IE.
  3020. // Also, delta is multiple of 3.
  3021. delta = -event.detail / 3;
  3022. }
  3023. // If delta is nonzero, handle it.
  3024. // Basically, delta is now positive if wheel was scrolled up,
  3025. // and negative, if wheel was scrolled down.
  3026. if (delta) {
  3027. var me = this;
  3028. var zoom = function () {
  3029. // perform the zoom action. Delta is normally 1 or -1
  3030. var zoomFactor = delta / 5.0;
  3031. var zoomAround = null;
  3032. var frame = listener.component.frame;
  3033. if (frame) {
  3034. var size, conversion;
  3035. if (listener.direction == 'horizontal') {
  3036. size = listener.component.width;
  3037. conversion = me.conversion(size);
  3038. var frameLeft = util.getAbsoluteLeft(frame);
  3039. var mouseX = util.getPageX(event);
  3040. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  3041. }
  3042. else {
  3043. size = listener.component.height;
  3044. conversion = me.conversion(size);
  3045. var frameTop = util.getAbsoluteTop(frame);
  3046. var mouseY = util.getPageY(event);
  3047. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  3048. }
  3049. }
  3050. me.zoom(zoomFactor, zoomAround);
  3051. };
  3052. zoom();
  3053. }
  3054. // Prevent default actions caused by mouse wheel.
  3055. // That might be ugly, but we handle scrolls somehow
  3056. // anyway, so don't bother here...
  3057. util.preventDefault(event);
  3058. };
  3059. /**
  3060. * Zoom the range the given zoomfactor in or out. Start and end date will
  3061. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3062. * date around which to zoom.
  3063. * For example, try zoomfactor = 0.1 or -0.1
  3064. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  3065. * negative value will zoom out
  3066. * @param {Number} zoomAround Value around which will be zoomed. Optional
  3067. */
  3068. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  3069. // if zoomAroundDate is not provided, take it half between start Date and end Date
  3070. if (zoomAround == null) {
  3071. zoomAround = (this.start + this.end) / 2;
  3072. }
  3073. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  3074. // result in a start>=end )
  3075. if (zoomFactor >= 1) {
  3076. zoomFactor = 0.9;
  3077. }
  3078. if (zoomFactor <= -1) {
  3079. zoomFactor = -0.9;
  3080. }
  3081. // adjust a negative factor such that zooming in with 0.1 equals zooming
  3082. // out with a factor -0.1
  3083. if (zoomFactor < 0) {
  3084. zoomFactor = zoomFactor / (1 + zoomFactor);
  3085. }
  3086. // zoom start and end relative to the zoomAround value
  3087. var startDiff = (this.start - zoomAround);
  3088. var endDiff = (this.end - zoomAround);
  3089. // calculate new start and end
  3090. var newStart = this.start - startDiff * zoomFactor;
  3091. var newEnd = this.end - endDiff * zoomFactor;
  3092. this.setRange(newStart, newEnd);
  3093. };
  3094. /**
  3095. * Move the range with a given factor to the left or right. Start and end
  3096. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  3097. * @param {Number} moveFactor Moving amount. Positive value will move right,
  3098. * negative value will move left
  3099. */
  3100. Range.prototype.move = function(moveFactor) {
  3101. // zoom start Date and end Date relative to the zoomAroundDate
  3102. var diff = (this.end - this.start);
  3103. // apply new values
  3104. var newStart = this.start + diff * moveFactor;
  3105. var newEnd = this.end + diff * moveFactor;
  3106. // TODO: reckon with min and max range
  3107. this.start = newStart;
  3108. this.end = newEnd;
  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. type: '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} [type]
  4191. * Default type 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. options.type ||
  4312. 'box';
  4313. var constructor = ItemSet.types[type];
  4314. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4315. if (item) {
  4316. // update item
  4317. if (!constructor || !(item instanceof constructor)) {
  4318. // item type has changed, hide and delete the item
  4319. changed += item.hide();
  4320. item = null;
  4321. }
  4322. else {
  4323. item.data = itemData; // TODO: create a method item.setData ?
  4324. changed++;
  4325. }
  4326. }
  4327. if (!item) {
  4328. // create item
  4329. if (constructor) {
  4330. item = new constructor(me, itemData, options, defaultOptions);
  4331. changed++;
  4332. }
  4333. else {
  4334. throw new TypeError('Unknown item type "' + type + '"');
  4335. }
  4336. }
  4337. // force a repaint (not only a reposition)
  4338. item.repaint();
  4339. items[id] = item;
  4340. }
  4341. // update queue
  4342. delete queue[id];
  4343. break;
  4344. case 'remove':
  4345. if (item) {
  4346. // remove DOM of the item
  4347. changed += item.hide();
  4348. }
  4349. // update lists
  4350. delete items[id];
  4351. delete queue[id];
  4352. break;
  4353. default:
  4354. console.log('Error: unknown action "' + action + '"');
  4355. }
  4356. });
  4357. // reposition all items. Show items only when in the visible area
  4358. util.forEach(this.items, function (item) {
  4359. if (item.visible) {
  4360. changed += item.show();
  4361. item.reposition();
  4362. }
  4363. else {
  4364. changed += item.hide();
  4365. }
  4366. });
  4367. return (changed > 0);
  4368. };
  4369. /**
  4370. * Get the foreground container element
  4371. * @return {HTMLElement} foreground
  4372. */
  4373. ItemSet.prototype.getForeground = function getForeground() {
  4374. return this.dom.foreground;
  4375. };
  4376. /**
  4377. * Get the background container element
  4378. * @return {HTMLElement} background
  4379. */
  4380. ItemSet.prototype.getBackground = function getBackground() {
  4381. return this.dom.background;
  4382. };
  4383. /**
  4384. * Get the axis container element
  4385. * @return {HTMLElement} axis
  4386. */
  4387. ItemSet.prototype.getAxis = function getAxis() {
  4388. return this.dom.axis;
  4389. };
  4390. /**
  4391. * Reflow the component
  4392. * @return {Boolean} resized
  4393. */
  4394. ItemSet.prototype.reflow = function reflow () {
  4395. var changed = 0,
  4396. options = this.options,
  4397. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4398. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4399. update = util.updateProperty,
  4400. asNumber = util.option.asNumber,
  4401. asSize = util.option.asSize,
  4402. frame = this.frame;
  4403. if (frame) {
  4404. this._updateConversion();
  4405. util.forEach(this.items, function (item) {
  4406. changed += item.reflow();
  4407. });
  4408. // TODO: stack.update should be triggered via an event, in stack itself
  4409. // TODO: only update the stack when there are changed items
  4410. this.stack.update();
  4411. var maxHeight = asNumber(options.maxHeight);
  4412. var fixedHeight = (asSize(options.height) != null);
  4413. var height;
  4414. if (fixedHeight) {
  4415. height = frame.offsetHeight;
  4416. }
  4417. else {
  4418. // height is not specified, determine the height from the height and positioned items
  4419. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4420. if (visibleItems.length) {
  4421. var min = visibleItems[0].top;
  4422. var max = visibleItems[0].top + visibleItems[0].height;
  4423. util.forEach(visibleItems, function (item) {
  4424. min = Math.min(min, item.top);
  4425. max = Math.max(max, (item.top + item.height));
  4426. });
  4427. height = (max - min) + marginAxis + marginItem;
  4428. }
  4429. else {
  4430. height = marginAxis + marginItem;
  4431. }
  4432. }
  4433. if (maxHeight != null) {
  4434. height = Math.min(height, maxHeight);
  4435. }
  4436. changed += update(this, 'height', height);
  4437. // calculate height from items
  4438. changed += update(this, 'top', frame.offsetTop);
  4439. changed += update(this, 'left', frame.offsetLeft);
  4440. changed += update(this, 'width', frame.offsetWidth);
  4441. }
  4442. else {
  4443. changed += 1;
  4444. }
  4445. return (changed > 0);
  4446. };
  4447. /**
  4448. * Hide this component from the DOM
  4449. * @return {Boolean} changed
  4450. */
  4451. ItemSet.prototype.hide = function hide() {
  4452. var changed = false;
  4453. // remove the DOM
  4454. if (this.frame && this.frame.parentNode) {
  4455. this.frame.parentNode.removeChild(this.frame);
  4456. changed = true;
  4457. }
  4458. if (this.dom.axis && this.dom.axis.parentNode) {
  4459. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4460. changed = true;
  4461. }
  4462. return changed;
  4463. };
  4464. /**
  4465. * Set items
  4466. * @param {vis.DataSet | null} items
  4467. */
  4468. ItemSet.prototype.setItems = function setItems(items) {
  4469. var me = this,
  4470. ids;
  4471. // unsubscribe from current dataset
  4472. var current = this.itemsData;
  4473. if (current) {
  4474. util.forEach(this.listeners, function (callback, event) {
  4475. current.unsubscribe(event, callback);
  4476. });
  4477. // remove all drawn items
  4478. ids = current.getIds();
  4479. this._onRemove(ids);
  4480. }
  4481. // replace the dataset
  4482. if (!items) {
  4483. this.itemsData = null;
  4484. }
  4485. else if (items instanceof DataSet || items instanceof DataView) {
  4486. this.itemsData = items;
  4487. }
  4488. else {
  4489. throw new TypeError('Data must be an instance of DataSet');
  4490. }
  4491. if (this.itemsData) {
  4492. // subscribe to new dataset
  4493. var id = this.id;
  4494. util.forEach(this.listeners, function (callback, event) {
  4495. me.itemsData.subscribe(event, callback, id);
  4496. });
  4497. // draw all new items
  4498. ids = this.itemsData.getIds();
  4499. this._onAdd(ids);
  4500. }
  4501. };
  4502. /**
  4503. * Get the current items items
  4504. * @returns {vis.DataSet | null}
  4505. */
  4506. ItemSet.prototype.getItems = function getItems() {
  4507. return this.itemsData;
  4508. };
  4509. /**
  4510. * Handle updated items
  4511. * @param {Number[]} ids
  4512. * @private
  4513. */
  4514. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4515. this._toQueue('update', ids);
  4516. };
  4517. /**
  4518. * Handle changed items
  4519. * @param {Number[]} ids
  4520. * @private
  4521. */
  4522. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4523. this._toQueue('add', ids);
  4524. };
  4525. /**
  4526. * Handle removed items
  4527. * @param {Number[]} ids
  4528. * @private
  4529. */
  4530. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4531. this._toQueue('remove', ids);
  4532. };
  4533. /**
  4534. * Put items in the queue to be added/updated/remove
  4535. * @param {String} action can be 'add', 'update', 'remove'
  4536. * @param {Number[]} ids
  4537. */
  4538. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4539. var queue = this.queue;
  4540. ids.forEach(function (id) {
  4541. queue[id] = action;
  4542. });
  4543. if (this.controller) {
  4544. //this.requestReflow();
  4545. this.requestRepaint();
  4546. }
  4547. };
  4548. /**
  4549. * Calculate the factor and offset to convert a position on screen to the
  4550. * corresponding date and vice versa.
  4551. * After the method _updateConversion is executed once, the methods toTime
  4552. * and toScreen can be used.
  4553. * @private
  4554. */
  4555. ItemSet.prototype._updateConversion = function _updateConversion() {
  4556. var range = this.range;
  4557. if (!range) {
  4558. throw new Error('No range configured');
  4559. }
  4560. if (range.conversion) {
  4561. this.conversion = range.conversion(this.width);
  4562. }
  4563. else {
  4564. this.conversion = Range.conversion(range.start, range.end, this.width);
  4565. }
  4566. };
  4567. /**
  4568. * Convert a position on screen (pixels) to a datetime
  4569. * Before this method can be used, the method _updateConversion must be
  4570. * executed once.
  4571. * @param {int} x Position on the screen in pixels
  4572. * @return {Date} time The datetime the corresponds with given position x
  4573. */
  4574. ItemSet.prototype.toTime = function toTime(x) {
  4575. var conversion = this.conversion;
  4576. return new Date(x / conversion.factor + conversion.offset);
  4577. };
  4578. /**
  4579. * Convert a datetime (Date object) into a position on the screen
  4580. * Before this method can be used, the method _updateConversion must be
  4581. * executed once.
  4582. * @param {Date} time A date
  4583. * @return {int} x The position on the screen in pixels which corresponds
  4584. * with the given date.
  4585. */
  4586. ItemSet.prototype.toScreen = function toScreen(time) {
  4587. var conversion = this.conversion;
  4588. return (time.valueOf() - conversion.offset) * conversion.factor;
  4589. };
  4590. /**
  4591. * @constructor Item
  4592. * @param {ItemSet} parent
  4593. * @param {Object} data Object containing (optional) parameters type,
  4594. * start, end, content, group, className.
  4595. * @param {Object} [options] Options to set initial property values
  4596. * @param {Object} [defaultOptions] default options
  4597. * // TODO: describe available options
  4598. */
  4599. function Item (parent, data, options, defaultOptions) {
  4600. this.parent = parent;
  4601. this.data = data;
  4602. this.dom = null;
  4603. this.options = options || {};
  4604. this.defaultOptions = defaultOptions || {};
  4605. this.selected = false;
  4606. this.visible = false;
  4607. this.top = 0;
  4608. this.left = 0;
  4609. this.width = 0;
  4610. this.height = 0;
  4611. }
  4612. /**
  4613. * Select current item
  4614. */
  4615. Item.prototype.select = function select() {
  4616. this.selected = true;
  4617. };
  4618. /**
  4619. * Unselect current item
  4620. */
  4621. Item.prototype.unselect = function unselect() {
  4622. this.selected = false;
  4623. };
  4624. /**
  4625. * Show the Item in the DOM (when not already visible)
  4626. * @return {Boolean} changed
  4627. */
  4628. Item.prototype.show = function show() {
  4629. return false;
  4630. };
  4631. /**
  4632. * Hide the Item from the DOM (when visible)
  4633. * @return {Boolean} changed
  4634. */
  4635. Item.prototype.hide = function hide() {
  4636. return false;
  4637. };
  4638. /**
  4639. * Repaint the item
  4640. * @return {Boolean} changed
  4641. */
  4642. Item.prototype.repaint = function repaint() {
  4643. // should be implemented by the item
  4644. return false;
  4645. };
  4646. /**
  4647. * Reflow the item
  4648. * @return {Boolean} resized
  4649. */
  4650. Item.prototype.reflow = function reflow() {
  4651. // should be implemented by the item
  4652. return false;
  4653. };
  4654. /**
  4655. * @constructor ItemBox
  4656. * @extends Item
  4657. * @param {ItemSet} parent
  4658. * @param {Object} data Object containing parameters start
  4659. * content, className.
  4660. * @param {Object} [options] Options to set initial property values
  4661. * @param {Object} [defaultOptions] default options
  4662. * // TODO: describe available options
  4663. */
  4664. function ItemBox (parent, data, options, defaultOptions) {
  4665. this.props = {
  4666. dot: {
  4667. left: 0,
  4668. top: 0,
  4669. width: 0,
  4670. height: 0
  4671. },
  4672. line: {
  4673. top: 0,
  4674. left: 0,
  4675. width: 0,
  4676. height: 0
  4677. }
  4678. };
  4679. Item.call(this, parent, data, options, defaultOptions);
  4680. }
  4681. ItemBox.prototype = new Item (null, null);
  4682. /**
  4683. * Select the item
  4684. * @override
  4685. */
  4686. ItemBox.prototype.select = function select() {
  4687. this.selected = true;
  4688. // TODO: select and unselect
  4689. };
  4690. /**
  4691. * Unselect the item
  4692. * @override
  4693. */
  4694. ItemBox.prototype.unselect = function unselect() {
  4695. this.selected = false;
  4696. // TODO: select and unselect
  4697. };
  4698. /**
  4699. * Repaint the item
  4700. * @return {Boolean} changed
  4701. */
  4702. ItemBox.prototype.repaint = function repaint() {
  4703. // TODO: make an efficient repaint
  4704. var changed = false;
  4705. var dom = this.dom;
  4706. if (!dom) {
  4707. this._create();
  4708. dom = this.dom;
  4709. changed = true;
  4710. }
  4711. if (dom) {
  4712. if (!this.parent) {
  4713. throw new Error('Cannot repaint item: no parent attached');
  4714. }
  4715. var foreground = this.parent.getForeground();
  4716. if (!foreground) {
  4717. throw new Error('Cannot repaint time axis: ' +
  4718. 'parent has no foreground container element');
  4719. }
  4720. var background = this.parent.getBackground();
  4721. if (!background) {
  4722. throw new Error('Cannot repaint time axis: ' +
  4723. 'parent has no background container element');
  4724. }
  4725. var axis = this.parent.getAxis();
  4726. if (!background) {
  4727. throw new Error('Cannot repaint time axis: ' +
  4728. 'parent has no axis container element');
  4729. }
  4730. if (!dom.box.parentNode) {
  4731. foreground.appendChild(dom.box);
  4732. changed = true;
  4733. }
  4734. if (!dom.line.parentNode) {
  4735. background.appendChild(dom.line);
  4736. changed = true;
  4737. }
  4738. if (!dom.dot.parentNode) {
  4739. axis.appendChild(dom.dot);
  4740. changed = true;
  4741. }
  4742. // update contents
  4743. if (this.data.content != this.content) {
  4744. this.content = this.data.content;
  4745. if (this.content instanceof Element) {
  4746. dom.content.innerHTML = '';
  4747. dom.content.appendChild(this.content);
  4748. }
  4749. else if (this.data.content != undefined) {
  4750. dom.content.innerHTML = this.content;
  4751. }
  4752. else {
  4753. throw new Error('Property "content" missing in item ' + this.data.id);
  4754. }
  4755. changed = true;
  4756. }
  4757. // update class
  4758. var className = (this.data.className? ' ' + this.data.className : '') +
  4759. (this.selected ? ' selected' : '');
  4760. if (this.className != className) {
  4761. this.className = className;
  4762. dom.box.className = 'item box' + className;
  4763. dom.line.className = 'item line' + className;
  4764. dom.dot.className = 'item dot' + className;
  4765. changed = true;
  4766. }
  4767. }
  4768. return changed;
  4769. };
  4770. /**
  4771. * Show the item in the DOM (when not already visible). The items DOM will
  4772. * be created when needed.
  4773. * @return {Boolean} changed
  4774. */
  4775. ItemBox.prototype.show = function show() {
  4776. if (!this.dom || !this.dom.box.parentNode) {
  4777. return this.repaint();
  4778. }
  4779. else {
  4780. return false;
  4781. }
  4782. };
  4783. /**
  4784. * Hide the item from the DOM (when visible)
  4785. * @return {Boolean} changed
  4786. */
  4787. ItemBox.prototype.hide = function hide() {
  4788. var changed = false,
  4789. dom = this.dom;
  4790. if (dom) {
  4791. if (dom.box.parentNode) {
  4792. dom.box.parentNode.removeChild(dom.box);
  4793. changed = true;
  4794. }
  4795. if (dom.line.parentNode) {
  4796. dom.line.parentNode.removeChild(dom.line);
  4797. }
  4798. if (dom.dot.parentNode) {
  4799. dom.dot.parentNode.removeChild(dom.dot);
  4800. }
  4801. }
  4802. return changed;
  4803. };
  4804. /**
  4805. * Reflow the item: calculate its actual size and position from the DOM
  4806. * @return {boolean} resized returns true if the axis is resized
  4807. * @override
  4808. */
  4809. ItemBox.prototype.reflow = function reflow() {
  4810. var changed = 0,
  4811. update,
  4812. dom,
  4813. props,
  4814. options,
  4815. margin,
  4816. start,
  4817. align,
  4818. orientation,
  4819. top,
  4820. left,
  4821. data,
  4822. range;
  4823. if (this.data.start == undefined) {
  4824. throw new Error('Property "start" missing in item ' + this.data.id);
  4825. }
  4826. data = this.data;
  4827. range = this.parent && this.parent.range;
  4828. if (data && range) {
  4829. // TODO: account for the width of the item. Take some margin
  4830. this.visible = (data.start > range.start) && (data.start < range.end);
  4831. }
  4832. else {
  4833. this.visible = false;
  4834. }
  4835. if (this.visible) {
  4836. dom = this.dom;
  4837. if (dom) {
  4838. update = util.updateProperty;
  4839. props = this.props;
  4840. options = this.options;
  4841. start = this.parent.toScreen(this.data.start);
  4842. align = options.align || this.defaultOptions.align;
  4843. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  4844. orientation = options.orientation || this.defaultOptions.orientation;
  4845. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4846. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4847. changed += update(props.line, 'width', dom.line.offsetWidth);
  4848. changed += update(props.line, 'height', dom.line.offsetHeight);
  4849. changed += update(props.line, 'top', dom.line.offsetTop);
  4850. changed += update(this, 'width', dom.box.offsetWidth);
  4851. changed += update(this, 'height', dom.box.offsetHeight);
  4852. if (align == 'right') {
  4853. left = start - this.width;
  4854. }
  4855. else if (align == 'left') {
  4856. left = start;
  4857. }
  4858. else {
  4859. // default or 'center'
  4860. left = start - this.width / 2;
  4861. }
  4862. changed += update(this, 'left', left);
  4863. changed += update(props.line, 'left', start - props.line.width / 2);
  4864. changed += update(props.dot, 'left', start - props.dot.width / 2);
  4865. changed += update(props.dot, 'top', -props.dot.height / 2);
  4866. if (orientation == 'top') {
  4867. top = margin;
  4868. changed += update(this, 'top', top);
  4869. }
  4870. else {
  4871. // default or 'bottom'
  4872. var parentHeight = this.parent.height;
  4873. top = parentHeight - this.height - margin;
  4874. changed += update(this, 'top', top);
  4875. }
  4876. }
  4877. else {
  4878. changed += 1;
  4879. }
  4880. }
  4881. return (changed > 0);
  4882. };
  4883. /**
  4884. * Create an items DOM
  4885. * @private
  4886. */
  4887. ItemBox.prototype._create = function _create() {
  4888. var dom = this.dom;
  4889. if (!dom) {
  4890. this.dom = dom = {};
  4891. // create the box
  4892. dom.box = document.createElement('DIV');
  4893. // className is updated in repaint()
  4894. // contents box (inside the background box). used for making margins
  4895. dom.content = document.createElement('DIV');
  4896. dom.content.className = 'content';
  4897. dom.box.appendChild(dom.content);
  4898. // line to axis
  4899. dom.line = document.createElement('DIV');
  4900. dom.line.className = 'line';
  4901. // dot on axis
  4902. dom.dot = document.createElement('DIV');
  4903. dom.dot.className = 'dot';
  4904. }
  4905. };
  4906. /**
  4907. * Reposition the item, recalculate its left, top, and width, using the current
  4908. * range and size of the items itemset
  4909. * @override
  4910. */
  4911. ItemBox.prototype.reposition = function reposition() {
  4912. var dom = this.dom,
  4913. props = this.props,
  4914. orientation = this.options.orientation || this.defaultOptions.orientation;
  4915. if (dom) {
  4916. var box = dom.box,
  4917. line = dom.line,
  4918. dot = dom.dot;
  4919. box.style.left = this.left + 'px';
  4920. box.style.top = this.top + 'px';
  4921. line.style.left = props.line.left + 'px';
  4922. if (orientation == 'top') {
  4923. line.style.top = 0 + 'px';
  4924. line.style.height = this.top + 'px';
  4925. }
  4926. else {
  4927. // orientation 'bottom'
  4928. line.style.top = (this.top + this.height) + 'px';
  4929. line.style.height = Math.max(this.parent.height - this.top - this.height +
  4930. this.props.dot.height / 2, 0) + 'px';
  4931. }
  4932. dot.style.left = props.dot.left + 'px';
  4933. dot.style.top = props.dot.top + 'px';
  4934. }
  4935. };
  4936. /**
  4937. * @constructor ItemPoint
  4938. * @extends Item
  4939. * @param {ItemSet} parent
  4940. * @param {Object} data Object containing parameters start
  4941. * content, className.
  4942. * @param {Object} [options] Options to set initial property values
  4943. * @param {Object} [defaultOptions] default options
  4944. * // TODO: describe available options
  4945. */
  4946. function ItemPoint (parent, data, options, defaultOptions) {
  4947. this.props = {
  4948. dot: {
  4949. top: 0,
  4950. width: 0,
  4951. height: 0
  4952. },
  4953. content: {
  4954. height: 0,
  4955. marginLeft: 0
  4956. }
  4957. };
  4958. Item.call(this, parent, data, options, defaultOptions);
  4959. }
  4960. ItemPoint.prototype = new Item (null, null);
  4961. /**
  4962. * Select the item
  4963. * @override
  4964. */
  4965. ItemPoint.prototype.select = function select() {
  4966. this.selected = true;
  4967. // TODO: select and unselect
  4968. };
  4969. /**
  4970. * Unselect the item
  4971. * @override
  4972. */
  4973. ItemPoint.prototype.unselect = function unselect() {
  4974. this.selected = false;
  4975. // TODO: select and unselect
  4976. };
  4977. /**
  4978. * Repaint the item
  4979. * @return {Boolean} changed
  4980. */
  4981. ItemPoint.prototype.repaint = function repaint() {
  4982. // TODO: make an efficient repaint
  4983. var changed = false;
  4984. var dom = this.dom;
  4985. if (!dom) {
  4986. this._create();
  4987. dom = this.dom;
  4988. changed = true;
  4989. }
  4990. if (dom) {
  4991. if (!this.parent) {
  4992. throw new Error('Cannot repaint item: no parent attached');
  4993. }
  4994. var foreground = this.parent.getForeground();
  4995. if (!foreground) {
  4996. throw new Error('Cannot repaint time axis: ' +
  4997. 'parent has no foreground container element');
  4998. }
  4999. if (!dom.point.parentNode) {
  5000. foreground.appendChild(dom.point);
  5001. foreground.appendChild(dom.point);
  5002. changed = true;
  5003. }
  5004. // update contents
  5005. if (this.data.content != this.content) {
  5006. this.content = this.data.content;
  5007. if (this.content instanceof Element) {
  5008. dom.content.innerHTML = '';
  5009. dom.content.appendChild(this.content);
  5010. }
  5011. else if (this.data.content != undefined) {
  5012. dom.content.innerHTML = this.content;
  5013. }
  5014. else {
  5015. throw new Error('Property "content" missing in item ' + this.data.id);
  5016. }
  5017. changed = true;
  5018. }
  5019. // update class
  5020. var className = (this.data.className? ' ' + this.data.className : '') +
  5021. (this.selected ? ' selected' : '');
  5022. if (this.className != className) {
  5023. this.className = className;
  5024. dom.point.className = 'item point' + className;
  5025. changed = true;
  5026. }
  5027. }
  5028. return changed;
  5029. };
  5030. /**
  5031. * Show the item in the DOM (when not already visible). The items DOM will
  5032. * be created when needed.
  5033. * @return {Boolean} changed
  5034. */
  5035. ItemPoint.prototype.show = function show() {
  5036. if (!this.dom || !this.dom.point.parentNode) {
  5037. return this.repaint();
  5038. }
  5039. else {
  5040. return false;
  5041. }
  5042. };
  5043. /**
  5044. * Hide the item from the DOM (when visible)
  5045. * @return {Boolean} changed
  5046. */
  5047. ItemPoint.prototype.hide = function hide() {
  5048. var changed = false,
  5049. dom = this.dom;
  5050. if (dom) {
  5051. if (dom.point.parentNode) {
  5052. dom.point.parentNode.removeChild(dom.point);
  5053. changed = true;
  5054. }
  5055. }
  5056. return changed;
  5057. };
  5058. /**
  5059. * Reflow the item: calculate its actual size from the DOM
  5060. * @return {boolean} resized returns true if the axis is resized
  5061. * @override
  5062. */
  5063. ItemPoint.prototype.reflow = function reflow() {
  5064. var changed = 0,
  5065. update,
  5066. dom,
  5067. props,
  5068. options,
  5069. margin,
  5070. orientation,
  5071. start,
  5072. top,
  5073. data,
  5074. range;
  5075. if (this.data.start == undefined) {
  5076. throw new Error('Property "start" missing in item ' + this.data.id);
  5077. }
  5078. data = this.data;
  5079. range = this.parent && this.parent.range;
  5080. if (data && range) {
  5081. // TODO: account for the width of the item. Take some margin
  5082. this.visible = (data.start > range.start) && (data.start < range.end);
  5083. }
  5084. else {
  5085. this.visible = false;
  5086. }
  5087. if (this.visible) {
  5088. dom = this.dom;
  5089. if (dom) {
  5090. update = util.updateProperty;
  5091. props = this.props;
  5092. options = this.options;
  5093. orientation = options.orientation || this.defaultOptions.orientation;
  5094. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5095. start = this.parent.toScreen(this.data.start);
  5096. changed += update(this, 'width', dom.point.offsetWidth);
  5097. changed += update(this, 'height', dom.point.offsetHeight);
  5098. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5099. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5100. changed += update(props.content, 'height', dom.content.offsetHeight);
  5101. if (orientation == 'top') {
  5102. top = margin;
  5103. }
  5104. else {
  5105. // default or 'bottom'
  5106. var parentHeight = this.parent.height;
  5107. top = Math.max(parentHeight - this.height - margin, 0);
  5108. }
  5109. changed += update(this, 'top', top);
  5110. changed += update(this, 'left', start - props.dot.width / 2);
  5111. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5112. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5113. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5114. }
  5115. else {
  5116. changed += 1;
  5117. }
  5118. }
  5119. return (changed > 0);
  5120. };
  5121. /**
  5122. * Create an items DOM
  5123. * @private
  5124. */
  5125. ItemPoint.prototype._create = function _create() {
  5126. var dom = this.dom;
  5127. if (!dom) {
  5128. this.dom = dom = {};
  5129. // background box
  5130. dom.point = document.createElement('div');
  5131. // className is updated in repaint()
  5132. // contents box, right from the dot
  5133. dom.content = document.createElement('div');
  5134. dom.content.className = 'content';
  5135. dom.point.appendChild(dom.content);
  5136. // dot at start
  5137. dom.dot = document.createElement('div');
  5138. dom.dot.className = 'dot';
  5139. dom.point.appendChild(dom.dot);
  5140. }
  5141. };
  5142. /**
  5143. * Reposition the item, recalculate its left, top, and width, using the current
  5144. * range and size of the items itemset
  5145. * @override
  5146. */
  5147. ItemPoint.prototype.reposition = function reposition() {
  5148. var dom = this.dom,
  5149. props = this.props;
  5150. if (dom) {
  5151. dom.point.style.top = this.top + 'px';
  5152. dom.point.style.left = this.left + 'px';
  5153. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5154. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5155. dom.dot.style.top = props.dot.top + 'px';
  5156. }
  5157. };
  5158. /**
  5159. * @constructor ItemRange
  5160. * @extends Item
  5161. * @param {ItemSet} parent
  5162. * @param {Object} data Object containing parameters start, end
  5163. * content, className.
  5164. * @param {Object} [options] Options to set initial property values
  5165. * @param {Object} [defaultOptions] default options
  5166. * // TODO: describe available options
  5167. */
  5168. function ItemRange (parent, data, options, defaultOptions) {
  5169. this.props = {
  5170. content: {
  5171. left: 0,
  5172. width: 0
  5173. }
  5174. };
  5175. Item.call(this, parent, data, options, defaultOptions);
  5176. }
  5177. ItemRange.prototype = new Item (null, null);
  5178. /**
  5179. * Select the item
  5180. * @override
  5181. */
  5182. ItemRange.prototype.select = function select() {
  5183. this.selected = true;
  5184. // TODO: select and unselect
  5185. };
  5186. /**
  5187. * Unselect the item
  5188. * @override
  5189. */
  5190. ItemRange.prototype.unselect = function unselect() {
  5191. this.selected = false;
  5192. // TODO: select and unselect
  5193. };
  5194. /**
  5195. * Repaint the item
  5196. * @return {Boolean} changed
  5197. */
  5198. ItemRange.prototype.repaint = function repaint() {
  5199. // TODO: make an efficient repaint
  5200. var changed = false;
  5201. var dom = this.dom;
  5202. if (!dom) {
  5203. this._create();
  5204. dom = this.dom;
  5205. changed = true;
  5206. }
  5207. if (dom) {
  5208. if (!this.parent) {
  5209. throw new Error('Cannot repaint item: no parent attached');
  5210. }
  5211. var foreground = this.parent.getForeground();
  5212. if (!foreground) {
  5213. throw new Error('Cannot repaint time axis: ' +
  5214. 'parent has no foreground container element');
  5215. }
  5216. if (!dom.box.parentNode) {
  5217. foreground.appendChild(dom.box);
  5218. changed = true;
  5219. }
  5220. // update content
  5221. if (this.data.content != this.content) {
  5222. this.content = this.data.content;
  5223. if (this.content instanceof Element) {
  5224. dom.content.innerHTML = '';
  5225. dom.content.appendChild(this.content);
  5226. }
  5227. else if (this.data.content != undefined) {
  5228. dom.content.innerHTML = this.content;
  5229. }
  5230. else {
  5231. throw new Error('Property "content" missing in item ' + this.data.id);
  5232. }
  5233. changed = true;
  5234. }
  5235. // update class
  5236. var className = this.data.className ? ('' + this.data.className) : '';
  5237. if (this.className != className) {
  5238. this.className = className;
  5239. dom.box.className = 'item range' + className;
  5240. changed = true;
  5241. }
  5242. }
  5243. return changed;
  5244. };
  5245. /**
  5246. * Show the item in the DOM (when not already visible). The items DOM will
  5247. * be created when needed.
  5248. * @return {Boolean} changed
  5249. */
  5250. ItemRange.prototype.show = function show() {
  5251. if (!this.dom || !this.dom.box.parentNode) {
  5252. return this.repaint();
  5253. }
  5254. else {
  5255. return false;
  5256. }
  5257. };
  5258. /**
  5259. * Hide the item from the DOM (when visible)
  5260. * @return {Boolean} changed
  5261. */
  5262. ItemRange.prototype.hide = function hide() {
  5263. var changed = false,
  5264. dom = this.dom;
  5265. if (dom) {
  5266. if (dom.box.parentNode) {
  5267. dom.box.parentNode.removeChild(dom.box);
  5268. changed = true;
  5269. }
  5270. }
  5271. return changed;
  5272. };
  5273. /**
  5274. * Reflow the item: calculate its actual size from the DOM
  5275. * @return {boolean} resized returns true if the axis is resized
  5276. * @override
  5277. */
  5278. ItemRange.prototype.reflow = function reflow() {
  5279. var changed = 0,
  5280. dom,
  5281. props,
  5282. options,
  5283. margin,
  5284. padding,
  5285. parent,
  5286. start,
  5287. end,
  5288. data,
  5289. range,
  5290. update,
  5291. box,
  5292. parentWidth,
  5293. contentLeft,
  5294. orientation,
  5295. top;
  5296. if (this.data.start == undefined) {
  5297. throw new Error('Property "start" missing in item ' + this.data.id);
  5298. }
  5299. if (this.data.end == undefined) {
  5300. throw new Error('Property "end" missing in item ' + this.data.id);
  5301. }
  5302. data = this.data;
  5303. range = this.parent && this.parent.range;
  5304. if (data && range) {
  5305. // TODO: account for the width of the item. Take some margin
  5306. this.visible = (data.start < range.end) && (data.end > range.start);
  5307. }
  5308. else {
  5309. this.visible = false;
  5310. }
  5311. if (this.visible) {
  5312. dom = this.dom;
  5313. if (dom) {
  5314. props = this.props;
  5315. options = this.options;
  5316. parent = this.parent;
  5317. start = parent.toScreen(this.data.start);
  5318. end = parent.toScreen(this.data.end);
  5319. update = util.updateProperty;
  5320. box = dom.box;
  5321. parentWidth = parent.width;
  5322. orientation = options.orientation || this.defaultOptions.orientation;
  5323. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5324. padding = options.padding || this.defaultOptions.padding;
  5325. changed += update(props.content, 'width', dom.content.offsetWidth);
  5326. changed += update(this, 'height', box.offsetHeight);
  5327. // limit the width of the this, as browsers cannot draw very wide divs
  5328. if (start < -parentWidth) {
  5329. start = -parentWidth;
  5330. }
  5331. if (end > 2 * parentWidth) {
  5332. end = 2 * parentWidth;
  5333. }
  5334. // when range exceeds left of the window, position the contents at the left of the visible area
  5335. if (start < 0) {
  5336. contentLeft = Math.min(-start,
  5337. (end - start - props.content.width - 2 * padding));
  5338. // TODO: remove the need for options.padding. it's terrible.
  5339. }
  5340. else {
  5341. contentLeft = 0;
  5342. }
  5343. changed += update(props.content, 'left', contentLeft);
  5344. if (orientation == 'top') {
  5345. top = margin;
  5346. changed += update(this, 'top', top);
  5347. }
  5348. else {
  5349. // default or 'bottom'
  5350. top = parent.height - this.height - margin;
  5351. changed += update(this, 'top', top);
  5352. }
  5353. changed += update(this, 'left', start);
  5354. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5355. }
  5356. else {
  5357. changed += 1;
  5358. }
  5359. }
  5360. return (changed > 0);
  5361. };
  5362. /**
  5363. * Create an items DOM
  5364. * @private
  5365. */
  5366. ItemRange.prototype._create = function _create() {
  5367. var dom = this.dom;
  5368. if (!dom) {
  5369. this.dom = dom = {};
  5370. // background box
  5371. dom.box = document.createElement('div');
  5372. // className is updated in repaint()
  5373. // contents box
  5374. dom.content = document.createElement('div');
  5375. dom.content.className = 'content';
  5376. dom.box.appendChild(dom.content);
  5377. }
  5378. };
  5379. /**
  5380. * Reposition the item, recalculate its left, top, and width, using the current
  5381. * range and size of the items itemset
  5382. * @override
  5383. */
  5384. ItemRange.prototype.reposition = function reposition() {
  5385. var dom = this.dom,
  5386. props = this.props;
  5387. if (dom) {
  5388. dom.box.style.top = this.top + 'px';
  5389. dom.box.style.left = this.left + 'px';
  5390. dom.box.style.width = this.width + 'px';
  5391. dom.content.style.left = props.content.left + 'px';
  5392. }
  5393. };
  5394. /**
  5395. * @constructor Group
  5396. * @param {GroupSet} parent
  5397. * @param {Number | String} groupId
  5398. * @param {Object} [options] Options to set initial property values
  5399. * // TODO: describe available options
  5400. * @extends Component
  5401. */
  5402. function Group (parent, groupId, options) {
  5403. this.id = util.randomUUID();
  5404. this.parent = parent;
  5405. this.groupId = groupId;
  5406. this.itemsData = null; // DataSet
  5407. this.itemset = null; // ItemSet
  5408. this.options = options || {};
  5409. this.options.top = 0;
  5410. this.top = 0;
  5411. this.left = 0;
  5412. this.width = 0;
  5413. this.height = 0;
  5414. }
  5415. Group.prototype = new Component();
  5416. // TODO: comment
  5417. Group.prototype.setOptions = Component.prototype.setOptions;
  5418. /**
  5419. * Get the container element of the panel, which can be used by a child to
  5420. * add its own widgets.
  5421. * @returns {HTMLElement} container
  5422. */
  5423. Group.prototype.getContainer = function () {
  5424. return this.parent.getContainer();
  5425. };
  5426. /**
  5427. * Set item set for the group. The group will create a view on the itemset,
  5428. * filtered by the groups id.
  5429. * @param {DataSet | DataView} items
  5430. */
  5431. Group.prototype.setItems = function setItems(items) {
  5432. if (this.itemset) {
  5433. // remove current item set
  5434. this.itemset.hide();
  5435. this.itemset.setItems();
  5436. this.parent.controller.remove(this.itemset);
  5437. this.itemset = null;
  5438. }
  5439. if (items) {
  5440. var groupId = this.groupId;
  5441. var itemsetOptions = Object.create(this.options);
  5442. this.itemset = new ItemSet(this, null, itemsetOptions);
  5443. this.itemset.setRange(this.parent.range);
  5444. this.view = new DataView(items, {
  5445. filter: function (item) {
  5446. return item.group == groupId;
  5447. }
  5448. });
  5449. this.itemset.setItems(this.view);
  5450. this.parent.controller.add(this.itemset);
  5451. }
  5452. };
  5453. /**
  5454. * Repaint the item
  5455. * @return {Boolean} changed
  5456. */
  5457. Group.prototype.repaint = function repaint() {
  5458. return false;
  5459. };
  5460. /**
  5461. * Reflow the item
  5462. * @return {Boolean} resized
  5463. */
  5464. Group.prototype.reflow = function reflow() {
  5465. var changed = 0,
  5466. update = util.updateProperty;
  5467. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5468. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5469. return (changed > 0);
  5470. };
  5471. /**
  5472. * An GroupSet holds a set of groups
  5473. * @param {Component} parent
  5474. * @param {Component[]} [depends] Components on which this components depends
  5475. * (except for the parent)
  5476. * @param {Object} [options] See GroupSet.setOptions for the available
  5477. * options.
  5478. * @constructor GroupSet
  5479. * @extends Panel
  5480. */
  5481. function GroupSet(parent, depends, options) {
  5482. this.id = util.randomUUID();
  5483. this.parent = parent;
  5484. this.depends = depends;
  5485. this.options = options || {};
  5486. this.range = null; // Range or Object {start: number, end: number}
  5487. this.itemsData = null; // DataSet with items
  5488. this.groupsData = null; // DataSet with groups
  5489. this.groups = {}; // map with groups
  5490. // changes in groups are queued key/value map containing id/action
  5491. this.queue = {};
  5492. var me = this;
  5493. this.listeners = {
  5494. 'add': function (event, params) {
  5495. me._onAdd(params.items);
  5496. },
  5497. 'update': function (event, params) {
  5498. me._onUpdate(params.items);
  5499. },
  5500. 'remove': function (event, params) {
  5501. me._onRemove(params.items);
  5502. }
  5503. };
  5504. }
  5505. GroupSet.prototype = new Panel();
  5506. /**
  5507. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5508. * @param {Object} [options] The following options are available:
  5509. * {String | function} groupsOrder
  5510. * TODO: describe options
  5511. */
  5512. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5513. GroupSet.prototype.setRange = function (range) {
  5514. // TODO: implement setRange
  5515. };
  5516. /**
  5517. * Set items
  5518. * @param {vis.DataSet | null} items
  5519. */
  5520. GroupSet.prototype.setItems = function setItems(items) {
  5521. this.itemsData = items;
  5522. for (var id in this.groups) {
  5523. if (this.groups.hasOwnProperty(id)) {
  5524. var group = this.groups[id];
  5525. group.setItems(items);
  5526. }
  5527. }
  5528. };
  5529. /**
  5530. * Get items
  5531. * @return {vis.DataSet | null} items
  5532. */
  5533. GroupSet.prototype.getItems = function getItems() {
  5534. return this.itemsData;
  5535. };
  5536. /**
  5537. * Set range (start and end).
  5538. * @param {Range | Object} range A Range or an object containing start and end.
  5539. */
  5540. GroupSet.prototype.setRange = function setRange(range) {
  5541. this.range = range;
  5542. };
  5543. /**
  5544. * Set groups
  5545. * @param {vis.DataSet} groups
  5546. */
  5547. GroupSet.prototype.setGroups = function setGroups(groups) {
  5548. var me = this,
  5549. ids;
  5550. // unsubscribe from current dataset
  5551. if (this.groupsData) {
  5552. util.forEach(this.listeners, function (callback, event) {
  5553. me.groupsData.unsubscribe(event, callback);
  5554. });
  5555. // remove all drawn groups
  5556. ids = this.groupsData.getIds();
  5557. this._onRemove(ids);
  5558. }
  5559. // replace the dataset
  5560. if (!groups) {
  5561. this.groupsData = null;
  5562. }
  5563. else if (groups instanceof DataSet) {
  5564. this.groupsData = groups;
  5565. }
  5566. else {
  5567. this.groupsData = new DataSet({
  5568. fieldTypes: {
  5569. start: 'Date',
  5570. end: 'Date'
  5571. }
  5572. });
  5573. this.groupsData.add(groups);
  5574. }
  5575. if (this.groupsData) {
  5576. // subscribe to new dataset
  5577. var id = this.id;
  5578. util.forEach(this.listeners, function (callback, event) {
  5579. me.groupsData.subscribe(event, callback, id);
  5580. });
  5581. // draw all new groups
  5582. ids = this.groupsData.getIds();
  5583. this._onAdd(ids);
  5584. }
  5585. };
  5586. /**
  5587. * Get groups
  5588. * @return {vis.DataSet | null} groups
  5589. */
  5590. GroupSet.prototype.getGroups = function getGroups() {
  5591. return this.groupsData;
  5592. };
  5593. /**
  5594. * Repaint the component
  5595. * @return {Boolean} changed
  5596. */
  5597. GroupSet.prototype.repaint = function repaint() {
  5598. var changed = 0,
  5599. update = util.updateProperty,
  5600. asSize = util.option.asSize,
  5601. options = this.options,
  5602. frame = this.frame;
  5603. if (!frame) {
  5604. frame = document.createElement('div');
  5605. frame.className = 'groupset';
  5606. var className = options.className;
  5607. if (className) {
  5608. util.addClassName(frame, util.option.asString(className));
  5609. }
  5610. this.frame = frame;
  5611. changed += 1;
  5612. }
  5613. if (!this.parent) {
  5614. throw new Error('Cannot repaint groupset: no parent attached');
  5615. }
  5616. var parentContainer = this.parent.getContainer();
  5617. if (!parentContainer) {
  5618. throw new Error('Cannot repaint groupset: parent has no container element');
  5619. }
  5620. if (!frame.parentNode) {
  5621. parentContainer.appendChild(frame);
  5622. changed += 1;
  5623. }
  5624. // reposition frame
  5625. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  5626. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  5627. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  5628. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  5629. var me = this,
  5630. queue = this.queue,
  5631. groups = this.groups,
  5632. groupsData = this.groupsData;
  5633. // show/hide added/changed/removed items
  5634. var ids = Object.keys(queue);
  5635. if (ids.length) {
  5636. ids.forEach(function (id) {
  5637. var action = queue[id];
  5638. var group = groups[id];
  5639. //noinspection FallthroughInSwitchStatementJS
  5640. switch (action) {
  5641. case 'add':
  5642. case 'update':
  5643. if (!group) {
  5644. var groupOptions = Object.create(me.options);
  5645. group = new Group(me, id, groupOptions);
  5646. group.setItems(me.itemsData); // attach items data
  5647. groups[id] = group;
  5648. me.controller.add(group);
  5649. }
  5650. // TODO: update group data
  5651. group.data = groupsData.get(id);
  5652. delete queue[id];
  5653. break;
  5654. case 'remove':
  5655. if (group) {
  5656. group.setItems(); // detach items data
  5657. delete groups[id];
  5658. me.controller.remove(group);
  5659. }
  5660. // update lists
  5661. delete queue[id];
  5662. break;
  5663. default:
  5664. console.log('Error: unknown action "' + action + '"');
  5665. }
  5666. });
  5667. // the groupset depends on each of the groups
  5668. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  5669. // TODO: apply dependencies of the groupset
  5670. // update the top positions of the groups in the correct order
  5671. var orderedGroups = this.groupsData.getIds({
  5672. order: this.options.groupsOrder
  5673. });
  5674. for (var i = 0; i < orderedGroups.length; i++) {
  5675. (function (group, prevGroup) {
  5676. var top = 0;
  5677. if (prevGroup) {
  5678. top = function () {
  5679. // TODO: top must reckon with options.maxHeight
  5680. return prevGroup.top + prevGroup.height;
  5681. }
  5682. }
  5683. group.setOptions({
  5684. top: top
  5685. });
  5686. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  5687. }
  5688. changed++;
  5689. }
  5690. return (changed > 0);
  5691. };
  5692. /**
  5693. * Get container element
  5694. * @return {HTMLElement} container
  5695. */
  5696. GroupSet.prototype.getContainer = function getContainer() {
  5697. // TODO: replace later on with container element for holding itemsets
  5698. return this.frame;
  5699. };
  5700. /**
  5701. * Reflow the component
  5702. * @return {Boolean} resized
  5703. */
  5704. GroupSet.prototype.reflow = function reflow() {
  5705. var changed = 0,
  5706. options = this.options,
  5707. update = util.updateProperty,
  5708. asNumber = util.option.asNumber,
  5709. asSize = util.option.asSize,
  5710. frame = this.frame;
  5711. if (frame) {
  5712. var maxHeight = asNumber(options.maxHeight);
  5713. var fixedHeight = (asSize(options.height) != null);
  5714. var height;
  5715. if (fixedHeight) {
  5716. height = frame.offsetHeight;
  5717. }
  5718. else {
  5719. // height is not specified, calculate the sum of the height of all groups
  5720. height = 0;
  5721. for (var id in this.groups) {
  5722. if (this.groups.hasOwnProperty(id)) {
  5723. var group = this.groups[id];
  5724. height += group.height;
  5725. }
  5726. }
  5727. }
  5728. if (maxHeight != null) {
  5729. height = Math.min(height, maxHeight);
  5730. }
  5731. changed += update(this, 'height', height);
  5732. changed += update(this, 'top', frame.offsetTop);
  5733. changed += update(this, 'left', frame.offsetLeft);
  5734. changed += update(this, 'width', frame.offsetWidth);
  5735. }
  5736. return (changed > 0);
  5737. };
  5738. /**
  5739. * Hide the component from the DOM
  5740. * @return {Boolean} changed
  5741. */
  5742. GroupSet.prototype.hide = function hide() {
  5743. if (this.frame && this.frame.parentNode) {
  5744. this.frame.parentNode.removeChild(this.frame);
  5745. return true;
  5746. }
  5747. else {
  5748. return false;
  5749. }
  5750. };
  5751. /**
  5752. * Show the component in the DOM (when not already visible).
  5753. * A repaint will be executed when the component is not visible
  5754. * @return {Boolean} changed
  5755. */
  5756. GroupSet.prototype.show = function show() {
  5757. if (!this.frame || !this.frame.parentNode) {
  5758. return this.repaint();
  5759. }
  5760. else {
  5761. return false;
  5762. }
  5763. };
  5764. /**
  5765. * Handle updated groups
  5766. * @param {Number[]} ids
  5767. * @private
  5768. */
  5769. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  5770. this._toQueue(ids, 'update');
  5771. };
  5772. /**
  5773. * Handle changed groups
  5774. * @param {Number[]} ids
  5775. * @private
  5776. */
  5777. GroupSet.prototype._onAdd = function _onAdd(ids) {
  5778. this._toQueue(ids, 'add');
  5779. };
  5780. /**
  5781. * Handle removed groups
  5782. * @param {Number[]} ids
  5783. * @private
  5784. */
  5785. GroupSet.prototype._onRemove = function _onRemove(ids) {
  5786. this._toQueue(ids, 'remove');
  5787. };
  5788. /**
  5789. * Put groups in the queue to be added/updated/remove
  5790. * @param {Number[]} ids
  5791. * @param {String} action can be 'add', 'update', 'remove'
  5792. */
  5793. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  5794. var queue = this.queue;
  5795. ids.forEach(function (id) {
  5796. queue[id] = action;
  5797. });
  5798. if (this.controller) {
  5799. //this.requestReflow();
  5800. this.requestRepaint();
  5801. }
  5802. };
  5803. /**
  5804. * Create a timeline visualization
  5805. * @param {HTMLElement} container
  5806. * @param {vis.DataSet | Array | DataTable} [items]
  5807. * @param {Object} [options] See Timeline.setOptions for the available options.
  5808. * @constructor
  5809. */
  5810. function Timeline (container, items, options) {
  5811. var me = this;
  5812. this.options = util.extend({
  5813. orientation: 'bottom',
  5814. min: null,
  5815. max: null,
  5816. zoomMin: 10, // milliseconds
  5817. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  5818. // moveable: true, // TODO: option moveable
  5819. // zoomable: true, // TODO: option zoomable
  5820. showMinorLabels: true,
  5821. showMajorLabels: true,
  5822. autoResize: false
  5823. }, options);
  5824. // controller
  5825. this.controller = new Controller();
  5826. // root panel
  5827. if (!container) {
  5828. throw new Error('No container element provided');
  5829. }
  5830. var mainOptions = Object.create(this.options);
  5831. mainOptions.height = function () {
  5832. if (me.options.height) {
  5833. // fixed height
  5834. return me.options.height;
  5835. }
  5836. else {
  5837. // auto height
  5838. return me.timeaxis.height + me.content.height;
  5839. }
  5840. };
  5841. this.root = new RootPanel(container, mainOptions);
  5842. this.controller.add(this.root);
  5843. // range
  5844. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  5845. this.range = new Range({
  5846. start: now.clone().add('days', -3).valueOf(),
  5847. end: now.clone().add('days', 4).valueOf()
  5848. });
  5849. // TODO: reckon with options moveable and zoomable
  5850. this.range.subscribe(this.root, 'move', 'horizontal');
  5851. this.range.subscribe(this.root, 'zoom', 'horizontal');
  5852. this.range.on('rangechange', function () {
  5853. var force = true;
  5854. me.controller.requestReflow(force);
  5855. });
  5856. this.range.on('rangechanged', function () {
  5857. var force = true;
  5858. me.controller.requestReflow(force);
  5859. });
  5860. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  5861. // time axis
  5862. var timeaxisOptions = Object.create(mainOptions);
  5863. timeaxisOptions.range = this.range;
  5864. this.timeaxis = new TimeAxis(this.root, [], timeaxisOptions);
  5865. this.timeaxis.setRange(this.range);
  5866. this.controller.add(this.timeaxis);
  5867. // create itemset or groupset
  5868. this.setGroups(null);
  5869. this.itemsData = null; // DataSet
  5870. this.groupsData = null; // DataSet
  5871. // set data
  5872. if (items) {
  5873. this.setItems(items);
  5874. }
  5875. }
  5876. /**
  5877. * Set options
  5878. * @param {Object} options TODO: describe the available options
  5879. */
  5880. Timeline.prototype.setOptions = function (options) {
  5881. if (options) {
  5882. util.extend(this.options, options);
  5883. }
  5884. this.controller.reflow();
  5885. this.controller.repaint();
  5886. };
  5887. /**
  5888. * Set items
  5889. * @param {vis.DataSet | Array | DataTable | null} items
  5890. */
  5891. Timeline.prototype.setItems = function(items) {
  5892. var initialLoad = (this.itemsData == null);
  5893. // convert to type DataSet when needed
  5894. var newItemSet;
  5895. if (!items) {
  5896. newItemSet = null;
  5897. }
  5898. else if (items instanceof DataSet) {
  5899. newItemSet = items;
  5900. }
  5901. if (!(items instanceof DataSet)) {
  5902. newItemSet = new DataSet({
  5903. fieldTypes: {
  5904. start: 'Date',
  5905. end: 'Date'
  5906. }
  5907. });
  5908. newItemSet.add(items);
  5909. }
  5910. // set items
  5911. this.itemsData = newItemSet;
  5912. this.content.setItems(newItemSet);
  5913. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  5914. // apply the data range as range
  5915. var dataRange = this.getItemRange();
  5916. // add 5% on both sides
  5917. var min = dataRange.min;
  5918. var max = dataRange.max;
  5919. if (min != null && max != null) {
  5920. var interval = (max.valueOf() - min.valueOf());
  5921. min = new Date(min.valueOf() - interval * 0.05);
  5922. max = new Date(max.valueOf() + interval * 0.05);
  5923. }
  5924. // override specified start and/or end date
  5925. if (this.options.start != undefined) {
  5926. min = new Date(this.options.start.valueOf());
  5927. }
  5928. if (this.options.end != undefined) {
  5929. max = new Date(this.options.end.valueOf());
  5930. }
  5931. // apply range if there is a min or max available
  5932. if (min != null || max != null) {
  5933. this.range.setRange(min, max);
  5934. }
  5935. }
  5936. };
  5937. /**
  5938. * Set groups
  5939. * @param {vis.DataSet | Array | DataTable} groups
  5940. */
  5941. Timeline.prototype.setGroups = function(groups) {
  5942. var me = this;
  5943. this.groupsData = groups;
  5944. // switch content type between ItemSet or GroupSet when needed
  5945. var type = this.groupsData ? GroupSet : ItemSet;
  5946. if (!(this.content instanceof type)) {
  5947. // remove old content set
  5948. if (this.content) {
  5949. this.content.hide();
  5950. if (this.content.setItems) {
  5951. this.content.setItems(); // disconnect from items
  5952. }
  5953. if (this.content.setGroups) {
  5954. this.content.setGroups(); // disconnect from groups
  5955. }
  5956. this.controller.remove(this.content);
  5957. }
  5958. // create new content set
  5959. var options = Object.create(this.options);
  5960. util.extend(options, {
  5961. top: function () {
  5962. if (me.options.orientation == 'top') {
  5963. return me.timeaxis.height;
  5964. }
  5965. else {
  5966. return me.root.height - me.timeaxis.height - me.content.height;
  5967. }
  5968. },
  5969. height: function () {
  5970. if (me.options.height) {
  5971. return me.root.height - me.timeaxis.height;
  5972. }
  5973. else {
  5974. return null;
  5975. }
  5976. },
  5977. maxHeight: function () {
  5978. if (me.options.maxHeight) {
  5979. if (!util.isNumber(me.options.maxHeight)) {
  5980. throw new TypeError('Number expected for property maxHeight');
  5981. }
  5982. return me.options.maxHeight - me.timeaxis.height;
  5983. }
  5984. else {
  5985. return null;
  5986. }
  5987. }
  5988. });
  5989. this.content = new type(this.root, [this.timeaxis], options);
  5990. if (this.content.setRange) {
  5991. this.content.setRange(this.range);
  5992. }
  5993. if (this.content.setItems) {
  5994. this.content.setItems(this.itemsData);
  5995. }
  5996. if (this.content.setGroups) {
  5997. this.content.setGroups(this.groupsData);
  5998. }
  5999. this.controller.add(this.content);
  6000. }
  6001. };
  6002. /**
  6003. * Get the data range of the item set.
  6004. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6005. * When no minimum is found, min==null
  6006. * When no maximum is found, max==null
  6007. */
  6008. Timeline.prototype.getItemRange = function getItemRange() {
  6009. // calculate min from start filed
  6010. var itemsData = this.itemsData,
  6011. min = null,
  6012. max = null;
  6013. if (itemsData) {
  6014. // calculate the minimum value of the field 'start'
  6015. var minItem = itemsData.min('start');
  6016. min = minItem ? minItem.start.valueOf() : null;
  6017. // calculate maximum value of fields 'start' and 'end'
  6018. var maxStartItem = itemsData.max('start');
  6019. if (maxStartItem) {
  6020. max = maxStartItem.start.valueOf();
  6021. }
  6022. var maxEndItem = itemsData.max('end');
  6023. if (maxEndItem) {
  6024. if (max == null) {
  6025. max = maxEndItem.end.valueOf();
  6026. }
  6027. else {
  6028. max = Math.max(max, maxEndItem.end.valueOf());
  6029. }
  6030. }
  6031. }
  6032. return {
  6033. min: (min != null) ? new Date(min) : null,
  6034. max: (max != null) ? new Date(max) : null
  6035. };
  6036. };
  6037. (function(exports) {
  6038. /**
  6039. * Parse a text source containing data in DOT language into a JSON object.
  6040. * The object contains two lists: one with nodes and one with edges.
  6041. * @param {String} data Text containing a graph in DOT-notation
  6042. * @return {Object} graph An object containing two parameters:
  6043. * {Object[]} nodes
  6044. * {Object[]} edges
  6045. */
  6046. function parseDOT (data) {
  6047. dot = data;
  6048. return parseGraph();
  6049. }
  6050. // token types enumeration
  6051. var TOKENTYPE = {
  6052. NULL : 0,
  6053. DELIMITER : 1,
  6054. IDENTIFIER: 2,
  6055. UNKNOWN : 3
  6056. };
  6057. // map with all delimiters
  6058. var DELIMITERS = {
  6059. '{': true,
  6060. '}': true,
  6061. '[': true,
  6062. ']': true,
  6063. ';': true,
  6064. '=': true,
  6065. ',': true,
  6066. '->': true,
  6067. '--': true
  6068. };
  6069. var dot = ''; // current dot file
  6070. var index = 0; // current index in dot file
  6071. var c = ''; // current token character in expr
  6072. var token = ''; // current token
  6073. var tokenType = TOKENTYPE.NULL; // type of the token
  6074. var graph = null; // object with the graph to be build
  6075. var nodeAttr = null; // global node attributes
  6076. var edgeAttr = null; // global edge attributes
  6077. /**
  6078. * Get the first character from the dot file.
  6079. * The character is stored into the char c. If the end of the dot file is
  6080. * reached, the function puts an empty string in c.
  6081. */
  6082. function first() {
  6083. index = 0;
  6084. c = dot.charAt(0);
  6085. }
  6086. /**
  6087. * Get the next character from the dot file.
  6088. * The character is stored into the char c. If the end of the dot file is
  6089. * reached, the function puts an empty string in c.
  6090. */
  6091. function next() {
  6092. index++;
  6093. c = dot.charAt(index);
  6094. }
  6095. /**
  6096. * Preview the next character from the dot file.
  6097. * @return {String} cNext
  6098. */
  6099. function nextPreview() {
  6100. return dot.charAt(index + 1);
  6101. }
  6102. /**
  6103. * Test whether given character is alphabetic or numeric
  6104. * @param {String} c
  6105. * @return {Boolean} isAlphaNumeric
  6106. */
  6107. var regexAlphaNumeric = /[a-zA-Z_0-9.#]/;
  6108. function isAlphaNumeric(c) {
  6109. return regexAlphaNumeric.test(c);
  6110. }
  6111. /**
  6112. * Merge all properties of object b into object b
  6113. * @param {Object} a
  6114. * @param {Object} b
  6115. * @return {Object} a
  6116. */
  6117. function merge (a, b) {
  6118. if (!a) {
  6119. a = {};
  6120. }
  6121. if (b) {
  6122. for (var name in b) {
  6123. if (b.hasOwnProperty(name)) {
  6124. a[name] = b[name];
  6125. }
  6126. }
  6127. }
  6128. return a;
  6129. }
  6130. /**
  6131. * Add a node to the current graph object. If there is already a node with
  6132. * the same id, their attributes will be merged.
  6133. * @param {Object} node
  6134. */
  6135. function addNode(node) {
  6136. if (!graph.nodes) {
  6137. graph.nodes = {};
  6138. }
  6139. var current = graph.nodes[node.id];
  6140. if (current) {
  6141. // merge attributes
  6142. if (node.attr) {
  6143. current.attr = merge(current.attr, node.attr);
  6144. }
  6145. }
  6146. else {
  6147. // add
  6148. graph.nodes[node.id] = node;
  6149. if (nodeAttr) {
  6150. var attr = merge({}, nodeAttr); // clone global attributes
  6151. node.attr = merge(attr, node.attr); // merge attributes
  6152. }
  6153. }
  6154. }
  6155. /**
  6156. * Add an edge to the current graph obect
  6157. * @param {Object} edge
  6158. */
  6159. function addEdge(edge) {
  6160. if (!graph.edges) {
  6161. graph.edges = [];
  6162. }
  6163. graph.edges.push(edge);
  6164. if (edgeAttr) {
  6165. var attr = merge({}, edgeAttr); // clone global attributes
  6166. edge.attr = merge(attr, edge.attr); // merge attributes
  6167. }
  6168. }
  6169. /**
  6170. * Get next token in the current dot file.
  6171. * The token and token type are available as token and tokenType
  6172. */
  6173. function getToken() {
  6174. tokenType = TOKENTYPE.NULL;
  6175. token = '';
  6176. // skip over whitespaces
  6177. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6178. next();
  6179. }
  6180. do {
  6181. var isComment = false;
  6182. // skip comment
  6183. if (c == '#') {
  6184. // find the previous non-space character
  6185. var i = index - 1;
  6186. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  6187. i--;
  6188. }
  6189. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  6190. // the # is at the start of a line, this is indeed a line comment
  6191. while (c != '' && c != '\n') {
  6192. next();
  6193. }
  6194. isComment = true;
  6195. }
  6196. }
  6197. if (c == '/' && nextPreview() == '/') {
  6198. // skip line comment
  6199. while (c != '' && c != '\n') {
  6200. next();
  6201. }
  6202. isComment = true;
  6203. }
  6204. if (c == '/' && nextPreview() == '*') {
  6205. // skip block comment
  6206. while (c != '') {
  6207. if (c == '*' && nextPreview() == '/') {
  6208. // end of block comment found. skip these last two characters
  6209. next();
  6210. next();
  6211. break;
  6212. }
  6213. else {
  6214. next();
  6215. }
  6216. }
  6217. isComment = true;
  6218. }
  6219. // skip over whitespaces
  6220. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6221. next();
  6222. }
  6223. }
  6224. while (isComment);
  6225. // check for end of dot file
  6226. if (c == '') {
  6227. // token is still empty
  6228. tokenType = TOKENTYPE.DELIMITER;
  6229. return;
  6230. }
  6231. // check for delimiters consisting of 2 characters
  6232. var c2 = c + nextPreview();
  6233. if (DELIMITERS[c2]) {
  6234. tokenType = TOKENTYPE.DELIMITER;
  6235. token = c2;
  6236. next();
  6237. next();
  6238. return;
  6239. }
  6240. // check for delimiters consisting of 1 character
  6241. if (DELIMITERS[c]) {
  6242. tokenType = TOKENTYPE.DELIMITER;
  6243. token = c;
  6244. next();
  6245. return;
  6246. }
  6247. // check for an identifier (number or string)
  6248. // TODO: more precise parsing of numbers/strings
  6249. if (isAlphaNumeric(c) || c == '-') {
  6250. token += c;
  6251. next();
  6252. while (isAlphaNumeric(c)) {
  6253. token += c;
  6254. next();
  6255. }
  6256. if (token == 'false') {
  6257. token = false; // cast to boolean
  6258. }
  6259. else if (token == 'true') {
  6260. token = true; // cast to boolean
  6261. }
  6262. else if (!isNaN(Number(token))) {
  6263. token = Number(token); // cast to number
  6264. }
  6265. tokenType = TOKENTYPE.IDENTIFIER;
  6266. return;
  6267. }
  6268. // check for a string enclosed by double quotes
  6269. if (c == '"') {
  6270. next();
  6271. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  6272. token += c;
  6273. if (c == '"') { // skip the escape character
  6274. next();
  6275. }
  6276. next();
  6277. }
  6278. if (c != '"') {
  6279. throw newSyntaxError('End of string " expected');
  6280. }
  6281. next();
  6282. tokenType = TOKENTYPE.IDENTIFIER;
  6283. return;
  6284. }
  6285. // something unknown is found, wrong characters, a syntax error
  6286. tokenType = TOKENTYPE.UNKNOWN;
  6287. while (c != '') {
  6288. token += c;
  6289. next();
  6290. }
  6291. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  6292. }
  6293. /**
  6294. * Parse a graph.
  6295. * @returns {Object} graph
  6296. */
  6297. function parseGraph() {
  6298. graph = {};
  6299. nodeAttr = null;
  6300. edgeAttr = null;
  6301. first();
  6302. getToken();
  6303. // optional strict keyword
  6304. if (token == 'strict') {
  6305. graph.strict = true;
  6306. getToken();
  6307. }
  6308. // graph or digraph keyword
  6309. if (token == 'graph' || token == 'digraph') {
  6310. graph.type = token;
  6311. getToken();
  6312. }
  6313. // graph id
  6314. if (tokenType == TOKENTYPE.IDENTIFIER) {
  6315. graph.id = token;
  6316. getToken();
  6317. }
  6318. // open angle bracket
  6319. if (token != '{') {
  6320. throw newSyntaxError('Angle bracket { expected');
  6321. }
  6322. getToken();
  6323. // statements
  6324. parseStatements();
  6325. // close angle bracket
  6326. if (token != '}') {
  6327. throw newSyntaxError('Angle bracket } expected');
  6328. }
  6329. getToken();
  6330. // end of file
  6331. if (token !== '') {
  6332. throw newSyntaxError('End of file expected');
  6333. }
  6334. getToken();
  6335. return graph;
  6336. }
  6337. /**
  6338. * Parse a list with statements.
  6339. */
  6340. function parseStatements () {
  6341. while (token !== '' && token != '}') {
  6342. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6343. throw newSyntaxError('Identifier expected');
  6344. }
  6345. parseStatement();
  6346. if (token == ';') {
  6347. getToken();
  6348. }
  6349. }
  6350. }
  6351. /**
  6352. * Parse a single statement. Can be a an attribute statement, node
  6353. * statement, a series of node statements and edge statements, or a
  6354. * parameter.
  6355. */
  6356. function parseStatement() {
  6357. var attr;
  6358. var id = token; // can be as string or a number
  6359. getToken();
  6360. // attribute statements
  6361. if (id == 'node') {
  6362. // node attributes
  6363. attr = parseAttributes();
  6364. if (attr) {
  6365. nodeAttr = merge(nodeAttr, attr);
  6366. }
  6367. }
  6368. else if (id == 'edge') {
  6369. // edge attributes
  6370. attr = parseAttributes();
  6371. if (attr) {
  6372. edgeAttr = merge(edgeAttr, attr);
  6373. }
  6374. }
  6375. else if (id == 'graph') {
  6376. // graph attributes
  6377. attr = parseAttributes();
  6378. if (attr) {
  6379. graph.attr = merge(graph.attr, attr);
  6380. }
  6381. }
  6382. else {
  6383. if (token == '=') {
  6384. // id statement
  6385. getToken();
  6386. if (!graph.attr) {
  6387. graph.attr = {};
  6388. }
  6389. graph.attr[id] = token;
  6390. getToken();
  6391. }
  6392. else {
  6393. // node statement
  6394. var node = {
  6395. id: String(id)
  6396. };
  6397. attr = parseAttributes();
  6398. if (attr) {
  6399. node.attr = attr;
  6400. }
  6401. addNode(node);
  6402. // edge statements
  6403. var from = id;
  6404. while (token == '->' || token == '--') {
  6405. var type = token;
  6406. getToken();
  6407. var to = token;
  6408. addNode({
  6409. id: String(to)
  6410. });
  6411. getToken();
  6412. attr = parseAttributes();
  6413. // create edge
  6414. var edge = {
  6415. from: String(from),
  6416. to: String(to),
  6417. type: type
  6418. };
  6419. if (attr) {
  6420. edge.attr = attr;
  6421. }
  6422. addEdge(edge);
  6423. from = to;
  6424. }
  6425. }
  6426. }
  6427. }
  6428. /**
  6429. * Parse a set with attributes,
  6430. * for example [label="1.000", shape=solid]
  6431. * @return {Object | undefined} attr
  6432. */
  6433. function parseAttributes() {
  6434. if (token == '[') {
  6435. getToken();
  6436. var attr = {};
  6437. while (token !== '' && token != ']') {
  6438. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6439. throw newSyntaxError('Attribute name expected');
  6440. }
  6441. var name = token;
  6442. getToken();
  6443. if (token != '=') {
  6444. throw newSyntaxError('Equal sign = expected');
  6445. }
  6446. getToken();
  6447. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6448. throw newSyntaxError('Attribute value expected');
  6449. }
  6450. var value = token;
  6451. attr[name] = value;
  6452. getToken();
  6453. if (token ==',') {
  6454. getToken();
  6455. }
  6456. }
  6457. getToken();
  6458. return attr;
  6459. }
  6460. else {
  6461. return undefined;
  6462. }
  6463. }
  6464. /**
  6465. * Create a syntax error with extra information on current token and index.
  6466. * @param {String} message
  6467. * @returns {SyntaxError} err
  6468. */
  6469. function newSyntaxError(message) {
  6470. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  6471. }
  6472. /**
  6473. * Chop off text after a maximum length
  6474. * @param {String} text
  6475. * @param {Number} maxLength
  6476. * @returns {String}
  6477. */
  6478. function chop (text, maxLength) {
  6479. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  6480. }
  6481. /**
  6482. * Convert a string containing a graph in DOT language into a map containing
  6483. * with nodes and edges in the format of graph.
  6484. * @param {String} data Text containing a graph in DOT-notation
  6485. * @return {Object} graphData
  6486. */
  6487. function DOTToGraph (data) {
  6488. // parse the DOT file
  6489. var dotData = parseDOT(data);
  6490. var graphData = {
  6491. nodes: [],
  6492. edges: [],
  6493. options: {}
  6494. };
  6495. // copy the nodes
  6496. if (dotData.nodes) {
  6497. for (var id in dotData.nodes) {
  6498. if (dotData.nodes.hasOwnProperty(id)) {
  6499. var node = {
  6500. id: id,
  6501. label: id
  6502. };
  6503. merge(node, dotData.nodes[id].attr);
  6504. if (node.image) {
  6505. node.shape = 'image';
  6506. }
  6507. graphData.nodes.push(node);
  6508. }
  6509. }
  6510. }
  6511. // copy the edges
  6512. if (dotData.edges) {
  6513. dotData.edges.forEach(function (dotEdge) {
  6514. var graphEdge = {
  6515. from: dotEdge.from,
  6516. to: dotEdge.to
  6517. };
  6518. merge(graphEdge, dotEdge.attr);
  6519. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  6520. graphData.edges.push(graphEdge);
  6521. });
  6522. }
  6523. // copy the options
  6524. if (dotData.attr) {
  6525. graphData.options = dotData.attr;
  6526. }
  6527. return graphData;
  6528. }
  6529. // exports
  6530. exports.parseDOT = parseDOT;
  6531. exports.DOTToGraph = DOTToGraph;
  6532. })(typeof util !== 'undefined' ? util : exports);
  6533. /**
  6534. * Canvas shapes used by the Graph
  6535. */
  6536. if (typeof CanvasRenderingContext2D !== 'undefined') {
  6537. /**
  6538. * Draw a circle shape
  6539. */
  6540. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  6541. this.beginPath();
  6542. this.arc(x, y, r, 0, 2*Math.PI, false);
  6543. };
  6544. /**
  6545. * Draw a square shape
  6546. * @param {Number} x horizontal center
  6547. * @param {Number} y vertical center
  6548. * @param {Number} r size, width and height of the square
  6549. */
  6550. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  6551. this.beginPath();
  6552. this.rect(x - r, y - r, r * 2, r * 2);
  6553. };
  6554. /**
  6555. * Draw a triangle shape
  6556. * @param {Number} x horizontal center
  6557. * @param {Number} y vertical center
  6558. * @param {Number} r radius, half the length of the sides of the triangle
  6559. */
  6560. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  6561. // http://en.wikipedia.org/wiki/Equilateral_triangle
  6562. this.beginPath();
  6563. var s = r * 2;
  6564. var s2 = s / 2;
  6565. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  6566. var h = Math.sqrt(s * s - s2 * s2); // height
  6567. this.moveTo(x, y - (h - ir));
  6568. this.lineTo(x + s2, y + ir);
  6569. this.lineTo(x - s2, y + ir);
  6570. this.lineTo(x, y - (h - ir));
  6571. this.closePath();
  6572. };
  6573. /**
  6574. * Draw a triangle shape in downward orientation
  6575. * @param {Number} x horizontal center
  6576. * @param {Number} y vertical center
  6577. * @param {Number} r radius
  6578. */
  6579. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  6580. // http://en.wikipedia.org/wiki/Equilateral_triangle
  6581. this.beginPath();
  6582. var s = r * 2;
  6583. var s2 = s / 2;
  6584. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  6585. var h = Math.sqrt(s * s - s2 * s2); // height
  6586. this.moveTo(x, y + (h - ir));
  6587. this.lineTo(x + s2, y - ir);
  6588. this.lineTo(x - s2, y - ir);
  6589. this.lineTo(x, y + (h - ir));
  6590. this.closePath();
  6591. };
  6592. /**
  6593. * Draw a star shape, a star with 5 points
  6594. * @param {Number} x horizontal center
  6595. * @param {Number} y vertical center
  6596. * @param {Number} r radius, half the length of the sides of the triangle
  6597. */
  6598. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  6599. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  6600. this.beginPath();
  6601. for (var n = 0; n < 10; n++) {
  6602. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  6603. this.lineTo(
  6604. x + radius * Math.sin(n * 2 * Math.PI / 10),
  6605. y - radius * Math.cos(n * 2 * Math.PI / 10)
  6606. );
  6607. }
  6608. this.closePath();
  6609. };
  6610. /**
  6611. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  6612. */
  6613. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  6614. var r2d = Math.PI/180;
  6615. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  6616. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  6617. this.beginPath();
  6618. this.moveTo(x+r,y);
  6619. this.lineTo(x+w-r,y);
  6620. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  6621. this.lineTo(x+w,y+h-r);
  6622. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  6623. this.lineTo(x+r,y+h);
  6624. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  6625. this.lineTo(x,y+r);
  6626. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  6627. };
  6628. /**
  6629. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  6630. */
  6631. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  6632. var kappa = .5522848,
  6633. ox = (w / 2) * kappa, // control point offset horizontal
  6634. oy = (h / 2) * kappa, // control point offset vertical
  6635. xe = x + w, // x-end
  6636. ye = y + h, // y-end
  6637. xm = x + w / 2, // x-middle
  6638. ym = y + h / 2; // y-middle
  6639. this.beginPath();
  6640. this.moveTo(x, ym);
  6641. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  6642. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  6643. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  6644. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  6645. };
  6646. /**
  6647. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  6648. */
  6649. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  6650. var f = 1/3;
  6651. var wEllipse = w;
  6652. var hEllipse = h * f;
  6653. var kappa = .5522848,
  6654. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  6655. oy = (hEllipse / 2) * kappa, // control point offset vertical
  6656. xe = x + wEllipse, // x-end
  6657. ye = y + hEllipse, // y-end
  6658. xm = x + wEllipse / 2, // x-middle
  6659. ym = y + hEllipse / 2, // y-middle
  6660. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  6661. yeb = y + h; // y-end, bottom ellipse
  6662. this.beginPath();
  6663. this.moveTo(xe, ym);
  6664. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  6665. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  6666. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  6667. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  6668. this.lineTo(xe, ymb);
  6669. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  6670. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  6671. this.lineTo(x, ym);
  6672. };
  6673. /**
  6674. * Draw an arrow point (no line)
  6675. */
  6676. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  6677. // tail
  6678. var xt = x - length * Math.cos(angle);
  6679. var yt = y - length * Math.sin(angle);
  6680. // inner tail
  6681. // TODO: allow to customize different shapes
  6682. var xi = x - length * 0.9 * Math.cos(angle);
  6683. var yi = y - length * 0.9 * Math.sin(angle);
  6684. // left
  6685. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  6686. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  6687. // right
  6688. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  6689. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  6690. this.beginPath();
  6691. this.moveTo(x, y);
  6692. this.lineTo(xl, yl);
  6693. this.lineTo(xi, yi);
  6694. this.lineTo(xr, yr);
  6695. this.closePath();
  6696. };
  6697. /**
  6698. * Sets up the dashedLine functionality for drawing
  6699. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  6700. * @author David Jordan
  6701. * @date 2012-08-08
  6702. */
  6703. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  6704. if (!dashArray) dashArray=[10,5];
  6705. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  6706. var dashCount = dashArray.length;
  6707. this.moveTo(x, y);
  6708. var dx = (x2-x), dy = (y2-y);
  6709. var slope = dy/dx;
  6710. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  6711. var dashIndex=0, draw=true;
  6712. while (distRemaining>=0.1){
  6713. var dashLength = dashArray[dashIndex++%dashCount];
  6714. if (dashLength > distRemaining) dashLength = distRemaining;
  6715. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  6716. if (dx<0) xStep = -xStep;
  6717. x += xStep;
  6718. y += slope*xStep;
  6719. this[draw ? 'lineTo' : 'moveTo'](x,y);
  6720. distRemaining -= dashLength;
  6721. draw = !draw;
  6722. }
  6723. };
  6724. // TODO: add diamond shape
  6725. }
  6726. /**
  6727. * @class Node
  6728. * A node. A node can be connected to other nodes via one or multiple edges.
  6729. * @param {object} properties An object containing properties for the node. All
  6730. * properties are optional, except for the id.
  6731. * {number} id Id of the node. Required
  6732. * {string} label Text label for the node
  6733. * {number} x Horizontal position of the node
  6734. * {number} y Vertical position of the node
  6735. * {string} shape Node shape, available:
  6736. * "database", "circle", "ellipse",
  6737. * "box", "image", "text", "dot",
  6738. * "star", "triangle", "triangleDown",
  6739. * "square"
  6740. * {string} image An image url
  6741. * {string} title An title text, can be HTML
  6742. * {anytype} group A group name or number
  6743. * @param {Graph.Images} imagelist A list with images. Only needed
  6744. * when the node has an image
  6745. * @param {Graph.Groups} grouplist A list with groups. Needed for
  6746. * retrieving group properties
  6747. * @param {Object} constants An object with default values for
  6748. * example for the color
  6749. */
  6750. function Node(properties, imagelist, grouplist, constants) {
  6751. this.selected = false;
  6752. this.edges = []; // all edges connected to this node
  6753. this.group = constants.nodes.group;
  6754. this.fontSize = constants.nodes.fontSize;
  6755. this.fontFace = constants.nodes.fontFace;
  6756. this.fontColor = constants.nodes.fontColor;
  6757. this.color = constants.nodes.color;
  6758. // set defaults for the properties
  6759. this.id = undefined;
  6760. this.shape = constants.nodes.shape;
  6761. this.image = constants.nodes.image;
  6762. this.x = 0;
  6763. this.y = 0;
  6764. this.xFixed = false;
  6765. this.yFixed = false;
  6766. this.radius = constants.nodes.radius;
  6767. this.radiusFixed = false;
  6768. this.radiusMin = constants.nodes.radiusMin;
  6769. this.radiusMax = constants.nodes.radiusMax;
  6770. this.imagelist = imagelist;
  6771. this.grouplist = grouplist;
  6772. this.setProperties(properties, constants);
  6773. // mass, force, velocity
  6774. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  6775. this.fx = 0.0; // external force x
  6776. this.fy = 0.0; // external force y
  6777. this.vx = 0.0; // velocity x
  6778. this.vy = 0.0; // velocity y
  6779. this.minForce = constants.minForce;
  6780. this.damping = 0.9; // damping factor
  6781. };
  6782. /**
  6783. * Attach a edge to the node
  6784. * @param {Edge} edge
  6785. */
  6786. Node.prototype.attachEdge = function(edge) {
  6787. this.edges.push(edge);
  6788. this._updateMass();
  6789. };
  6790. /**
  6791. * Detach a edge from the node
  6792. * @param {Edge} edge
  6793. */
  6794. Node.prototype.detachEdge = function(edge) {
  6795. var index = this.edges.indexOf(edge);
  6796. if (index != -1) {
  6797. this.edges.splice(index, 1);
  6798. }
  6799. this._updateMass();
  6800. };
  6801. /**
  6802. * Update the nodes mass, which is determined by the number of edges connecting
  6803. * to it (more edges -> heavier node).
  6804. * @private
  6805. */
  6806. Node.prototype._updateMass = function() {
  6807. this.mass = 50 + 20 * this.edges.length; // kg
  6808. };
  6809. /**
  6810. * Set or overwrite properties for the node
  6811. * @param {Object} properties an object with properties
  6812. * @param {Object} constants and object with default, global properties
  6813. */
  6814. Node.prototype.setProperties = function(properties, constants) {
  6815. if (!properties) {
  6816. return;
  6817. }
  6818. // basic properties
  6819. if (properties.id != undefined) {this.id = properties.id;}
  6820. if (properties.label != undefined) {this.label = properties.label;}
  6821. if (properties.title != undefined) {this.title = properties.title;}
  6822. if (properties.group != undefined) {this.group = properties.group;}
  6823. if (properties.x != undefined) {this.x = properties.x;}
  6824. if (properties.y != undefined) {this.y = properties.y;}
  6825. if (properties.value != undefined) {this.value = properties.value;}
  6826. if (this.id === undefined) {
  6827. throw "Node must have an id";
  6828. }
  6829. // copy group properties
  6830. if (this.group) {
  6831. var groupObj = this.grouplist.get(this.group);
  6832. for (var prop in groupObj) {
  6833. if (groupObj.hasOwnProperty(prop)) {
  6834. this[prop] = groupObj[prop];
  6835. }
  6836. }
  6837. }
  6838. // individual shape properties
  6839. if (properties.shape != undefined) {this.shape = properties.shape;}
  6840. if (properties.image != undefined) {this.image = properties.image;}
  6841. if (properties.radius != undefined) {this.radius = properties.radius;}
  6842. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  6843. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  6844. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  6845. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  6846. if (this.image != undefined) {
  6847. if (this.imagelist) {
  6848. this.imageObj = this.imagelist.load(this.image);
  6849. }
  6850. else {
  6851. throw "No imagelist provided";
  6852. }
  6853. }
  6854. this.xFixed = this.xFixed || (properties.x != undefined);
  6855. this.yFixed = this.yFixed || (properties.y != undefined);
  6856. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  6857. if (this.shape == 'image') {
  6858. this.radiusMin = constants.nodes.widthMin;
  6859. this.radiusMax = constants.nodes.widthMax;
  6860. }
  6861. // choose draw method depending on the shape
  6862. switch (this.shape) {
  6863. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  6864. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  6865. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  6866. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  6867. // TODO: add diamond shape
  6868. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  6869. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  6870. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  6871. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  6872. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  6873. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  6874. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  6875. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  6876. }
  6877. // reset the size of the node, this can be changed
  6878. this._reset();
  6879. };
  6880. /**
  6881. * Parse a color property into an object with border, background, and
  6882. * hightlight colors
  6883. * @param {Object | String} color
  6884. * @return {Object} colorObject
  6885. */
  6886. Node.parseColor = function(color) {
  6887. var c;
  6888. if (util.isString(color)) {
  6889. c = {
  6890. border: color,
  6891. background: color,
  6892. highlight: {
  6893. border: color,
  6894. background: color
  6895. }
  6896. };
  6897. // TODO: automatically generate a nice highlight color
  6898. }
  6899. else {
  6900. c = {};
  6901. c.background = color.background || 'white';
  6902. c.border = color.border || c.background;
  6903. if (util.isString(color.highlight)) {
  6904. c.highlight = {
  6905. border: color.highlight,
  6906. background: color.highlight
  6907. }
  6908. }
  6909. else {
  6910. c.highlight = {};
  6911. c.highlight.background = color.highlight && color.highlight.background || c.background;
  6912. c.highlight.border = color.highlight && color.highlight.border || c.border;
  6913. }
  6914. }
  6915. return c;
  6916. };
  6917. /**
  6918. * select this node
  6919. */
  6920. Node.prototype.select = function() {
  6921. this.selected = true;
  6922. this._reset();
  6923. };
  6924. /**
  6925. * unselect this node
  6926. */
  6927. Node.prototype.unselect = function() {
  6928. this.selected = false;
  6929. this._reset();
  6930. };
  6931. /**
  6932. * Reset the calculated size of the node, forces it to recalculate its size
  6933. * @private
  6934. */
  6935. Node.prototype._reset = function() {
  6936. this.width = undefined;
  6937. this.height = undefined;
  6938. };
  6939. /**
  6940. * get the title of this node.
  6941. * @return {string} title The title of the node, or undefined when no title
  6942. * has been set.
  6943. */
  6944. Node.prototype.getTitle = function() {
  6945. return this.title;
  6946. };
  6947. /**
  6948. * Calculate the distance to the border of the Node
  6949. * @param {CanvasRenderingContext2D} ctx
  6950. * @param {Number} angle Angle in radians
  6951. * @returns {number} distance Distance to the border in pixels
  6952. */
  6953. Node.prototype.distanceToBorder = function (ctx, angle) {
  6954. var borderWidth = 1;
  6955. if (!this.width) {
  6956. this.resize(ctx);
  6957. }
  6958. //noinspection FallthroughInSwitchStatementJS
  6959. switch (this.shape) {
  6960. case 'circle':
  6961. case 'dot':
  6962. return this.radius + borderWidth;
  6963. case 'ellipse':
  6964. var a = this.width / 2;
  6965. var b = this.height / 2;
  6966. var w = (Math.sin(angle) * a);
  6967. var h = (Math.cos(angle) * b);
  6968. return a * b / Math.sqrt(w * w + h * h);
  6969. // TODO: implement distanceToBorder for database
  6970. // TODO: implement distanceToBorder for triangle
  6971. // TODO: implement distanceToBorder for triangleDown
  6972. case 'box':
  6973. case 'image':
  6974. case 'text':
  6975. default:
  6976. if (this.width) {
  6977. return Math.min(
  6978. Math.abs(this.width / 2 / Math.cos(angle)),
  6979. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  6980. // TODO: reckon with border radius too in case of box
  6981. }
  6982. else {
  6983. return 0;
  6984. }
  6985. }
  6986. // TODO: implement calculation of distance to border for all shapes
  6987. };
  6988. /**
  6989. * Set forces acting on the node
  6990. * @param {number} fx Force in horizontal direction
  6991. * @param {number} fy Force in vertical direction
  6992. */
  6993. Node.prototype._setForce = function(fx, fy) {
  6994. this.fx = fx;
  6995. this.fy = fy;
  6996. };
  6997. /**
  6998. * Add forces acting on the node
  6999. * @param {number} fx Force in horizontal direction
  7000. * @param {number} fy Force in vertical direction
  7001. * @private
  7002. */
  7003. Node.prototype._addForce = function(fx, fy) {
  7004. this.fx += fx;
  7005. this.fy += fy;
  7006. };
  7007. /**
  7008. * Perform one discrete step for the node
  7009. * @param {number} interval Time interval in seconds
  7010. */
  7011. Node.prototype.discreteStep = function(interval) {
  7012. if (!this.xFixed) {
  7013. var dx = -this.damping * this.vx; // damping force
  7014. var ax = (this.fx + dx) / this.mass; // acceleration
  7015. this.vx += ax / interval; // velocity
  7016. this.x += this.vx / interval; // position
  7017. }
  7018. if (!this.yFixed) {
  7019. var dy = -this.damping * this.vy; // damping force
  7020. var ay = (this.fy + dy) / this.mass; // acceleration
  7021. this.vy += ay / interval; // velocity
  7022. this.y += this.vy / interval; // position
  7023. }
  7024. };
  7025. /**
  7026. * Check if this node has a fixed x and y position
  7027. * @return {boolean} true if fixed, false if not
  7028. */
  7029. Node.prototype.isFixed = function() {
  7030. return (this.xFixed && this.yFixed);
  7031. };
  7032. /**
  7033. * Check if this node is moving
  7034. * @param {number} vmin the minimum velocity considered as "moving"
  7035. * @return {boolean} true if moving, false if it has no velocity
  7036. */
  7037. // TODO: replace this method with calculating the kinetic energy
  7038. Node.prototype.isMoving = function(vmin) {
  7039. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  7040. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  7041. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  7042. };
  7043. /**
  7044. * check if this node is selecte
  7045. * @return {boolean} selected True if node is selected, else false
  7046. */
  7047. Node.prototype.isSelected = function() {
  7048. return this.selected;
  7049. };
  7050. /**
  7051. * Retrieve the value of the node. Can be undefined
  7052. * @return {Number} value
  7053. */
  7054. Node.prototype.getValue = function() {
  7055. return this.value;
  7056. };
  7057. /**
  7058. * Calculate the distance from the nodes location to the given location (x,y)
  7059. * @param {Number} x
  7060. * @param {Number} y
  7061. * @return {Number} value
  7062. */
  7063. Node.prototype.getDistance = function(x, y) {
  7064. var dx = this.x - x,
  7065. dy = this.y - y;
  7066. return Math.sqrt(dx * dx + dy * dy);
  7067. };
  7068. /**
  7069. * Adjust the value range of the node. The node will adjust it's radius
  7070. * based on its value.
  7071. * @param {Number} min
  7072. * @param {Number} max
  7073. */
  7074. Node.prototype.setValueRange = function(min, max) {
  7075. if (!this.radiusFixed && this.value !== undefined) {
  7076. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  7077. this.radius = (this.value - min) * scale + this.radiusMin;
  7078. }
  7079. };
  7080. /**
  7081. * Draw this node in the given canvas
  7082. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7083. * @param {CanvasRenderingContext2D} ctx
  7084. */
  7085. Node.prototype.draw = function(ctx) {
  7086. throw "Draw method not initialized for node";
  7087. };
  7088. /**
  7089. * Recalculate the size of this node in the given canvas
  7090. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7091. * @param {CanvasRenderingContext2D} ctx
  7092. */
  7093. Node.prototype.resize = function(ctx) {
  7094. throw "Resize method not initialized for node";
  7095. };
  7096. /**
  7097. * Check if this object is overlapping with the provided object
  7098. * @param {Object} obj an object with parameters left, top, right, bottom
  7099. * @return {boolean} True if location is located on node
  7100. */
  7101. Node.prototype.isOverlappingWith = function(obj) {
  7102. return (this.left < obj.right &&
  7103. this.left + this.width > obj.left &&
  7104. this.top < obj.bottom &&
  7105. this.top + this.height > obj.top);
  7106. };
  7107. Node.prototype._resizeImage = function (ctx) {
  7108. // TODO: pre calculate the image size
  7109. if (!this.width) { // undefined or 0
  7110. var width, height;
  7111. if (this.value) {
  7112. var scale = this.imageObj.height / this.imageObj.width;
  7113. width = this.radius || this.imageObj.width;
  7114. height = this.radius * scale || this.imageObj.height;
  7115. }
  7116. else {
  7117. width = this.imageObj.width;
  7118. height = this.imageObj.height;
  7119. }
  7120. this.width = width;
  7121. this.height = height;
  7122. }
  7123. };
  7124. Node.prototype._drawImage = function (ctx) {
  7125. this._resizeImage(ctx);
  7126. this.left = this.x - this.width / 2;
  7127. this.top = this.y - this.height / 2;
  7128. var yLabel;
  7129. if (this.imageObj) {
  7130. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  7131. yLabel = this.y + this.height / 2;
  7132. }
  7133. else {
  7134. // image still loading... just draw the label for now
  7135. yLabel = this.y;
  7136. }
  7137. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  7138. };
  7139. Node.prototype._resizeBox = function (ctx) {
  7140. if (!this.width) {
  7141. var margin = 5;
  7142. var textSize = this.getTextSize(ctx);
  7143. this.width = textSize.width + 2 * margin;
  7144. this.height = textSize.height + 2 * margin;
  7145. }
  7146. };
  7147. Node.prototype._drawBox = function (ctx) {
  7148. this._resizeBox(ctx);
  7149. this.left = this.x - this.width / 2;
  7150. this.top = this.y - this.height / 2;
  7151. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7152. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7153. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7154. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  7155. ctx.fill();
  7156. ctx.stroke();
  7157. this._label(ctx, this.label, this.x, this.y);
  7158. };
  7159. Node.prototype._resizeDatabase = function (ctx) {
  7160. if (!this.width) {
  7161. var margin = 5;
  7162. var textSize = this.getTextSize(ctx);
  7163. var size = textSize.width + 2 * margin;
  7164. this.width = size;
  7165. this.height = size;
  7166. }
  7167. };
  7168. Node.prototype._drawDatabase = function (ctx) {
  7169. this._resizeDatabase(ctx);
  7170. this.left = this.x - this.width / 2;
  7171. this.top = this.y - this.height / 2;
  7172. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7173. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7174. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7175. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  7176. ctx.fill();
  7177. ctx.stroke();
  7178. this._label(ctx, this.label, this.x, this.y);
  7179. };
  7180. Node.prototype._resizeCircle = function (ctx) {
  7181. if (!this.width) {
  7182. var margin = 5;
  7183. var textSize = this.getTextSize(ctx);
  7184. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  7185. this.radius = diameter / 2;
  7186. this.width = diameter;
  7187. this.height = diameter;
  7188. }
  7189. };
  7190. Node.prototype._drawCircle = function (ctx) {
  7191. this._resizeCircle(ctx);
  7192. this.left = this.x - this.width / 2;
  7193. this.top = this.y - this.height / 2;
  7194. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7195. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7196. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7197. ctx.circle(this.x, this.y, this.radius);
  7198. ctx.fill();
  7199. ctx.stroke();
  7200. this._label(ctx, this.label, this.x, this.y);
  7201. };
  7202. Node.prototype._resizeEllipse = function (ctx) {
  7203. if (!this.width) {
  7204. var textSize = this.getTextSize(ctx);
  7205. this.width = textSize.width * 1.5;
  7206. this.height = textSize.height * 2;
  7207. if (this.width < this.height) {
  7208. this.width = this.height;
  7209. }
  7210. }
  7211. };
  7212. Node.prototype._drawEllipse = function (ctx) {
  7213. this._resizeEllipse(ctx);
  7214. this.left = this.x - this.width / 2;
  7215. this.top = this.y - this.height / 2;
  7216. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7217. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7218. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7219. ctx.ellipse(this.left, this.top, this.width, this.height);
  7220. ctx.fill();
  7221. ctx.stroke();
  7222. this._label(ctx, this.label, this.x, this.y);
  7223. };
  7224. Node.prototype._drawDot = function (ctx) {
  7225. this._drawShape(ctx, 'circle');
  7226. };
  7227. Node.prototype._drawTriangle = function (ctx) {
  7228. this._drawShape(ctx, 'triangle');
  7229. };
  7230. Node.prototype._drawTriangleDown = function (ctx) {
  7231. this._drawShape(ctx, 'triangleDown');
  7232. };
  7233. Node.prototype._drawSquare = function (ctx) {
  7234. this._drawShape(ctx, 'square');
  7235. };
  7236. Node.prototype._drawStar = function (ctx) {
  7237. this._drawShape(ctx, 'star');
  7238. };
  7239. Node.prototype._resizeShape = function (ctx) {
  7240. if (!this.width) {
  7241. var size = 2 * this.radius;
  7242. this.width = size;
  7243. this.height = size;
  7244. }
  7245. };
  7246. Node.prototype._drawShape = function (ctx, shape) {
  7247. this._resizeShape(ctx);
  7248. this.left = this.x - this.width / 2;
  7249. this.top = this.y - this.height / 2;
  7250. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7251. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7252. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7253. ctx[shape](this.x, this.y, this.radius);
  7254. ctx.fill();
  7255. ctx.stroke();
  7256. if (this.label) {
  7257. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  7258. }
  7259. };
  7260. Node.prototype._resizeText = function (ctx) {
  7261. if (!this.width) {
  7262. var margin = 5;
  7263. var textSize = this.getTextSize(ctx);
  7264. this.width = textSize.width + 2 * margin;
  7265. this.height = textSize.height + 2 * margin;
  7266. }
  7267. };
  7268. Node.prototype._drawText = function (ctx) {
  7269. this._resizeText(ctx);
  7270. this.left = this.x - this.width / 2;
  7271. this.top = this.y - this.height / 2;
  7272. this._label(ctx, this.label, this.x, this.y);
  7273. };
  7274. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  7275. if (text) {
  7276. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7277. ctx.fillStyle = this.fontColor || "black";
  7278. ctx.textAlign = align || "center";
  7279. ctx.textBaseline = baseline || "middle";
  7280. var lines = text.split('\n'),
  7281. lineCount = lines.length,
  7282. fontSize = (this.fontSize + 4),
  7283. yLine = y + (1 - lineCount) / 2 * fontSize;
  7284. for (var i = 0; i < lineCount; i++) {
  7285. ctx.fillText(lines[i], x, yLine);
  7286. yLine += fontSize;
  7287. }
  7288. }
  7289. };
  7290. Node.prototype.getTextSize = function(ctx) {
  7291. if (this.label != undefined) {
  7292. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7293. var lines = this.label.split('\n'),
  7294. height = (this.fontSize + 4) * lines.length,
  7295. width = 0;
  7296. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  7297. width = Math.max(width, ctx.measureText(lines[i]).width);
  7298. }
  7299. return {"width": width, "height": height};
  7300. }
  7301. else {
  7302. return {"width": 0, "height": 0};
  7303. }
  7304. };
  7305. /**
  7306. * @class Edge
  7307. *
  7308. * A edge connects two nodes
  7309. * @param {Object} properties Object with properties. Must contain
  7310. * At least properties from and to.
  7311. * Available properties: from (number),
  7312. * to (number), label (string, color (string),
  7313. * width (number), style (string),
  7314. * length (number), title (string)
  7315. * @param {Graph} graph A graph object, used to find and edge to
  7316. * nodes.
  7317. * @param {Object} constants An object with default values for
  7318. * example for the color
  7319. */
  7320. function Edge (properties, graph, constants) {
  7321. if (!graph) {
  7322. throw "No graph provided";
  7323. }
  7324. this.graph = graph;
  7325. // initialize constants
  7326. this.widthMin = constants.edges.widthMin;
  7327. this.widthMax = constants.edges.widthMax;
  7328. // initialize variables
  7329. this.id = undefined;
  7330. this.style = constants.edges.style;
  7331. this.title = undefined;
  7332. this.width = constants.edges.width;
  7333. this.value = undefined;
  7334. this.length = constants.edges.length;
  7335. // Added to support dashed lines
  7336. // David Jordan
  7337. // 2012-08-08
  7338. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gaph, altLength
  7339. this.stiffness = undefined; // depends on the length of the edge
  7340. this.color = constants.edges.color;
  7341. this.widthFixed = false;
  7342. this.lengthFixed = false;
  7343. this.setProperties(properties, constants);
  7344. };
  7345. /**
  7346. * Set or overwrite properties for the edge
  7347. * @param {Object} properties an object with properties
  7348. * @param {Object} constants and object with default, global properties
  7349. */
  7350. Edge.prototype.setProperties = function(properties, constants) {
  7351. if (!properties) {
  7352. return;
  7353. }
  7354. if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);}
  7355. if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);}
  7356. if (properties.id != undefined) {this.id = properties.id;}
  7357. if (properties.style != undefined) {this.style = properties.style;}
  7358. if (properties.label != undefined) {this.label = properties.label;}
  7359. if (this.label) {
  7360. this.fontSize = constants.edges.fontSize;
  7361. this.fontFace = constants.edges.fontFace;
  7362. this.fontColor = constants.edges.fontColor;
  7363. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7364. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7365. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7366. }
  7367. if (properties.title != undefined) {this.title = properties.title;}
  7368. if (properties.width != undefined) {this.width = properties.width;}
  7369. if (properties.value != undefined) {this.value = properties.value;}
  7370. if (properties.length != undefined) {this.length = properties.length;}
  7371. // Added to support dashed lines
  7372. // David Jordan
  7373. // 2012-08-08
  7374. if (properties.dash) {
  7375. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  7376. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  7377. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  7378. }
  7379. if (properties.color != undefined) {this.color = properties.color;}
  7380. if (!this.from) {
  7381. throw "Node with id " + properties.from + " not found";
  7382. }
  7383. if (!this.to) {
  7384. throw "Node with id " + properties.to + " not found";
  7385. }
  7386. this.widthFixed = this.widthFixed || (properties.width != undefined);
  7387. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  7388. this.stiffness = 1 / this.length;
  7389. // set draw method based on style
  7390. switch (this.style) {
  7391. case 'line': this.draw = this._drawLine; break;
  7392. case 'arrow': this.draw = this._drawArrow; break;
  7393. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  7394. case 'dash-line': this.draw = this._drawDashLine; break;
  7395. default: this.draw = this._drawLine; break;
  7396. }
  7397. };
  7398. /**
  7399. * get the title of this edge.
  7400. * @return {string} title The title of the edge, or undefined when no title
  7401. * has been set.
  7402. */
  7403. Edge.prototype.getTitle = function() {
  7404. return this.title;
  7405. };
  7406. /**
  7407. * Retrieve the value of the edge. Can be undefined
  7408. * @return {Number} value
  7409. */
  7410. Edge.prototype.getValue = function() {
  7411. return this.value;
  7412. };
  7413. /**
  7414. * Adjust the value range of the edge. The edge will adjust it's width
  7415. * based on its value.
  7416. * @param {Number} min
  7417. * @param {Number} max
  7418. */
  7419. Edge.prototype.setValueRange = function(min, max) {
  7420. if (!this.widthFixed && this.value !== undefined) {
  7421. var factor = (this.widthMax - this.widthMin) / (max - min);
  7422. this.width = (this.value - min) * factor + this.widthMin;
  7423. }
  7424. };
  7425. /**
  7426. * Redraw a edge
  7427. * Draw this edge in the given canvas
  7428. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7429. * @param {CanvasRenderingContext2D} ctx
  7430. */
  7431. Edge.prototype.draw = function(ctx) {
  7432. throw "Method draw not initialized in edge";
  7433. };
  7434. /**
  7435. * Check if this object is overlapping with the provided object
  7436. * @param {Object} obj an object with parameters left, top
  7437. * @return {boolean} True if location is located on the edge
  7438. */
  7439. Edge.prototype.isOverlappingWith = function(obj) {
  7440. var distMax = 10;
  7441. var xFrom = this.from.x;
  7442. var yFrom = this.from.y;
  7443. var xTo = this.to.x;
  7444. var yTo = this.to.y;
  7445. var xObj = obj.left;
  7446. var yObj = obj.top;
  7447. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  7448. return (dist < distMax);
  7449. };
  7450. /**
  7451. * Redraw a edge as a line
  7452. * Draw this edge in the given canvas
  7453. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7454. * @param {CanvasRenderingContext2D} ctx
  7455. * @private
  7456. */
  7457. Edge.prototype._drawLine = function(ctx) {
  7458. // set style
  7459. ctx.strokeStyle = this.color;
  7460. ctx.lineWidth = this._getLineWidth();
  7461. var point;
  7462. if (this.from != this.to) {
  7463. // draw line
  7464. this._line(ctx);
  7465. // draw label
  7466. if (this.label) {
  7467. point = this._pointOnLine(0.5);
  7468. this._label(ctx, this.label, point.x, point.y);
  7469. }
  7470. }
  7471. else {
  7472. var x, y;
  7473. var radius = this.length / 4;
  7474. var node = this.from;
  7475. if (!node.width) {
  7476. node.resize(ctx);
  7477. }
  7478. if (node.width > node.height) {
  7479. x = node.x + node.width / 2;
  7480. y = node.y - radius;
  7481. }
  7482. else {
  7483. x = node.x + radius;
  7484. y = node.y - node.height / 2;
  7485. }
  7486. this._circle(ctx, x, y, radius);
  7487. point = this._pointOnCircle(x, y, radius, 0.5);
  7488. this._label(ctx, this.label, point.x, point.y);
  7489. }
  7490. };
  7491. /**
  7492. * Get the line width of the edge. Depends on width and whether one of the
  7493. * connected nodes is selected.
  7494. * @return {Number} width
  7495. * @private
  7496. */
  7497. Edge.prototype._getLineWidth = function() {
  7498. if (this.from.selected || this.to.selected) {
  7499. return Math.min(this.width * 2, this.widthMax);
  7500. }
  7501. else {
  7502. return this.width;
  7503. }
  7504. };
  7505. /**
  7506. * Draw a line between two nodes
  7507. * @param {CanvasRenderingContext2D} ctx
  7508. * @private
  7509. */
  7510. Edge.prototype._line = function (ctx) {
  7511. // draw a straight line
  7512. ctx.beginPath();
  7513. ctx.moveTo(this.from.x, this.from.y);
  7514. ctx.lineTo(this.to.x, this.to.y);
  7515. ctx.stroke();
  7516. };
  7517. /**
  7518. * Draw a line from a node to itself, a circle
  7519. * @param {CanvasRenderingContext2D} ctx
  7520. * @param {Number} x
  7521. * @param {Number} y
  7522. * @param {Number} radius
  7523. * @private
  7524. */
  7525. Edge.prototype._circle = function (ctx, x, y, radius) {
  7526. // draw a circle
  7527. ctx.beginPath();
  7528. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  7529. ctx.stroke();
  7530. };
  7531. /**
  7532. * Draw label with white background and with the middle at (x, y)
  7533. * @param {CanvasRenderingContext2D} ctx
  7534. * @param {String} text
  7535. * @param {Number} x
  7536. * @param {Number} y
  7537. * @private
  7538. */
  7539. Edge.prototype._label = function (ctx, text, x, y) {
  7540. if (text) {
  7541. // TODO: cache the calculated size
  7542. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  7543. this.fontSize + "px " + this.fontFace;
  7544. ctx.fillStyle = 'white';
  7545. var width = ctx.measureText(text).width;
  7546. var height = this.fontSize;
  7547. var left = x - width / 2;
  7548. var top = y - height / 2;
  7549. ctx.fillRect(left, top, width, height);
  7550. // draw text
  7551. ctx.fillStyle = this.fontColor || "black";
  7552. ctx.textAlign = "left";
  7553. ctx.textBaseline = "top";
  7554. ctx.fillText(text, left, top);
  7555. }
  7556. };
  7557. /**
  7558. * Redraw a edge as a dashed line
  7559. * Draw this edge in the given canvas
  7560. * @author David Jordan
  7561. * @date 2012-08-08
  7562. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7563. * @param {CanvasRenderingContext2D} ctx
  7564. * @private
  7565. */
  7566. Edge.prototype._drawDashLine = function(ctx) {
  7567. // set style
  7568. ctx.strokeStyle = this.color;
  7569. ctx.lineWidth = this._getLineWidth();
  7570. // draw dashed line
  7571. ctx.beginPath();
  7572. ctx.lineCap = 'round';
  7573. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  7574. {
  7575. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  7576. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  7577. }
  7578. else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
  7579. {
  7580. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  7581. [this.dash.length,this.dash.gap]);
  7582. }
  7583. else //If all else fails draw a line
  7584. {
  7585. ctx.moveTo(this.from.x, this.from.y);
  7586. ctx.lineTo(this.to.x, this.to.y);
  7587. }
  7588. ctx.stroke();
  7589. // draw label
  7590. if (this.label) {
  7591. var point = this._pointOnLine(0.5);
  7592. this._label(ctx, this.label, point.x, point.y);
  7593. }
  7594. };
  7595. /**
  7596. * Get a point on a line
  7597. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  7598. * @return {Object} point
  7599. * @private
  7600. */
  7601. Edge.prototype._pointOnLine = function (percentage) {
  7602. return {
  7603. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  7604. y: (1 - percentage) * this.from.y + percentage * this.to.y
  7605. }
  7606. };
  7607. /**
  7608. * Get a point on a circle
  7609. * @param {Number} x
  7610. * @param {Number} y
  7611. * @param {Number} radius
  7612. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  7613. * @return {Object} point
  7614. * @private
  7615. */
  7616. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  7617. var angle = (percentage - 3/8) * 2 * Math.PI;
  7618. return {
  7619. x: x + radius * Math.cos(angle),
  7620. y: y - radius * Math.sin(angle)
  7621. }
  7622. };
  7623. /**
  7624. * Redraw a edge as a line with an arrow halfway the line
  7625. * Draw this edge in the given canvas
  7626. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7627. * @param {CanvasRenderingContext2D} ctx
  7628. * @private
  7629. */
  7630. Edge.prototype._drawArrowCenter = function(ctx) {
  7631. var point;
  7632. // set style
  7633. ctx.strokeStyle = this.color;
  7634. ctx.fillStyle = this.color;
  7635. ctx.lineWidth = this._getLineWidth();
  7636. if (this.from != this.to) {
  7637. // draw line
  7638. this._line(ctx);
  7639. // draw an arrow halfway the line
  7640. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  7641. var length = 10 + 5 * this.width; // TODO: make customizable?
  7642. point = this._pointOnLine(0.5);
  7643. ctx.arrow(point.x, point.y, angle, length);
  7644. ctx.fill();
  7645. ctx.stroke();
  7646. // draw label
  7647. if (this.label) {
  7648. point = this._pointOnLine(0.5);
  7649. this._label(ctx, this.label, point.x, point.y);
  7650. }
  7651. }
  7652. else {
  7653. // draw circle
  7654. var x, y;
  7655. var radius = this.length / 4;
  7656. var node = this.from;
  7657. if (!node.width) {
  7658. node.resize(ctx);
  7659. }
  7660. if (node.width > node.height) {
  7661. x = node.x + node.width / 2;
  7662. y = node.y - radius;
  7663. }
  7664. else {
  7665. x = node.x + radius;
  7666. y = node.y - node.height / 2;
  7667. }
  7668. this._circle(ctx, x, y, radius);
  7669. // draw all arrows
  7670. var angle = 0.2 * Math.PI;
  7671. var length = 10 + 5 * this.width; // TODO: make customizable?
  7672. point = this._pointOnCircle(x, y, radius, 0.5);
  7673. ctx.arrow(point.x, point.y, angle, length);
  7674. ctx.fill();
  7675. ctx.stroke();
  7676. // draw label
  7677. if (this.label) {
  7678. point = this._pointOnCircle(x, y, radius, 0.5);
  7679. this._label(ctx, this.label, point.x, point.y);
  7680. }
  7681. }
  7682. };
  7683. /**
  7684. * Redraw a edge as a line with an arrow
  7685. * Draw this edge in the given canvas
  7686. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7687. * @param {CanvasRenderingContext2D} ctx
  7688. * @private
  7689. */
  7690. Edge.prototype._drawArrow = function(ctx) {
  7691. // set style
  7692. ctx.strokeStyle = this.color;
  7693. ctx.fillStyle = this.color;
  7694. ctx.lineWidth = this._getLineWidth();
  7695. // draw line
  7696. var angle, length;
  7697. if (this.from != this.to) {
  7698. // calculate length and angle of the line
  7699. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  7700. var dx = (this.to.x - this.from.x);
  7701. var dy = (this.to.y - this.from.y);
  7702. var lEdge = Math.sqrt(dx * dx + dy * dy);
  7703. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  7704. var pFrom = (lEdge - lFrom) / lEdge;
  7705. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  7706. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  7707. var lTo = this.to.distanceToBorder(ctx, angle);
  7708. var pTo = (lEdge - lTo) / lEdge;
  7709. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  7710. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  7711. ctx.beginPath();
  7712. ctx.moveTo(xFrom, yFrom);
  7713. ctx.lineTo(xTo, yTo);
  7714. ctx.stroke();
  7715. // draw arrow at the end of the line
  7716. length = 10 + 5 * this.width; // TODO: make customizable?
  7717. ctx.arrow(xTo, yTo, angle, length);
  7718. ctx.fill();
  7719. ctx.stroke();
  7720. // draw label
  7721. if (this.label) {
  7722. var point = this._pointOnLine(0.5);
  7723. this._label(ctx, this.label, point.x, point.y);
  7724. }
  7725. }
  7726. else {
  7727. // draw circle
  7728. var node = this.from;
  7729. var x, y, arrow;
  7730. var radius = this.length / 4;
  7731. if (!node.width) {
  7732. node.resize(ctx);
  7733. }
  7734. if (node.width > node.height) {
  7735. x = node.x + node.width / 2;
  7736. y = node.y - radius;
  7737. arrow = {
  7738. x: x,
  7739. y: node.y,
  7740. angle: 0.9 * Math.PI
  7741. };
  7742. }
  7743. else {
  7744. x = node.x + radius;
  7745. y = node.y - node.height / 2;
  7746. arrow = {
  7747. x: node.x,
  7748. y: y,
  7749. angle: 0.6 * Math.PI
  7750. };
  7751. }
  7752. ctx.beginPath();
  7753. // TODO: do not draw a circle, but an arc
  7754. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  7755. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  7756. ctx.stroke();
  7757. // draw all arrows
  7758. length = 10 + 5 * this.width; // TODO: make customizable?
  7759. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  7760. ctx.fill();
  7761. ctx.stroke();
  7762. // draw label
  7763. if (this.label) {
  7764. point = this._pointOnCircle(x, y, radius, 0.5);
  7765. this._label(ctx, this.label, point.x, point.y);
  7766. }
  7767. }
  7768. };
  7769. /**
  7770. * Calculate the distance between a point (x3,y3) and a line segment from
  7771. * (x1,y1) to (x2,y2).
  7772. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  7773. * @param {number} x1
  7774. * @param {number} y1
  7775. * @param {number} x2
  7776. * @param {number} y2
  7777. * @param {number} x3
  7778. * @param {number} y3
  7779. * @private
  7780. */
  7781. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  7782. var px = x2-x1,
  7783. py = y2-y1,
  7784. something = px*px + py*py,
  7785. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  7786. if (u > 1) {
  7787. u = 1;
  7788. }
  7789. else if (u < 0) {
  7790. u = 0;
  7791. }
  7792. var x = x1 + u * px,
  7793. y = y1 + u * py,
  7794. dx = x - x3,
  7795. dy = y - y3;
  7796. //# Note: If the actual distance does not matter,
  7797. //# if you only want to compare what this function
  7798. //# returns to other results of this function, you
  7799. //# can just return the squared distance instead
  7800. //# (i.e. remove the sqrt) to gain a little performance
  7801. return Math.sqrt(dx*dx + dy*dy);
  7802. };
  7803. /**
  7804. * Popup is a class to create a popup window with some text
  7805. * @param {Element} container The container object.
  7806. * @param {Number} [x]
  7807. * @param {Number} [y]
  7808. * @param {String} [text]
  7809. */
  7810. function Popup(container, x, y, text) {
  7811. if (container) {
  7812. this.container = container;
  7813. }
  7814. else {
  7815. this.container = document.body;
  7816. }
  7817. this.x = 0;
  7818. this.y = 0;
  7819. this.padding = 5;
  7820. if (x !== undefined && y !== undefined ) {
  7821. this.setPosition(x, y);
  7822. }
  7823. if (text !== undefined) {
  7824. this.setText(text);
  7825. }
  7826. // create the frame
  7827. this.frame = document.createElement("div");
  7828. var style = this.frame.style;
  7829. style.position = "absolute";
  7830. style.visibility = "hidden";
  7831. style.border = "1px solid #666";
  7832. style.color = "black";
  7833. style.padding = this.padding + "px";
  7834. style.backgroundColor = "#FFFFC6";
  7835. style.borderRadius = "3px";
  7836. style.MozBorderRadius = "3px";
  7837. style.WebkitBorderRadius = "3px";
  7838. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  7839. style.whiteSpace = "nowrap";
  7840. this.container.appendChild(this.frame);
  7841. };
  7842. /**
  7843. * @param {number} x Horizontal position of the popup window
  7844. * @param {number} y Vertical position of the popup window
  7845. */
  7846. Popup.prototype.setPosition = function(x, y) {
  7847. this.x = parseInt(x);
  7848. this.y = parseInt(y);
  7849. };
  7850. /**
  7851. * Set the text for the popup window. This can be HTML code
  7852. * @param {string} text
  7853. */
  7854. Popup.prototype.setText = function(text) {
  7855. this.frame.innerHTML = text;
  7856. };
  7857. /**
  7858. * Show the popup window
  7859. * @param {boolean} show Optional. Show or hide the window
  7860. */
  7861. Popup.prototype.show = function (show) {
  7862. if (show === undefined) {
  7863. show = true;
  7864. }
  7865. if (show) {
  7866. var height = this.frame.clientHeight;
  7867. var width = this.frame.clientWidth;
  7868. var maxHeight = this.frame.parentNode.clientHeight;
  7869. var maxWidth = this.frame.parentNode.clientWidth;
  7870. var top = (this.y - height);
  7871. if (top + height + this.padding > maxHeight) {
  7872. top = maxHeight - height - this.padding;
  7873. }
  7874. if (top < this.padding) {
  7875. top = this.padding;
  7876. }
  7877. var left = this.x;
  7878. if (left + width + this.padding > maxWidth) {
  7879. left = maxWidth - width - this.padding;
  7880. }
  7881. if (left < this.padding) {
  7882. left = this.padding;
  7883. }
  7884. this.frame.style.left = left + "px";
  7885. this.frame.style.top = top + "px";
  7886. this.frame.style.visibility = "visible";
  7887. }
  7888. else {
  7889. this.hide();
  7890. }
  7891. };
  7892. /**
  7893. * Hide the popup window
  7894. */
  7895. Popup.prototype.hide = function () {
  7896. this.frame.style.visibility = "hidden";
  7897. };
  7898. /**
  7899. * @class Groups
  7900. * This class can store groups and properties specific for groups.
  7901. */
  7902. Groups = function () {
  7903. this.clear();
  7904. this.defaultIndex = 0;
  7905. };
  7906. /**
  7907. * default constants for group colors
  7908. */
  7909. Groups.DEFAULT = [
  7910. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  7911. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  7912. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  7913. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  7914. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  7915. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  7916. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  7917. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  7918. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  7919. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  7920. ];
  7921. /**
  7922. * Clear all groups
  7923. */
  7924. Groups.prototype.clear = function () {
  7925. this.groups = {};
  7926. this.groups.length = function()
  7927. {
  7928. var i = 0;
  7929. for ( var p in this ) {
  7930. if (this.hasOwnProperty(p)) {
  7931. i++;
  7932. }
  7933. }
  7934. return i;
  7935. }
  7936. };
  7937. /**
  7938. * get group properties of a groupname. If groupname is not found, a new group
  7939. * is added.
  7940. * @param {*} groupname Can be a number, string, Date, etc.
  7941. * @return {Object} group The created group, containing all group properties
  7942. */
  7943. Groups.prototype.get = function (groupname) {
  7944. var group = this.groups[groupname];
  7945. if (group == undefined) {
  7946. // create new group
  7947. var index = this.defaultIndex % Groups.DEFAULT.length;
  7948. this.defaultIndex++;
  7949. group = {};
  7950. group.color = Groups.DEFAULT[index];
  7951. this.groups[groupname] = group;
  7952. }
  7953. return group;
  7954. };
  7955. /**
  7956. * Add a custom group style
  7957. * @param {String} groupname
  7958. * @param {Object} style An object containing borderColor,
  7959. * backgroundColor, etc.
  7960. * @return {Object} group The created group object
  7961. */
  7962. Groups.prototype.add = function (groupname, style) {
  7963. this.groups[groupname] = style;
  7964. if (style.color) {
  7965. style.color = Node.parseColor(style.color);
  7966. }
  7967. return style;
  7968. };
  7969. /**
  7970. * @class Images
  7971. * This class loads images and keeps them stored.
  7972. */
  7973. Images = function () {
  7974. this.images = {};
  7975. this.callback = undefined;
  7976. };
  7977. /**
  7978. * Set an onload callback function. This will be called each time an image
  7979. * is loaded
  7980. * @param {function} callback
  7981. */
  7982. Images.prototype.setOnloadCallback = function(callback) {
  7983. this.callback = callback;
  7984. };
  7985. /**
  7986. *
  7987. * @param {string} url Url of the image
  7988. * @return {Image} img The image object
  7989. */
  7990. Images.prototype.load = function(url) {
  7991. var img = this.images[url];
  7992. if (img == undefined) {
  7993. // create the image
  7994. var images = this;
  7995. img = new Image();
  7996. this.images[url] = img;
  7997. img.onload = function() {
  7998. if (images.callback) {
  7999. images.callback(this);
  8000. }
  8001. };
  8002. img.src = url;
  8003. }
  8004. return img;
  8005. };
  8006. /**
  8007. * @constructor Graph
  8008. * Create a graph visualization, displaying nodes and edges.
  8009. *
  8010. * @param {Element} container The DOM element in which the Graph will
  8011. * be created. Normally a div element.
  8012. * @param {Object} data An object containing parameters
  8013. * {Array} nodes
  8014. * {Array} edges
  8015. * @param {Object} options Options
  8016. */
  8017. function Graph (container, data, options) {
  8018. // create variables and set default values
  8019. this.containerElement = container;
  8020. this.width = '100%';
  8021. this.height = '100%';
  8022. this.refreshRate = 50; // milliseconds
  8023. this.stabilize = true; // stabilize before displaying the graph
  8024. this.selectable = true;
  8025. // set constant values
  8026. this.constants = {
  8027. nodes: {
  8028. radiusMin: 5,
  8029. radiusMax: 20,
  8030. radius: 5,
  8031. distance: 100, // px
  8032. shape: 'ellipse',
  8033. image: undefined,
  8034. widthMin: 16, // px
  8035. widthMax: 64, // px
  8036. fontColor: 'black',
  8037. fontSize: 14, // px
  8038. //fontFace: verdana,
  8039. fontFace: 'arial',
  8040. color: {
  8041. border: '#2B7CE9',
  8042. background: '#97C2FC',
  8043. highlight: {
  8044. border: '#2B7CE9',
  8045. background: '#D2E5FF'
  8046. }
  8047. },
  8048. borderColor: '#2B7CE9',
  8049. backgroundColor: '#97C2FC',
  8050. highlightColor: '#D2E5FF',
  8051. group: undefined
  8052. },
  8053. edges: {
  8054. widthMin: 1,
  8055. widthMax: 15,
  8056. width: 1,
  8057. style: 'line',
  8058. color: '#343434',
  8059. fontColor: '#343434',
  8060. fontSize: 14, // px
  8061. fontFace: 'arial',
  8062. //distance: 100, //px
  8063. length: 100, // px
  8064. dash: {
  8065. length: 10,
  8066. gap: 5,
  8067. altLength: undefined
  8068. }
  8069. },
  8070. minForce: 0.05,
  8071. minVelocity: 0.02, // px/s
  8072. maxIterations: 1000 // maximum number of iteration to stabilize
  8073. };
  8074. var graph = this;
  8075. this.nodes = []; // array with Node objects
  8076. this.edges = []; // array with Edge objects
  8077. this.groups = new Groups(); // object with groups
  8078. this.images = new Images(); // object with images
  8079. this.images.setOnloadCallback(function () {
  8080. graph._redraw();
  8081. });
  8082. // properties of the data
  8083. this.moving = false; // True if any of the nodes have an undefined position
  8084. this.selection = [];
  8085. this.timer = undefined;
  8086. // create a frame and canvas
  8087. this._create();
  8088. // apply options
  8089. this.setOptions(options);
  8090. // draw data
  8091. this.setData(data);
  8092. }
  8093. /**
  8094. * Set nodes and edges, and optionally options as well.
  8095. *
  8096. * @param {Object} data Object containing parameters:
  8097. * {Array} [nodes] Array with nodes.
  8098. * Required when format is 'vis'
  8099. * {Array} [edges] Array with edges
  8100. * Required when format is 'vis'
  8101. * {String} [dot] String containing data in DOT
  8102. * format.
  8103. * {Options} [options] Object with options
  8104. */
  8105. Graph.prototype.setData = function(data) {
  8106. if (data && data.dot && (data.nodes || data.edges)) {
  8107. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  8108. ' parameter pair "nodes" and "edges", but not both.');
  8109. }
  8110. // set options
  8111. this.setOptions(data && data.options);
  8112. // set all data
  8113. if (data && data.dot) {
  8114. // parse DOT file
  8115. if(data && data.dot) {
  8116. var dotData = vis.util.DOTToGraph(data.dot);
  8117. this.setData(dotData);
  8118. return;
  8119. }
  8120. }
  8121. else {
  8122. this._setNodes(data && data.nodes);
  8123. this._setEdges(data && data.edges);
  8124. }
  8125. // find a stable position or start animating to a stable position
  8126. if (this.stabilize) {
  8127. this._doStabilize();
  8128. }
  8129. this.start();
  8130. };
  8131. /**
  8132. * Set options
  8133. * @param {Object} options
  8134. */
  8135. Graph.prototype.setOptions = function (options) {
  8136. if (options) {
  8137. // retrieve parameter values
  8138. if (options.width != undefined) {this.width = options.width;}
  8139. if (options.height != undefined) {this.height = options.height;}
  8140. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  8141. if (options.selectable != undefined) {this.selectable = options.selectable;}
  8142. // TODO: work out these options and document them
  8143. if (options.edges) {
  8144. for (var prop in options.edges) {
  8145. if (options.edges.hasOwnProperty(prop)) {
  8146. this.constants.edges[prop] = options.edges[prop];
  8147. }
  8148. }
  8149. if (options.edges.length != undefined &&
  8150. options.nodes && options.nodes.distance == undefined) {
  8151. this.constants.edges.length = options.edges.length;
  8152. this.constants.nodes.distance = options.edges.length * 1.25;
  8153. }
  8154. if (!options.edges.fontColor) {
  8155. this.constants.edges.fontColor = options.edges.color;
  8156. }
  8157. // Added to support dashed lines
  8158. // David Jordan
  8159. // 2012-08-08
  8160. if (options.edges.dash) {
  8161. if (options.edges.dash.length != undefined) {
  8162. this.constants.edges.dash.length = options.edges.dash.length;
  8163. }
  8164. if (options.edges.dash.gap != undefined) {
  8165. this.constants.edges.dash.gap = options.edges.dash.gap;
  8166. }
  8167. if (options.edges.dash.altLength != undefined) {
  8168. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  8169. }
  8170. }
  8171. }
  8172. if (options.nodes) {
  8173. for (prop in options.nodes) {
  8174. if (options.nodes.hasOwnProperty(prop)) {
  8175. this.constants.nodes[prop] = options.nodes[prop];
  8176. }
  8177. }
  8178. if (options.nodes.color) {
  8179. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  8180. }
  8181. /*
  8182. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  8183. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  8184. */
  8185. }
  8186. if (options.groups) {
  8187. for (var groupname in options.groups) {
  8188. if (options.groups.hasOwnProperty(groupname)) {
  8189. var group = options.groups[groupname];
  8190. this.groups.add(groupname, group);
  8191. }
  8192. }
  8193. }
  8194. }
  8195. this.setSize(this.width, this.height);
  8196. this._setTranslation(0, 0);
  8197. this._setScale(1);
  8198. };
  8199. /**
  8200. * fire an event
  8201. * @param {String} event The name of an event, for example "select"
  8202. * @param {Object} params Optional object with event parameters
  8203. * @private
  8204. */
  8205. Graph.prototype._trigger = function (event, params) {
  8206. events.trigger(this, event, params);
  8207. };
  8208. /**
  8209. * Create the main frame for the Graph.
  8210. * This function is executed once when a Graph object is created. The frame
  8211. * contains a canvas, and this canvas contains all objects like the axis and
  8212. * nodes.
  8213. * @private
  8214. */
  8215. Graph.prototype._create = function () {
  8216. // remove all elements from the container element.
  8217. while (this.containerElement.hasChildNodes()) {
  8218. this.containerElement.removeChild(this.containerElement.firstChild);
  8219. }
  8220. this.frame = document.createElement("div");
  8221. this.frame.className = "graph-frame";
  8222. this.frame.style.position = "relative";
  8223. this.frame.style.overflow = "hidden";
  8224. // create the graph canvas (HTML canvas element)
  8225. this.frame.canvas = document.createElement( "canvas" );
  8226. this.frame.canvas.style.position = "relative";
  8227. this.frame.appendChild(this.frame.canvas);
  8228. if (!this.frame.canvas.getContext) {
  8229. var noCanvas = document.createElement( "DIV" );
  8230. noCanvas.style.color = "red";
  8231. noCanvas.style.fontWeight = "bold" ;
  8232. noCanvas.style.padding = "10px";
  8233. noCanvas.innerHTML = "Error: your browser does not support HTML canvas";
  8234. this.frame.canvas.appendChild(noCanvas);
  8235. }
  8236. // create event listeners
  8237. var me = this;
  8238. var onmousedown = function (event) {me._onMouseDown(event);};
  8239. var onmousemove = function (event) {me._onMouseMoveTitle(event);};
  8240. var onmousewheel = function (event) {me._onMouseWheel(event);};
  8241. var ontouchstart = function (event) {me._onTouchStart(event);};
  8242. vis.util.addEventListener(this.frame.canvas, "mousedown", onmousedown);
  8243. vis.util.addEventListener(this.frame.canvas, "mousemove", onmousemove);
  8244. vis.util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
  8245. vis.util.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
  8246. // add the frame to the container element
  8247. this.containerElement.appendChild(this.frame);
  8248. };
  8249. /**
  8250. * handle on mouse down event
  8251. * @private
  8252. */
  8253. Graph.prototype._onMouseDown = function (event) {
  8254. event = event || window.event;
  8255. if (!this.selectable) {
  8256. return;
  8257. }
  8258. // check if mouse is still down (may be up when focus is lost for example
  8259. // in an iframe)
  8260. if (this.leftButtonDown) {
  8261. this._onMouseUp(event);
  8262. }
  8263. // only react on left mouse button down
  8264. this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  8265. if (!this.leftButtonDown && !this.touchDown) {
  8266. return;
  8267. }
  8268. // add event listeners to handle moving the contents
  8269. // we store the function onmousemove and onmouseup in the timeline, so we can
  8270. // remove the eventlisteners lateron in the function mouseUp()
  8271. var me = this;
  8272. if (!this.onmousemove) {
  8273. this.onmousemove = function (event) {me._onMouseMove(event);};
  8274. vis.util.addEventListener(document, "mousemove", me.onmousemove);
  8275. }
  8276. if (!this.onmouseup) {
  8277. this.onmouseup = function (event) {me._onMouseUp(event);};
  8278. vis.util.addEventListener(document, "mouseup", me.onmouseup);
  8279. }
  8280. vis.util.preventDefault(event);
  8281. // store the start x and y position of the mouse
  8282. this.startMouseX = event.clientX || event.targetTouches[0].clientX;
  8283. this.startMouseY = event.clientY || event.targetTouches[0].clientY;
  8284. this.startFrameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  8285. this.startFrameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  8286. this.startTranslation = this._getTranslation();
  8287. this.ctrlKeyDown = event.ctrlKey;
  8288. this.shiftKeyDown = event.shiftKey;
  8289. var obj = {
  8290. "left" : this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8291. "top" : this._yToCanvas(this.startMouseY - this.startFrameTop),
  8292. "right" : this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8293. "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop)
  8294. };
  8295. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8296. // if there are overlapping nodes, select the last one, this is the
  8297. // one which is drawn on top of the others
  8298. this.startClickedObj = (overlappingNodes.length > 0) ?
  8299. overlappingNodes[overlappingNodes.length - 1] : undefined;
  8300. if (this.startClickedObj) {
  8301. // move clicked node with the mouse
  8302. // make the clicked node temporarily fixed, and store their original state
  8303. var node = this.nodes[this.startClickedObj.row];
  8304. this.startClickedObj.xFixed = node.xFixed;
  8305. this.startClickedObj.yFixed = node.yFixed;
  8306. node.xFixed = true;
  8307. node.yFixed = true;
  8308. if (!this.ctrlKeyDown || !node.isSelected()) {
  8309. // select this node
  8310. this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
  8311. }
  8312. else {
  8313. // unselect this node
  8314. this._unselectNodes([this.startClickedObj]);
  8315. }
  8316. if (!this.moving) {
  8317. this._redraw();
  8318. }
  8319. }
  8320. else if (this.shiftKeyDown) {
  8321. // start selection of multiple nodes
  8322. }
  8323. else {
  8324. // start moving the graph
  8325. this.moved = false;
  8326. }
  8327. };
  8328. /**
  8329. * handle on mouse move event
  8330. * @param {Event} event
  8331. * @private
  8332. */
  8333. Graph.prototype._onMouseMove = function (event) {
  8334. event = event || window.event;
  8335. if (!this.selectable) {
  8336. return;
  8337. }
  8338. var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0;
  8339. var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0;
  8340. this.mouseX = mouseX;
  8341. this.mouseY = mouseY;
  8342. if (this.startClickedObj) {
  8343. var node = this.nodes[this.startClickedObj.row];
  8344. if (!this.startClickedObj.xFixed)
  8345. node.x = this._xToCanvas(mouseX - this.startFrameLeft);
  8346. if (!this.startClickedObj.yFixed)
  8347. node.y = this._yToCanvas(mouseY - this.startFrameTop);
  8348. // start animation if not yet running
  8349. if (!this.moving) {
  8350. this.moving = true;
  8351. this.start();
  8352. }
  8353. }
  8354. else if (this.shiftKeyDown) {
  8355. // draw a rect from start mouse location to current mouse location
  8356. if (this.frame.selRect == undefined) {
  8357. this.frame.selRect = document.createElement("DIV");
  8358. this.frame.appendChild(this.frame.selRect);
  8359. this.frame.selRect.style.position = "absolute";
  8360. this.frame.selRect.style.border = "1px dashed red";
  8361. }
  8362. var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
  8363. var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop;
  8364. var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
  8365. var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
  8366. this.frame.selRect.style.left = left + "px";
  8367. this.frame.selRect.style.top = top + "px";
  8368. this.frame.selRect.style.width = (right - left) + "px";
  8369. this.frame.selRect.style.height = (bottom - top) + "px";
  8370. }
  8371. else {
  8372. // move the graph
  8373. var diffX = mouseX - this.startMouseX;
  8374. var diffY = mouseY - this.startMouseY;
  8375. this._setTranslation(
  8376. this.startTranslation.x + diffX,
  8377. this.startTranslation.y + diffY);
  8378. this._redraw();
  8379. this.moved = true;
  8380. }
  8381. vis.util.preventDefault(event);
  8382. };
  8383. /**
  8384. * handle on mouse up event
  8385. * @param {Event} event
  8386. * @private
  8387. */
  8388. Graph.prototype._onMouseUp = function (event) {
  8389. event = event || window.event;
  8390. if (!this.selectable) {
  8391. return;
  8392. }
  8393. // remove event listeners here, important for Safari
  8394. if (this.onmousemove) {
  8395. vis.util.removeEventListener(document, "mousemove", this.onmousemove);
  8396. this.onmousemove = undefined;
  8397. }
  8398. if (this.onmouseup) {
  8399. vis.util.removeEventListener(document, "mouseup", this.onmouseup);
  8400. this.onmouseup = undefined;
  8401. }
  8402. vis.util.preventDefault(event);
  8403. // check selected nodes
  8404. var endMouseX = event.clientX || this.mouseX || 0;
  8405. var endMouseY = event.clientY || this.mouseY || 0;
  8406. var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
  8407. if (this.startClickedObj) {
  8408. // restore the original fixed state
  8409. var node = this.nodes[this.startClickedObj.row];
  8410. node.xFixed = this.startClickedObj.xFixed;
  8411. node.yFixed = this.startClickedObj.yFixed;
  8412. }
  8413. else if (this.shiftKeyDown) {
  8414. // select nodes inside selection area
  8415. var obj = {
  8416. "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
  8417. "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
  8418. "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
  8419. "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
  8420. };
  8421. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8422. this._selectNodes(overlappingNodes, ctrlKey);
  8423. this.redraw();
  8424. // remove the selection rectangle
  8425. if (this.frame.selRect) {
  8426. this.frame.removeChild(this.frame.selRect);
  8427. this.frame.selRect = undefined;
  8428. }
  8429. }
  8430. else {
  8431. if (!this.ctrlKeyDown && !this.moved) {
  8432. // remove selection
  8433. this._unselectNodes();
  8434. this._redraw();
  8435. }
  8436. }
  8437. this.leftButtonDown = false;
  8438. this.ctrlKeyDown = false;
  8439. };
  8440. /**
  8441. * Event handler for mouse wheel event, used to zoom the timeline
  8442. * Code from http://adomas.org/javascript-mouse-wheel/
  8443. * @param {Event} event
  8444. * @private
  8445. */
  8446. Graph.prototype._onMouseWheel = function(event) {
  8447. event = event || window.event;
  8448. var mouseX = event.clientX;
  8449. var mouseY = event.clientY;
  8450. // retrieve delta
  8451. var delta = 0;
  8452. if (event.wheelDelta) { /* IE/Opera. */
  8453. delta = event.wheelDelta/120;
  8454. } else if (event.detail) { /* Mozilla case. */
  8455. // In Mozilla, sign of delta is different than in IE.
  8456. // Also, delta is multiple of 3.
  8457. delta = -event.detail/3;
  8458. }
  8459. // If delta is nonzero, handle it.
  8460. // Basically, delta is now positive if wheel was scrolled up,
  8461. // and negative, if wheel was scrolled down.
  8462. if (delta) {
  8463. // determine zoom factor, and adjust the zoom factor such that zooming in
  8464. // and zooming out correspond wich each other
  8465. var zoom = delta / 10;
  8466. if (delta < 0) {
  8467. zoom = zoom / (1 - zoom);
  8468. }
  8469. var scaleOld = this._getScale();
  8470. var scaleNew = scaleOld * (1 + zoom);
  8471. if (scaleNew < 0.01) {
  8472. scaleNew = 0.01;
  8473. }
  8474. if (scaleNew > 10) {
  8475. scaleNew = 10;
  8476. }
  8477. var frameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  8478. var frameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  8479. var x = mouseX - frameLeft;
  8480. var y = mouseY - frameTop;
  8481. var translation = this._getTranslation();
  8482. var scaleFrac = scaleNew / scaleOld;
  8483. var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
  8484. var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
  8485. this._setScale(scaleNew);
  8486. this._setTranslation(tx, ty);
  8487. this._redraw();
  8488. }
  8489. // Prevent default actions caused by mouse wheel.
  8490. // That might be ugly, but we handle scrolls somehow
  8491. // anyway, so don't bother here...
  8492. vis.util.preventDefault(event);
  8493. };
  8494. /**
  8495. * Mouse move handler for checking whether the title moves over a node with a title.
  8496. * @param {Event} event
  8497. * @private
  8498. */
  8499. Graph.prototype._onMouseMoveTitle = function (event) {
  8500. event = event || window.event;
  8501. var startMouseX = event.clientX;
  8502. var startMouseY = event.clientY;
  8503. this.startFrameLeft = this.startFrameLeft || vis.util.getAbsoluteLeft(this.frame.canvas);
  8504. this.startFrameTop = this.startFrameTop || vis.util.getAbsoluteTop(this.frame.canvas);
  8505. var x = startMouseX - this.startFrameLeft;
  8506. var y = startMouseY - this.startFrameTop;
  8507. // check if the previously selected node is still selected
  8508. if (this.popupNode) {
  8509. this._checkHidePopup(x, y);
  8510. }
  8511. // start a timeout that will check if the mouse is positioned above
  8512. // an element
  8513. var me = this;
  8514. var checkShow = function() {
  8515. me._checkShowPopup(x, y);
  8516. };
  8517. if (this.popupTimer) {
  8518. clearInterval(this.popupTimer); // stop any running timer
  8519. }
  8520. if (!this.leftButtonDown) {
  8521. this.popupTimer = setTimeout(checkShow, 300);
  8522. }
  8523. };
  8524. /**
  8525. * Check if there is an element on the given position in the graph
  8526. * (a node or edge). If so, and if this element has a title,
  8527. * show a popup window with its title.
  8528. *
  8529. * @param {number} x
  8530. * @param {number} y
  8531. * @private
  8532. */
  8533. Graph.prototype._checkShowPopup = function (x, y) {
  8534. var obj = {
  8535. "left" : this._xToCanvas(x),
  8536. "top" : this._yToCanvas(y),
  8537. "right" : this._xToCanvas(x),
  8538. "bottom" : this._yToCanvas(y)
  8539. };
  8540. var i, len;
  8541. var lastPopupNode = this.popupNode;
  8542. if (this.popupNode == undefined) {
  8543. // search the nodes for overlap, select the top one in case of multiple nodes
  8544. var nodes = this.nodes;
  8545. for (i = nodes.length - 1; i >= 0; i--) {
  8546. var node = nodes[i];
  8547. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  8548. this.popupNode = node;
  8549. break;
  8550. }
  8551. }
  8552. }
  8553. if (this.popupNode == undefined) {
  8554. // search the edges for overlap
  8555. var allEdges = this.edges;
  8556. for (i = 0, len = allEdges.length; i < len; i++) {
  8557. var edge = allEdges[i];
  8558. if (edge.getTitle() != undefined && edge.isOverlappingWith(obj)) {
  8559. this.popupNode = edge;
  8560. break;
  8561. }
  8562. }
  8563. }
  8564. if (this.popupNode) {
  8565. // show popup message window
  8566. if (this.popupNode != lastPopupNode) {
  8567. var me = this;
  8568. if (!me.popup) {
  8569. me.popup = new Popup(me.frame);
  8570. }
  8571. // adjust a small offset such that the mouse cursor is located in the
  8572. // bottom left location of the popup, and you can easily move over the
  8573. // popup area
  8574. me.popup.setPosition(x - 3, y - 3);
  8575. me.popup.setText(me.popupNode.getTitle());
  8576. me.popup.show();
  8577. }
  8578. }
  8579. else {
  8580. if (this.popup) {
  8581. this.popup.hide();
  8582. }
  8583. }
  8584. };
  8585. /**
  8586. * Check if the popup must be hided, which is the case when the mouse is no
  8587. * longer hovering on the object
  8588. * @param {number} x
  8589. * @param {number} y
  8590. * @private
  8591. */
  8592. Graph.prototype._checkHidePopup = function (x, y) {
  8593. var obj = {
  8594. "left" : x,
  8595. "top" : y,
  8596. "right" : x,
  8597. "bottom" : y
  8598. };
  8599. if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
  8600. this.popupNode = undefined;
  8601. if (this.popup) {
  8602. this.popup.hide();
  8603. }
  8604. }
  8605. };
  8606. /**
  8607. * Event handler for touchstart event on mobile devices
  8608. * @param {Event} event
  8609. * @private
  8610. */
  8611. Graph.prototype._onTouchStart = function(event) {
  8612. vis.util.preventDefault(event);
  8613. if (this.touchDown) {
  8614. // if already moving, return
  8615. return;
  8616. }
  8617. this.touchDown = true;
  8618. var me = this;
  8619. if (!this.ontouchmove) {
  8620. this.ontouchmove = function (event) {me._onTouchMove(event);};
  8621. vis.util.addEventListener(document, "touchmove", this.ontouchmove);
  8622. }
  8623. if (!this.ontouchend) {
  8624. this.ontouchend = function (event) {me._onTouchEnd(event);};
  8625. vis.util.addEventListener(document, "touchend", this.ontouchend);
  8626. }
  8627. this._onMouseDown(event);
  8628. };
  8629. /**
  8630. * Event handler for touchmove event on mobile devices
  8631. * @param {Event} event
  8632. * @private
  8633. */
  8634. Graph.prototype._onTouchMove = function(event) {
  8635. vis.util.preventDefault(event);
  8636. this._onMouseMove(event);
  8637. };
  8638. /**
  8639. * Event handler for touchend event on mobile devices
  8640. * @param {Event} event
  8641. * @private
  8642. */
  8643. Graph.prototype._onTouchEnd = function(event) {
  8644. vis.util.preventDefault(event);
  8645. this.touchDown = false;
  8646. if (this.ontouchmove) {
  8647. vis.util.removeEventListener(document, "touchmove", this.ontouchmove);
  8648. this.ontouchmove = undefined;
  8649. }
  8650. if (this.ontouchend) {
  8651. vis.util.removeEventListener(document, "touchend", this.ontouchend);
  8652. this.ontouchend = undefined;
  8653. }
  8654. this._onMouseUp(event);
  8655. };
  8656. /**
  8657. * Unselect selected nodes. If no selection array is provided, all nodes
  8658. * are unselected
  8659. * @param {Object[]} selection Array with selection objects, each selection
  8660. * object has a parameter row. Optional
  8661. * @param {Boolean} triggerSelect If true (default), the select event
  8662. * is triggered when nodes are unselected
  8663. * @return {Boolean} changed True if the selection is changed
  8664. * @private
  8665. */
  8666. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  8667. var changed = false;
  8668. var i, iMax, row;
  8669. if (selection) {
  8670. // remove provided selections
  8671. for (i = 0, iMax = selection.length; i < iMax; i++) {
  8672. row = selection[i].row;
  8673. this.nodes[row].unselect();
  8674. var j = 0;
  8675. while (j < this.selection.length) {
  8676. if (this.selection[j].row == row) {
  8677. this.selection.splice(j, 1);
  8678. changed = true;
  8679. }
  8680. else {
  8681. j++;
  8682. }
  8683. }
  8684. }
  8685. }
  8686. else if (this.selection && this.selection.length) {
  8687. // remove all selections
  8688. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  8689. row = this.selection[i].row;
  8690. this.nodes[row].unselect();
  8691. changed = true;
  8692. }
  8693. this.selection = [];
  8694. }
  8695. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  8696. // fire the select event
  8697. this._trigger('select');
  8698. }
  8699. return changed;
  8700. };
  8701. /**
  8702. * select all nodes on given location x, y
  8703. * @param {Array} selection an array with selection objects. Each selection
  8704. * object has a parameter row
  8705. * @param {boolean} append If true, the new selection will be appended to the
  8706. * current selection (except for duplicate entries)
  8707. * @return {Boolean} changed True if the selection is changed
  8708. * @private
  8709. */
  8710. Graph.prototype._selectNodes = function(selection, append) {
  8711. var changed = false;
  8712. var i, iMax;
  8713. // TODO: the selectNodes method is a little messy, rework this
  8714. // check if the current selection equals the desired selection
  8715. var selectionAlreadyDone = true;
  8716. if (selection.length != this.selection.length) {
  8717. selectionAlreadyDone = false;
  8718. }
  8719. else {
  8720. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  8721. if (selection[i].row != this.selection[i].row) {
  8722. selectionAlreadyDone = false;
  8723. break;
  8724. }
  8725. }
  8726. }
  8727. if (selectionAlreadyDone) {
  8728. return changed;
  8729. }
  8730. if (append == undefined || append == false) {
  8731. // first deselect any selected node
  8732. var triggerSelect = false;
  8733. changed = this._unselectNodes(undefined, triggerSelect);
  8734. }
  8735. for (i = 0, iMax = selection.length; i < iMax; i++) {
  8736. // add each of the new selections, but only when they are not duplicate
  8737. var row = selection[i].row;
  8738. var isDuplicate = false;
  8739. for (var j = 0, jMax = this.selection.length; j < jMax; j++) {
  8740. if (this.selection[j].row == row) {
  8741. isDuplicate = true;
  8742. break;
  8743. }
  8744. }
  8745. if (!isDuplicate) {
  8746. this.nodes[row].select();
  8747. this.selection.push(selection[i]);
  8748. changed = true;
  8749. }
  8750. }
  8751. if (changed) {
  8752. // fire the select event
  8753. this._trigger('select');
  8754. }
  8755. return changed;
  8756. };
  8757. /**
  8758. * retrieve all nodes overlapping with given object
  8759. * @param {Object} obj An object with parameters left, top, right, bottom
  8760. * @return {Object[]} An array with selection objects containing
  8761. * the parameter row.
  8762. * @private
  8763. */
  8764. Graph.prototype._getNodesOverlappingWith = function (obj) {
  8765. var overlappingNodes = [];
  8766. for (var i = 0; i < this.nodes.length; i++) {
  8767. if (this.nodes[i].isOverlappingWith(obj)) {
  8768. var sel = {"row": i};
  8769. overlappingNodes.push(sel);
  8770. }
  8771. }
  8772. return overlappingNodes;
  8773. };
  8774. /**
  8775. * retrieve the currently selected nodes
  8776. * @return {Object[]} an array with zero or more objects. Each object
  8777. * contains the parameter row
  8778. */
  8779. Graph.prototype.getSelection = function() {
  8780. var selection = [];
  8781. for (var i = 0; i < this.selection.length; i++) {
  8782. var row = this.selection[i].row;
  8783. selection.push({"row": row});
  8784. }
  8785. return selection;
  8786. };
  8787. /**
  8788. * select zero or more nodes
  8789. * @param {object[]} selection an array with zero or more objects. Each object
  8790. * contains the parameter row
  8791. */
  8792. Graph.prototype.setSelection = function(selection) {
  8793. var i, iMax, row;
  8794. if (selection.length == undefined)
  8795. throw "Selection must be an array with objects";
  8796. // first unselect any selected node
  8797. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  8798. row = this.selection[i].row;
  8799. this.nodes[row].unselect();
  8800. }
  8801. this.selection = [];
  8802. for (i = 0, iMax = selection.length; i < iMax; i++) {
  8803. row = selection[i].row;
  8804. if (row == undefined)
  8805. throw "Parameter row missing in selection object";
  8806. if (row > this.nodes.length-1)
  8807. throw "Parameter row out of range";
  8808. var sel = {"row": row};
  8809. this.selection.push(sel);
  8810. this.nodes[row].select();
  8811. }
  8812. this.redraw();
  8813. };
  8814. /**
  8815. * Temporary method to test calculating a hub value for the nodes
  8816. * @param {number} level Maximum number edges between two nodes in order
  8817. * to call them connected. Optional, 1 by default
  8818. * @return {Number[]} connectioncount array with the connection count
  8819. * for each node
  8820. * @private
  8821. */
  8822. Graph.prototype._getConnectionCount = function(level) {
  8823. var conn = this.edges;
  8824. if (level == undefined) {
  8825. level = 1;
  8826. }
  8827. // get the nodes connected to given nodes
  8828. function getConnectedNodes(nodes) {
  8829. var connectedNodes = [];
  8830. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  8831. var node = nodes[j];
  8832. // find all nodes connected to this node
  8833. for (var i = 0, iMax = conn.length; i < iMax; i++) {
  8834. var other = null;
  8835. // check if connected
  8836. if (conn[i].from == node)
  8837. other = conn[i].to;
  8838. else if (conn[i].to == node)
  8839. other = conn[i].from;
  8840. // check if the other node is not already in the list with nodes
  8841. var k, kMax;
  8842. if (other) {
  8843. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  8844. if (nodes[k] == other) {
  8845. other = null;
  8846. break;
  8847. }
  8848. }
  8849. }
  8850. if (other) {
  8851. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  8852. if (connectedNodes[k] == other) {
  8853. other = null;
  8854. break;
  8855. }
  8856. }
  8857. }
  8858. if (other)
  8859. connectedNodes.push(other);
  8860. }
  8861. }
  8862. return connectedNodes;
  8863. }
  8864. var connections = [];
  8865. var level0 = [];
  8866. var nodes = this.nodes;
  8867. var i, iMax;
  8868. for (i = 0, iMax = nodes.length; i < iMax; i++) {
  8869. var c = [nodes[i]];
  8870. for (var l = 0; l < level; l++) {
  8871. c = c.concat(getConnectedNodes(c));
  8872. }
  8873. connections.push(c);
  8874. }
  8875. var hubs = [];
  8876. for (i = 0, len = connections.length; i < len; i++) {
  8877. hubs.push(connections[i].length);
  8878. }
  8879. return hubs;
  8880. };
  8881. /**
  8882. * Set a new size for the graph
  8883. * @param {string} width Width in pixels or percentage (for example "800px"
  8884. * or "50%")
  8885. * @param {string} height Height in pixels or percentage (for example "400px"
  8886. * or "30%")
  8887. */
  8888. Graph.prototype.setSize = function(width, height) {
  8889. this.frame.style.width = width;
  8890. this.frame.style.height = height;
  8891. this.frame.canvas.style.width = "100%";
  8892. this.frame.canvas.style.height = "100%";
  8893. this.frame.canvas.width = this.frame.canvas.clientWidth;
  8894. this.frame.canvas.height = this.frame.canvas.clientHeight;
  8895. };
  8896. /**
  8897. * Set a data set with nodes for the graph
  8898. * @param {Array} nodes The data containing the nodes.
  8899. * @private
  8900. */
  8901. Graph.prototype._setNodes = function(nodes) {
  8902. this.selection = [];
  8903. this.nodes = [];
  8904. this.moving = false;
  8905. if (!nodes) {
  8906. return;
  8907. }
  8908. var hasValues = false;
  8909. var rowCount = nodes.length;
  8910. for (var i = 0; i < rowCount; i++) {
  8911. var properties = nodes[i];
  8912. if (properties.value != undefined) {
  8913. hasValues = true;
  8914. }
  8915. if (properties.id == undefined) {
  8916. throw "Column 'id' missing in table with nodes (row " + i + ")";
  8917. }
  8918. this._createNode(properties);
  8919. }
  8920. // calculate scaling function when value is provided
  8921. if (hasValues) {
  8922. this._updateValueRange(this.nodes);
  8923. }
  8924. // give the nodes some first (random) position
  8925. this._reposition(); // TODO: bad solution
  8926. };
  8927. /**
  8928. * Create a node with the given properties
  8929. * If the new node has an id identical to an existing node, the existing
  8930. * node will be overwritten.
  8931. * The properties can contain a property "action", which can have values
  8932. * "create", "update", or "delete"
  8933. * @param {Object} properties An object with properties
  8934. * @private
  8935. */
  8936. Graph.prototype._createNode = function(properties) {
  8937. var action = properties.action ? properties.action : "update";
  8938. var id, index, newNode, oldNode;
  8939. if (action === "create") {
  8940. // create the node
  8941. newNode = new Node(properties, this.images, this.groups, this.constants);
  8942. id = properties.id;
  8943. index = (id !== undefined) ? this._findNode(id) : undefined;
  8944. if (index !== undefined) {
  8945. // replace node
  8946. oldNode = this.nodes[index];
  8947. this.nodes[index] = newNode;
  8948. // remove selection of old node
  8949. if (oldNode.selected) {
  8950. this._unselectNodes([{'row': index}], false);
  8951. }
  8952. /* TODO: implement this? -> will give performance issues, searching all edges and nodes...
  8953. // update edges linking to this node
  8954. var edgesTable = this.edges;
  8955. for (var i = 0, iMax = edgesTable.length; i < iMax; i++) {
  8956. var edge = edgesTable[i];
  8957. if (edge.from == oldNode) {
  8958. edge.from = newNode;
  8959. }
  8960. if (edge.to == oldNode) {
  8961. edge.to = newNode;
  8962. }
  8963. }
  8964. */
  8965. }
  8966. else {
  8967. // add new node
  8968. this.nodes.push(newNode);
  8969. }
  8970. if (!newNode.isFixed()) {
  8971. // note: no not use node.isMoving() here, as that gives the current
  8972. // velocity of the node, which is zero after creation of the node.
  8973. this.moving = true;
  8974. }
  8975. }
  8976. else if (action === "update") {
  8977. // update existing node, or create it when not yet existing
  8978. id = properties.id;
  8979. if (id === undefined) {
  8980. throw "Cannot update a node without id";
  8981. }
  8982. index = this._findNode(id);
  8983. if (index !== undefined) {
  8984. // update node
  8985. this.nodes[index].setProperties(properties, this.constants);
  8986. }
  8987. else {
  8988. // create node
  8989. newNode = new Node(properties, this.images, this.groups, this.constants);
  8990. this.nodes.push(newNode);
  8991. if (!newNode.isFixed()) {
  8992. // note: no not use node.isMoving() here, as that gives the current
  8993. // velocity of the node, which is zero after creation of the node.
  8994. this.moving = true;
  8995. }
  8996. }
  8997. }
  8998. else if (action === "delete") {
  8999. // delete existing node
  9000. id = properties.id;
  9001. if (id === undefined) {
  9002. throw "Cannot delete node without its id";
  9003. }
  9004. index = this._findNode(id);
  9005. if (index !== undefined) {
  9006. oldNode = this.nodes[index];
  9007. // remove selection of old node
  9008. if (oldNode.selected) {
  9009. this._unselectNodes([{'row': index}], false);
  9010. }
  9011. this.nodes.splice(index, 1);
  9012. }
  9013. else {
  9014. throw "Node with id " + id + " not found";
  9015. }
  9016. }
  9017. else {
  9018. throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
  9019. }
  9020. };
  9021. /**
  9022. * Find a node by its id
  9023. * @param {Number} id Id of the node
  9024. * @return {Number | undefined} index Index of the node in the array
  9025. * this.nodes, or undefined when not found
  9026. * @private
  9027. */
  9028. Graph.prototype._findNode = function (id) {
  9029. var nodes = this.nodes;
  9030. for (var n = 0, len = nodes.length; n < len; n++) {
  9031. if (nodes[n].id === id) {
  9032. return n;
  9033. }
  9034. }
  9035. return undefined;
  9036. };
  9037. /**
  9038. * Find a node by its rowNumber
  9039. * @param {Number} row Row number of the node
  9040. * @return {Node} node     The node with the given row number, or
  9041. *                            undefined when not found.
  9042. * @private
  9043. */
  9044. Graph.prototype._findNodeByRow = function (row) {
  9045. return this.nodes[row];
  9046. };
  9047. /**
  9048. * Load edges by reading the data table
  9049. * @param {Array} edges The data containing the edges.
  9050. * @private
  9051. * @private
  9052. */
  9053. Graph.prototype._setEdges = function(edges) {
  9054. this.edges = [];
  9055. if (!edges) {
  9056. return;
  9057. }
  9058. var hasValues = false;
  9059. var rowCount = edges.length;
  9060. for (var i = 0; i < rowCount; i++) {
  9061. var properties = edges[i];
  9062. if (properties.from === undefined) {
  9063. throw "Column 'from' missing in table with edges (row " + i + ")";
  9064. }
  9065. if (properties.to === undefined) {
  9066. throw "Column 'to' missing in table with edges (row " + i + ")";
  9067. }
  9068. if (properties.value != undefined) {
  9069. hasValues = true;
  9070. }
  9071. this._createEdge(properties);
  9072. }
  9073. // calculate scaling function when value is provided
  9074. if (hasValues) {
  9075. this._updateValueRange(this.edges);
  9076. }
  9077. };
  9078. /**
  9079. * Create a edge with the given properties
  9080. * If the new edge has an id identical to an existing edge, the existing
  9081. * edge will be overwritten or updated.
  9082. * The properties can contain a property "action", which can have values
  9083. * "create", "update", or "delete"
  9084. * @param {Object} properties An object with properties
  9085. * @private
  9086. */
  9087. Graph.prototype._createEdge = function(properties) {
  9088. var action = properties.action ? properties.action : "create";
  9089. var id, index, edge, oldEdge, newEdge;
  9090. if (action === "create") {
  9091. // create the edge, or replace it if already existing
  9092. id = properties.id;
  9093. index = (id !== undefined) ? this._findEdge(id) : undefined;
  9094. edge = new Edge(properties, this, this.constants);
  9095. if (index !== undefined) {
  9096. // replace existing edge
  9097. oldEdge = this.edges[index];
  9098. oldEdge.from.detachEdge(oldEdge);
  9099. oldEdge.to.detachEdge(oldEdge);
  9100. this.edges[index] = edge;
  9101. }
  9102. else {
  9103. // add new edge
  9104. this.edges.push(edge);
  9105. }
  9106. edge.from.attachEdge(edge);
  9107. edge.to.attachEdge(edge);
  9108. }
  9109. else if (action === "update") {
  9110. // update existing edge, or create the edge if not existing
  9111. id = properties.id;
  9112. if (id === undefined) {
  9113. throw "Cannot update a edge without id";
  9114. }
  9115. index = this._findEdge(id);
  9116. if (index !== undefined) {
  9117. // update edge
  9118. edge = this.edges[index];
  9119. edge.from.detachEdge(edge);
  9120. edge.to.detachEdge(edge);
  9121. edge.setProperties(properties, this.constants);
  9122. edge.from.attachEdge(edge);
  9123. edge.to.attachEdge(edge);
  9124. }
  9125. else {
  9126. // add new edge
  9127. edge = new Edge(properties, this, this.constants);
  9128. edge.from.attachEdge(edge);
  9129. edge.to.attachEdge(edge);
  9130. this.edges.push(edge);
  9131. }
  9132. }
  9133. else if (action === "delete") {
  9134. // delete existing edge
  9135. id = properties.id;
  9136. if (id === undefined) {
  9137. throw "Cannot delete edge without its id";
  9138. }
  9139. index = this._findEdge(id);
  9140. if (index !== undefined) {
  9141. oldEdge = this.edges[id];
  9142. edge.from.detachEdge(oldEdge);
  9143. edge.to.detachEdge(oldEdge);
  9144. this.edges.splice(index, 1);
  9145. }
  9146. else {
  9147. throw "Edge with id " + id + " not found";
  9148. }
  9149. }
  9150. else {
  9151. throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'.";
  9152. }
  9153. };
  9154. /**
  9155. * Update the references to oldNode in all edges.
  9156. * @param {Node} oldNode
  9157. * @param {Node} newNode
  9158. * @private
  9159. */
  9160. // TODO: start utilizing this method _updateNodeReferences
  9161. Graph.prototype._updateNodeReferences = function(oldNode, newNode) {
  9162. var edges = this.edges;
  9163. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9164. var edge = edges[i];
  9165. if (edge.from === oldNode) {
  9166. edge.from = newNode;
  9167. }
  9168. if (edge.to === oldNode) {
  9169. edge.to = newNode;
  9170. }
  9171. }
  9172. };
  9173. /**
  9174. * Find a edge by its id
  9175. * @param {Number} id Id of the edge
  9176. * @return {Number | undefined} index Index of the edge in the array
  9177. * this.edges, or undefined when not found
  9178. * @private
  9179. */
  9180. Graph.prototype._findEdge = function (id) {
  9181. var edges = this.edges;
  9182. for (var n = 0, len = edges.length; n < len; n++) {
  9183. if (edges[n].id === id) {
  9184. return n;
  9185. }
  9186. }
  9187. return undefined;
  9188. };
  9189. /**
  9190. * Find a edge by its row
  9191. * @param {Number} row Row of the edge
  9192. * @return {Edge | undefined} the found edge, or undefined when not found
  9193. * @private
  9194. */
  9195. Graph.prototype._findEdgeByRow = function (row) {
  9196. return this.edges[row];
  9197. };
  9198. /**
  9199. * Update the values of all object in the given array according to the current
  9200. * value range of the objects in the array.
  9201. * @param {Array} array. An array with objects like Edges or Nodes
  9202. * The objects must have a method getValue() and
  9203. * setValueRange(min, max).
  9204. * @private
  9205. */
  9206. Graph.prototype._updateValueRange = function(array) {
  9207. var count = array.length;
  9208. var i;
  9209. // determine the range of the node values
  9210. var valueMin = undefined;
  9211. var valueMax = undefined;
  9212. for (i = 0; i < count; i++) {
  9213. var value = array[i].getValue();
  9214. if (value !== undefined) {
  9215. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  9216. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  9217. }
  9218. }
  9219. // adjust the range of all nodes
  9220. if (valueMin !== undefined && valueMax !== undefined) {
  9221. for (i = 0; i < count; i++) {
  9222. array[i].setValueRange(valueMin, valueMax);
  9223. }
  9224. }
  9225. };
  9226. /**
  9227. * Redraw the graph with the current data
  9228. * chart will be resized too.
  9229. */
  9230. Graph.prototype.redraw = function() {
  9231. this.setSize(this.width, this.height);
  9232. this._redraw();
  9233. };
  9234. /**
  9235. * Redraw the graph with the current data
  9236. * @private
  9237. */
  9238. Graph.prototype._redraw = function() {
  9239. var ctx = this.frame.canvas.getContext("2d");
  9240. // clear the canvas
  9241. var w = this.frame.canvas.width;
  9242. var h = this.frame.canvas.height;
  9243. ctx.clearRect(0, 0, w, h);
  9244. // set scaling and translation
  9245. ctx.save();
  9246. ctx.translate(this.translation.x, this.translation.y);
  9247. ctx.scale(this.scale, this.scale);
  9248. this._drawEdges(ctx);
  9249. this._drawNodes(ctx);
  9250. // restore original scaling and translation
  9251. ctx.restore();
  9252. };
  9253. /**
  9254. * Set the translation of the graph
  9255. * @param {Number} offsetX Horizontal offset
  9256. * @param {Number} offsetY Vertical offset
  9257. * @private
  9258. */
  9259. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  9260. if (this.translation === undefined) {
  9261. this.translation = {
  9262. "x": 0,
  9263. "y": 0
  9264. };
  9265. }
  9266. if (offsetX !== undefined) {
  9267. this.translation.x = offsetX;
  9268. }
  9269. if (offsetY !== undefined) {
  9270. this.translation.y = offsetY;
  9271. }
  9272. };
  9273. /**
  9274. * Get the translation of the graph
  9275. * @return {Object} translation An object with parameters x and y, both a number
  9276. * @private
  9277. */
  9278. Graph.prototype._getTranslation = function() {
  9279. return {
  9280. "x": this.translation.x,
  9281. "y": this.translation.y
  9282. };
  9283. };
  9284. /**
  9285. * Scale the graph
  9286. * @param {Number} scale Scaling factor 1.0 is unscaled
  9287. * @private
  9288. */
  9289. Graph.prototype._setScale = function(scale) {
  9290. this.scale = scale;
  9291. };
  9292. /**
  9293. * Get the current scale of the graph
  9294. * @return {Number} scale Scaling factor 1.0 is unscaled
  9295. * @private
  9296. */
  9297. Graph.prototype._getScale = function() {
  9298. return this.scale;
  9299. };
  9300. Graph.prototype._xToCanvas = function(x) {
  9301. return (x - this.translation.x) / this.scale;
  9302. };
  9303. Graph.prototype._canvasToX = function(x) {
  9304. return x * this.scale + this.translation.x;
  9305. };
  9306. Graph.prototype._yToCanvas = function(y) {
  9307. return (y - this.translation.y) / this.scale;
  9308. };
  9309. Graph.prototype._canvasToY = function(y) {
  9310. return y * this.scale + this.translation.y ;
  9311. };
  9312. /**
  9313. * Get a node by its id
  9314. * @param {number} id
  9315. * @return {Node} node, or null if not found
  9316. * @private
  9317. */
  9318. Graph.prototype._getNode = function(id) {
  9319. for (var i = 0; i < this.nodes.length; i++) {
  9320. if (this.nodes[i].id == id)
  9321. return this.nodes[i];
  9322. }
  9323. return null;
  9324. };
  9325. /**
  9326. * Redraw all nodes
  9327. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9328. * @param {CanvasRenderingContext2D} ctx
  9329. * @private
  9330. */
  9331. Graph.prototype._drawNodes = function(ctx) {
  9332. // first draw the unselected nodes
  9333. var nodes = this.nodes;
  9334. var selected = [];
  9335. for (var i = 0, iMax = nodes.length; i < iMax; i++) {
  9336. if (nodes[i].isSelected()) {
  9337. selected.push(i);
  9338. }
  9339. else {
  9340. nodes[i].draw(ctx);
  9341. }
  9342. }
  9343. // draw the selected nodes on top
  9344. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  9345. nodes[selected[s]].draw(ctx);
  9346. }
  9347. };
  9348. /**
  9349. * Redraw all edges
  9350. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9351. * @param {CanvasRenderingContext2D} ctx
  9352. * @private
  9353. */
  9354. Graph.prototype._drawEdges = function(ctx) {
  9355. var edges = this.edges;
  9356. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9357. edges[i].draw(ctx);
  9358. }
  9359. };
  9360. /**
  9361. * Recalculate the best positions for all nodes
  9362. * @private
  9363. */
  9364. Graph.prototype._reposition = function() {
  9365. // TODO: implement function reposition
  9366. /*
  9367. var w = this.frame.canvas.clientWidth;
  9368. var h = this.frame.canvas.clientHeight;
  9369. for (var i = 0; i < this.nodes.length; i++) {
  9370. if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random();
  9371. if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random();
  9372. }
  9373. //*/
  9374. //*
  9375. // TODO
  9376. var radius = this.constants.edges.length * 2;
  9377. var cx = this.frame.canvas.clientWidth / 2;
  9378. var cy = this.frame.canvas.clientHeight / 2;
  9379. for (var i = 0; i < this.nodes.length; i++) {
  9380. var angle = 2*Math.PI * (i / this.nodes.length);
  9381. if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle);
  9382. if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle);
  9383. }
  9384. //*/
  9385. /*
  9386. // TODO
  9387. var radius = this.constants.edges.length * 2;
  9388. var w = this.frame.canvas.clientWidth,
  9389. h = this.frame.canvas.clientHeight;
  9390. var cx = this.frame.canvas.clientWidth / 2;
  9391. var cy = this.frame.canvas.clientHeight / 2;
  9392. var s = Math.sqrt(this.nodes.length);
  9393. for (var i = 0; i < this.nodes.length; i++) {
  9394. //var angle = 2*Math.PI * (i / this.nodes.length);
  9395. if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s);
  9396. if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s);
  9397. }
  9398. //*/
  9399. /*
  9400. var cx = this.frame.canvas.clientWidth / 2;
  9401. var cy = this.frame.canvas.clientHeight / 2;
  9402. for (var i = 0; i < this.nodes.length; i++) {
  9403. this.nodes[i].x = cx;
  9404. this.nodes[i].y = cy;
  9405. }
  9406. //*/
  9407. };
  9408. /**
  9409. * Find a stable position for all nodes
  9410. * @private
  9411. */
  9412. Graph.prototype._doStabilize = function() {
  9413. var start = new Date();
  9414. // find stable position
  9415. var count = 0;
  9416. var vmin = this.constants.minVelocity;
  9417. var stable = false;
  9418. while (!stable && count < this.constants.maxIterations) {
  9419. this._calculateForces();
  9420. this._discreteStepNodes();
  9421. stable = !this._isMoving(vmin);
  9422. count++;
  9423. }
  9424. var end = new Date();
  9425. // console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
  9426. };
  9427. /**
  9428. * Calculate the external forces acting on the nodes
  9429. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9430. * @private
  9431. */
  9432. Graph.prototype._calculateForces = function() {
  9433. // create a local edge to the nodes and edges, that is faster
  9434. var nodes = this.nodes,
  9435. edges = this.edges;
  9436. // gravity, add a small constant force to pull the nodes towards the center of
  9437. // the graph
  9438. // Also, the forces are reset to zero in this loop by using _setForce instead
  9439. // of _addForce
  9440. var gravity = 0.01,
  9441. gx = this.frame.canvas.clientWidth / 2,
  9442. gy = this.frame.canvas.clientHeight / 2;
  9443. for (var n = 0; n < nodes.length; n++) {
  9444. var dx = gx - nodes[n].x,
  9445. dy = gy - nodes[n].y,
  9446. angle = Math.atan2(dy, dx),
  9447. fx = Math.cos(angle) * gravity,
  9448. fy = Math.sin(angle) * gravity;
  9449. this.nodes[n]._setForce(fx, fy);
  9450. }
  9451. // repulsing forces between nodes
  9452. var minimumDistance = this.constants.nodes.distance,
  9453. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9454. for (var n = 0; n < nodes.length; n++) {
  9455. for (var n2 = n + 1; n2 < this.nodes.length; n2++) {
  9456. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  9457. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  9458. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  9459. // calculate normally distributed force
  9460. var dx = nodes[n2].x - nodes[n].x,
  9461. dy = nodes[n2].y - nodes[n].y,
  9462. distance = Math.sqrt(dx * dx + dy * dy),
  9463. angle = Math.atan2(dy, dx),
  9464. // TODO: correct factor for repulsing force
  9465. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9466. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9467. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  9468. fx = Math.cos(angle) * repulsingforce,
  9469. fy = Math.sin(angle) * repulsingforce;
  9470. this.nodes[n]._addForce(-fx, -fy);
  9471. this.nodes[n2]._addForce(fx, fy);
  9472. }
  9473. /* TODO: re-implement repulsion of edges
  9474. for (var l = 0; l < edges.length; l++) {
  9475. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9476. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9477. // calculate normally distributed force
  9478. dx = nodes[n].x - lx,
  9479. dy = nodes[n].y - ly,
  9480. distance = Math.sqrt(dx * dx + dy * dy),
  9481. angle = Math.atan2(dy, dx),
  9482. // TODO: correct factor for repulsing force
  9483. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9484. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9485. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  9486. fx = Math.cos(angle) * repulsingforce,
  9487. fy = Math.sin(angle) * repulsingforce;
  9488. nodes[n]._addForce(fx, fy);
  9489. edges[l].from._addForce(-fx/2,-fy/2);
  9490. edges[l].to._addForce(-fx/2,-fy/2);
  9491. }
  9492. */
  9493. }
  9494. // forces caused by the edges, modelled as springs
  9495. for (var l = 0, lMax = edges.length; l < lMax; l++) {
  9496. var edge = edges[l],
  9497. dx = (edge.to.x - edge.from.x),
  9498. dy = (edge.to.y - edge.from.y),
  9499. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin
  9500. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin
  9501. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2,
  9502. edgeLength = edge.length,
  9503. length = Math.sqrt(dx * dx + dy * dy),
  9504. angle = Math.atan2(dy, dx),
  9505. springforce = edge.stiffness * (edgeLength - length),
  9506. fx = Math.cos(angle) * springforce,
  9507. fy = Math.sin(angle) * springforce;
  9508. edge.from._addForce(-fx, -fy);
  9509. edge.to._addForce(fx, fy);
  9510. }
  9511. /* TODO: re-implement repulsion of edges
  9512. // repulsing forces between edges
  9513. var minimumDistance = this.constants.edges.distance,
  9514. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9515. for (var l = 0; l < edges.length; l++) {
  9516. //Keep distance from other edge centers
  9517. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  9518. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  9519. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  9520. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  9521. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9522. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9523. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  9524. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  9525. // calculate normally distributed force
  9526. dx = l2x - lx,
  9527. dy = l2y - ly,
  9528. distance = Math.sqrt(dx * dx + dy * dy),
  9529. angle = Math.atan2(dy, dx),
  9530. // TODO: correct factor for repulsing force
  9531. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9532. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9533. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  9534. fx = Math.cos(angle) * repulsingforce,
  9535. fy = Math.sin(angle) * repulsingforce;
  9536. edges[l].from._addForce(-fx, -fy);
  9537. edges[l].to._addForce(-fx, -fy);
  9538. edges[l2].from._addForce(fx, fy);
  9539. edges[l2].to._addForce(fx, fy);
  9540. }
  9541. }
  9542. */
  9543. };
  9544. /**
  9545. * Check if any of the nodes is still moving
  9546. * @param {number} vmin the minimum velocity considered as "moving"
  9547. * @return {boolean} true if moving, false if non of the nodes is moving
  9548. * @private
  9549. */
  9550. Graph.prototype._isMoving = function(vmin) {
  9551. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  9552. var nodes = this.nodes;
  9553. for (var n = 0, nMax = nodes.length; n < nMax; n++) {
  9554. if (nodes[n].isMoving(vmin)) {
  9555. return true;
  9556. }
  9557. }
  9558. return false;
  9559. };
  9560. /**
  9561. * Perform one discrete step for all nodes
  9562. * @private
  9563. */
  9564. Graph.prototype._discreteStepNodes = function() {
  9565. var interval = this.refreshRate / 1000.0; // in seconds
  9566. var nodes = this.nodes;
  9567. for (var n = 0, nMax = nodes.length; n < nMax; n++) {
  9568. nodes[n].discreteStep(interval);
  9569. }
  9570. };
  9571. /**
  9572. * Start animating nodes and edges
  9573. */
  9574. Graph.prototype.start = function() {
  9575. if (this.moving) {
  9576. this._calculateForces();
  9577. this._discreteStepNodes();
  9578. var vmin = this.constants.minVelocity;
  9579. this.moving = this._isMoving(vmin);
  9580. }
  9581. if (this.moving) {
  9582. // start animation. only start timer if it is not already running
  9583. if (!this.timer) {
  9584. var graph = this;
  9585. this.timer = window.setTimeout(function () {
  9586. graph.timer = undefined;
  9587. graph.start();
  9588. graph._redraw();
  9589. }, this.refreshRate);
  9590. }
  9591. }
  9592. else {
  9593. this._redraw();
  9594. }
  9595. };
  9596. /**
  9597. * Stop animating nodes and edges.
  9598. */
  9599. Graph.prototype.stop = function () {
  9600. if (this.timer) {
  9601. window.clearInterval(this.timer);
  9602. this.timer = undefined;
  9603. }
  9604. };
  9605. /**
  9606. * vis.js module exports
  9607. */
  9608. var vis = {
  9609. util: util,
  9610. events: events,
  9611. Controller: Controller,
  9612. DataSet: DataSet,
  9613. DataView: DataView,
  9614. Range: Range,
  9615. Stack: Stack,
  9616. TimeStep: TimeStep,
  9617. EventBus: EventBus,
  9618. components: {
  9619. items: {
  9620. Item: Item,
  9621. ItemBox: ItemBox,
  9622. ItemPoint: ItemPoint,
  9623. ItemRange: ItemRange
  9624. },
  9625. Component: Component,
  9626. Panel: Panel,
  9627. RootPanel: RootPanel,
  9628. ItemSet: ItemSet,
  9629. TimeAxis: TimeAxis
  9630. },
  9631. graph: {
  9632. Node: Node,
  9633. Edge: Edge,
  9634. Popup: Popup,
  9635. Groups: Groups,
  9636. Images: Images
  9637. },
  9638. Timeline: Timeline,
  9639. Graph: Graph
  9640. };
  9641. /**
  9642. * CommonJS module exports
  9643. */
  9644. if (typeof exports !== 'undefined') {
  9645. exports = vis;
  9646. }
  9647. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  9648. module.exports = vis;
  9649. }
  9650. /**
  9651. * AMD module exports
  9652. */
  9653. if (typeof(define) === 'function') {
  9654. define(function () {
  9655. return vis;
  9656. });
  9657. }
  9658. /**
  9659. * Window exports
  9660. */
  9661. if (typeof window !== 'undefined') {
  9662. // attach the module to the window, load as a regular javascript file
  9663. window['vis'] = vis;
  9664. }
  9665. // inject css
  9666. 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");
  9667. })()
  9668. },{"moment":2}],2:[function(require,module,exports){
  9669. (function(){// moment.js
  9670. // version : 2.0.0
  9671. // author : Tim Wood
  9672. // license : MIT
  9673. // momentjs.com
  9674. (function (undefined) {
  9675. /************************************
  9676. Constants
  9677. ************************************/
  9678. var moment,
  9679. VERSION = "2.0.0",
  9680. round = Math.round, i,
  9681. // internal storage for language config files
  9682. languages = {},
  9683. // check for nodeJS
  9684. hasModule = (typeof module !== 'undefined' && module.exports),
  9685. // ASP.NET json date format regex
  9686. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  9687. // format tokens
  9688. 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,
  9689. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  9690. // parsing tokens
  9691. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  9692. // parsing token regexes
  9693. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  9694. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  9695. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  9696. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  9697. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  9698. 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.
  9699. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  9700. parseTokenT = /T/i, // T (ISO seperator)
  9701. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  9702. // preliminary iso regex
  9703. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  9704. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  9705. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  9706. // iso time formats and regexes
  9707. isoTimes = [
  9708. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  9709. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  9710. ['HH:mm', /(T| )\d\d:\d\d/],
  9711. ['HH', /(T| )\d\d/]
  9712. ],
  9713. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  9714. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  9715. // getter and setter names
  9716. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  9717. unitMillisecondFactors = {
  9718. 'Milliseconds' : 1,
  9719. 'Seconds' : 1e3,
  9720. 'Minutes' : 6e4,
  9721. 'Hours' : 36e5,
  9722. 'Days' : 864e5,
  9723. 'Months' : 2592e6,
  9724. 'Years' : 31536e6
  9725. },
  9726. // format function strings
  9727. formatFunctions = {},
  9728. // tokens to ordinalize and pad
  9729. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  9730. paddedTokens = 'M D H h m s w W'.split(' '),
  9731. formatTokenFunctions = {
  9732. M : function () {
  9733. return this.month() + 1;
  9734. },
  9735. MMM : function (format) {
  9736. return this.lang().monthsShort(this, format);
  9737. },
  9738. MMMM : function (format) {
  9739. return this.lang().months(this, format);
  9740. },
  9741. D : function () {
  9742. return this.date();
  9743. },
  9744. DDD : function () {
  9745. return this.dayOfYear();
  9746. },
  9747. d : function () {
  9748. return this.day();
  9749. },
  9750. dd : function (format) {
  9751. return this.lang().weekdaysMin(this, format);
  9752. },
  9753. ddd : function (format) {
  9754. return this.lang().weekdaysShort(this, format);
  9755. },
  9756. dddd : function (format) {
  9757. return this.lang().weekdays(this, format);
  9758. },
  9759. w : function () {
  9760. return this.week();
  9761. },
  9762. W : function () {
  9763. return this.isoWeek();
  9764. },
  9765. YY : function () {
  9766. return leftZeroFill(this.year() % 100, 2);
  9767. },
  9768. YYYY : function () {
  9769. return leftZeroFill(this.year(), 4);
  9770. },
  9771. YYYYY : function () {
  9772. return leftZeroFill(this.year(), 5);
  9773. },
  9774. a : function () {
  9775. return this.lang().meridiem(this.hours(), this.minutes(), true);
  9776. },
  9777. A : function () {
  9778. return this.lang().meridiem(this.hours(), this.minutes(), false);
  9779. },
  9780. H : function () {
  9781. return this.hours();
  9782. },
  9783. h : function () {
  9784. return this.hours() % 12 || 12;
  9785. },
  9786. m : function () {
  9787. return this.minutes();
  9788. },
  9789. s : function () {
  9790. return this.seconds();
  9791. },
  9792. S : function () {
  9793. return ~~(this.milliseconds() / 100);
  9794. },
  9795. SS : function () {
  9796. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  9797. },
  9798. SSS : function () {
  9799. return leftZeroFill(this.milliseconds(), 3);
  9800. },
  9801. Z : function () {
  9802. var a = -this.zone(),
  9803. b = "+";
  9804. if (a < 0) {
  9805. a = -a;
  9806. b = "-";
  9807. }
  9808. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  9809. },
  9810. ZZ : function () {
  9811. var a = -this.zone(),
  9812. b = "+";
  9813. if (a < 0) {
  9814. a = -a;
  9815. b = "-";
  9816. }
  9817. return b + leftZeroFill(~~(10 * a / 6), 4);
  9818. },
  9819. X : function () {
  9820. return this.unix();
  9821. }
  9822. };
  9823. function padToken(func, count) {
  9824. return function (a) {
  9825. return leftZeroFill(func.call(this, a), count);
  9826. };
  9827. }
  9828. function ordinalizeToken(func) {
  9829. return function (a) {
  9830. return this.lang().ordinal(func.call(this, a));
  9831. };
  9832. }
  9833. while (ordinalizeTokens.length) {
  9834. i = ordinalizeTokens.pop();
  9835. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  9836. }
  9837. while (paddedTokens.length) {
  9838. i = paddedTokens.pop();
  9839. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  9840. }
  9841. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  9842. /************************************
  9843. Constructors
  9844. ************************************/
  9845. function Language() {
  9846. }
  9847. // Moment prototype object
  9848. function Moment(config) {
  9849. extend(this, config);
  9850. }
  9851. // Duration Constructor
  9852. function Duration(duration) {
  9853. var data = this._data = {},
  9854. years = duration.years || duration.year || duration.y || 0,
  9855. months = duration.months || duration.month || duration.M || 0,
  9856. weeks = duration.weeks || duration.week || duration.w || 0,
  9857. days = duration.days || duration.day || duration.d || 0,
  9858. hours = duration.hours || duration.hour || duration.h || 0,
  9859. minutes = duration.minutes || duration.minute || duration.m || 0,
  9860. seconds = duration.seconds || duration.second || duration.s || 0,
  9861. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  9862. // representation for dateAddRemove
  9863. this._milliseconds = milliseconds +
  9864. seconds * 1e3 + // 1000
  9865. minutes * 6e4 + // 1000 * 60
  9866. hours * 36e5; // 1000 * 60 * 60
  9867. // Because of dateAddRemove treats 24 hours as different from a
  9868. // day when working around DST, we need to store them separately
  9869. this._days = days +
  9870. weeks * 7;
  9871. // It is impossible translate months into days without knowing
  9872. // which months you are are talking about, so we have to store
  9873. // it separately.
  9874. this._months = months +
  9875. years * 12;
  9876. // The following code bubbles up values, see the tests for
  9877. // examples of what that means.
  9878. data.milliseconds = milliseconds % 1000;
  9879. seconds += absRound(milliseconds / 1000);
  9880. data.seconds = seconds % 60;
  9881. minutes += absRound(seconds / 60);
  9882. data.minutes = minutes % 60;
  9883. hours += absRound(minutes / 60);
  9884. data.hours = hours % 24;
  9885. days += absRound(hours / 24);
  9886. days += weeks * 7;
  9887. data.days = days % 30;
  9888. months += absRound(days / 30);
  9889. data.months = months % 12;
  9890. years += absRound(months / 12);
  9891. data.years = years;
  9892. }
  9893. /************************************
  9894. Helpers
  9895. ************************************/
  9896. function extend(a, b) {
  9897. for (var i in b) {
  9898. if (b.hasOwnProperty(i)) {
  9899. a[i] = b[i];
  9900. }
  9901. }
  9902. return a;
  9903. }
  9904. function absRound(number) {
  9905. if (number < 0) {
  9906. return Math.ceil(number);
  9907. } else {
  9908. return Math.floor(number);
  9909. }
  9910. }
  9911. // left zero fill a number
  9912. // see http://jsperf.com/left-zero-filling for performance comparison
  9913. function leftZeroFill(number, targetLength) {
  9914. var output = number + '';
  9915. while (output.length < targetLength) {
  9916. output = '0' + output;
  9917. }
  9918. return output;
  9919. }
  9920. // helper function for _.addTime and _.subtractTime
  9921. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  9922. var ms = duration._milliseconds,
  9923. d = duration._days,
  9924. M = duration._months,
  9925. currentDate;
  9926. if (ms) {
  9927. mom._d.setTime(+mom + ms * isAdding);
  9928. }
  9929. if (d) {
  9930. mom.date(mom.date() + d * isAdding);
  9931. }
  9932. if (M) {
  9933. currentDate = mom.date();
  9934. mom.date(1)
  9935. .month(mom.month() + M * isAdding)
  9936. .date(Math.min(currentDate, mom.daysInMonth()));
  9937. }
  9938. }
  9939. // check if is an array
  9940. function isArray(input) {
  9941. return Object.prototype.toString.call(input) === '[object Array]';
  9942. }
  9943. // compare two arrays, return the number of differences
  9944. function compareArrays(array1, array2) {
  9945. var len = Math.min(array1.length, array2.length),
  9946. lengthDiff = Math.abs(array1.length - array2.length),
  9947. diffs = 0,
  9948. i;
  9949. for (i = 0; i < len; i++) {
  9950. if (~~array1[i] !== ~~array2[i]) {
  9951. diffs++;
  9952. }
  9953. }
  9954. return diffs + lengthDiff;
  9955. }
  9956. /************************************
  9957. Languages
  9958. ************************************/
  9959. Language.prototype = {
  9960. set : function (config) {
  9961. var prop, i;
  9962. for (i in config) {
  9963. prop = config[i];
  9964. if (typeof prop === 'function') {
  9965. this[i] = prop;
  9966. } else {
  9967. this['_' + i] = prop;
  9968. }
  9969. }
  9970. },
  9971. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  9972. months : function (m) {
  9973. return this._months[m.month()];
  9974. },
  9975. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  9976. monthsShort : function (m) {
  9977. return this._monthsShort[m.month()];
  9978. },
  9979. monthsParse : function (monthName) {
  9980. var i, mom, regex, output;
  9981. if (!this._monthsParse) {
  9982. this._monthsParse = [];
  9983. }
  9984. for (i = 0; i < 12; i++) {
  9985. // make the regex if we don't have it already
  9986. if (!this._monthsParse[i]) {
  9987. mom = moment([2000, i]);
  9988. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  9989. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  9990. }
  9991. // test the regex
  9992. if (this._monthsParse[i].test(monthName)) {
  9993. return i;
  9994. }
  9995. }
  9996. },
  9997. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  9998. weekdays : function (m) {
  9999. return this._weekdays[m.day()];
  10000. },
  10001. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  10002. weekdaysShort : function (m) {
  10003. return this._weekdaysShort[m.day()];
  10004. },
  10005. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  10006. weekdaysMin : function (m) {
  10007. return this._weekdaysMin[m.day()];
  10008. },
  10009. _longDateFormat : {
  10010. LT : "h:mm A",
  10011. L : "MM/DD/YYYY",
  10012. LL : "MMMM D YYYY",
  10013. LLL : "MMMM D YYYY LT",
  10014. LLLL : "dddd, MMMM D YYYY LT"
  10015. },
  10016. longDateFormat : function (key) {
  10017. var output = this._longDateFormat[key];
  10018. if (!output && this._longDateFormat[key.toUpperCase()]) {
  10019. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  10020. return val.slice(1);
  10021. });
  10022. this._longDateFormat[key] = output;
  10023. }
  10024. return output;
  10025. },
  10026. meridiem : function (hours, minutes, isLower) {
  10027. if (hours > 11) {
  10028. return isLower ? 'pm' : 'PM';
  10029. } else {
  10030. return isLower ? 'am' : 'AM';
  10031. }
  10032. },
  10033. _calendar : {
  10034. sameDay : '[Today at] LT',
  10035. nextDay : '[Tomorrow at] LT',
  10036. nextWeek : 'dddd [at] LT',
  10037. lastDay : '[Yesterday at] LT',
  10038. lastWeek : '[last] dddd [at] LT',
  10039. sameElse : 'L'
  10040. },
  10041. calendar : function (key, mom) {
  10042. var output = this._calendar[key];
  10043. return typeof output === 'function' ? output.apply(mom) : output;
  10044. },
  10045. _relativeTime : {
  10046. future : "in %s",
  10047. past : "%s ago",
  10048. s : "a few seconds",
  10049. m : "a minute",
  10050. mm : "%d minutes",
  10051. h : "an hour",
  10052. hh : "%d hours",
  10053. d : "a day",
  10054. dd : "%d days",
  10055. M : "a month",
  10056. MM : "%d months",
  10057. y : "a year",
  10058. yy : "%d years"
  10059. },
  10060. relativeTime : function (number, withoutSuffix, string, isFuture) {
  10061. var output = this._relativeTime[string];
  10062. return (typeof output === 'function') ?
  10063. output(number, withoutSuffix, string, isFuture) :
  10064. output.replace(/%d/i, number);
  10065. },
  10066. pastFuture : function (diff, output) {
  10067. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  10068. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  10069. },
  10070. ordinal : function (number) {
  10071. return this._ordinal.replace("%d", number);
  10072. },
  10073. _ordinal : "%d",
  10074. preparse : function (string) {
  10075. return string;
  10076. },
  10077. postformat : function (string) {
  10078. return string;
  10079. },
  10080. week : function (mom) {
  10081. return weekOfYear(mom, this._week.dow, this._week.doy);
  10082. },
  10083. _week : {
  10084. dow : 0, // Sunday is the first day of the week.
  10085. doy : 6 // The week that contains Jan 1st is the first week of the year.
  10086. }
  10087. };
  10088. // Loads a language definition into the `languages` cache. The function
  10089. // takes a key and optionally values. If not in the browser and no values
  10090. // are provided, it will load the language file module. As a convenience,
  10091. // this function also returns the language values.
  10092. function loadLang(key, values) {
  10093. values.abbr = key;
  10094. if (!languages[key]) {
  10095. languages[key] = new Language();
  10096. }
  10097. languages[key].set(values);
  10098. return languages[key];
  10099. }
  10100. // Determines which language definition to use and returns it.
  10101. //
  10102. // With no parameters, it will return the global language. If you
  10103. // pass in a language key, such as 'en', it will return the
  10104. // definition for 'en', so long as 'en' has already been loaded using
  10105. // moment.lang.
  10106. function getLangDefinition(key) {
  10107. if (!key) {
  10108. return moment.fn._lang;
  10109. }
  10110. if (!languages[key] && hasModule) {
  10111. require('./lang/' + key);
  10112. }
  10113. return languages[key];
  10114. }
  10115. /************************************
  10116. Formatting
  10117. ************************************/
  10118. function removeFormattingTokens(input) {
  10119. if (input.match(/\[.*\]/)) {
  10120. return input.replace(/^\[|\]$/g, "");
  10121. }
  10122. return input.replace(/\\/g, "");
  10123. }
  10124. function makeFormatFunction(format) {
  10125. var array = format.match(formattingTokens), i, length;
  10126. for (i = 0, length = array.length; i < length; i++) {
  10127. if (formatTokenFunctions[array[i]]) {
  10128. array[i] = formatTokenFunctions[array[i]];
  10129. } else {
  10130. array[i] = removeFormattingTokens(array[i]);
  10131. }
  10132. }
  10133. return function (mom) {
  10134. var output = "";
  10135. for (i = 0; i < length; i++) {
  10136. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  10137. }
  10138. return output;
  10139. };
  10140. }
  10141. // format date using native date object
  10142. function formatMoment(m, format) {
  10143. var i = 5;
  10144. function replaceLongDateFormatTokens(input) {
  10145. return m.lang().longDateFormat(input) || input;
  10146. }
  10147. while (i-- && localFormattingTokens.test(format)) {
  10148. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  10149. }
  10150. if (!formatFunctions[format]) {
  10151. formatFunctions[format] = makeFormatFunction(format);
  10152. }
  10153. return formatFunctions[format](m);
  10154. }
  10155. /************************************
  10156. Parsing
  10157. ************************************/
  10158. // get the regex to find the next token
  10159. function getParseRegexForToken(token) {
  10160. switch (token) {
  10161. case 'DDDD':
  10162. return parseTokenThreeDigits;
  10163. case 'YYYY':
  10164. return parseTokenFourDigits;
  10165. case 'YYYYY':
  10166. return parseTokenSixDigits;
  10167. case 'S':
  10168. case 'SS':
  10169. case 'SSS':
  10170. case 'DDD':
  10171. return parseTokenOneToThreeDigits;
  10172. case 'MMM':
  10173. case 'MMMM':
  10174. case 'dd':
  10175. case 'ddd':
  10176. case 'dddd':
  10177. case 'a':
  10178. case 'A':
  10179. return parseTokenWord;
  10180. case 'X':
  10181. return parseTokenTimestampMs;
  10182. case 'Z':
  10183. case 'ZZ':
  10184. return parseTokenTimezone;
  10185. case 'T':
  10186. return parseTokenT;
  10187. case 'MM':
  10188. case 'DD':
  10189. case 'YY':
  10190. case 'HH':
  10191. case 'hh':
  10192. case 'mm':
  10193. case 'ss':
  10194. case 'M':
  10195. case 'D':
  10196. case 'd':
  10197. case 'H':
  10198. case 'h':
  10199. case 'm':
  10200. case 's':
  10201. return parseTokenOneOrTwoDigits;
  10202. default :
  10203. return new RegExp(token.replace('\\', ''));
  10204. }
  10205. }
  10206. // function to convert string input to date
  10207. function addTimeToArrayFromToken(token, input, config) {
  10208. var a, b,
  10209. datePartArray = config._a;
  10210. switch (token) {
  10211. // MONTH
  10212. case 'M' : // fall through to MM
  10213. case 'MM' :
  10214. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  10215. break;
  10216. case 'MMM' : // fall through to MMMM
  10217. case 'MMMM' :
  10218. a = getLangDefinition(config._l).monthsParse(input);
  10219. // if we didn't find a month name, mark the date as invalid.
  10220. if (a != null) {
  10221. datePartArray[1] = a;
  10222. } else {
  10223. config._isValid = false;
  10224. }
  10225. break;
  10226. // DAY OF MONTH
  10227. case 'D' : // fall through to DDDD
  10228. case 'DD' : // fall through to DDDD
  10229. case 'DDD' : // fall through to DDDD
  10230. case 'DDDD' :
  10231. if (input != null) {
  10232. datePartArray[2] = ~~input;
  10233. }
  10234. break;
  10235. // YEAR
  10236. case 'YY' :
  10237. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  10238. break;
  10239. case 'YYYY' :
  10240. case 'YYYYY' :
  10241. datePartArray[0] = ~~input;
  10242. break;
  10243. // AM / PM
  10244. case 'a' : // fall through to A
  10245. case 'A' :
  10246. config._isPm = ((input + '').toLowerCase() === 'pm');
  10247. break;
  10248. // 24 HOUR
  10249. case 'H' : // fall through to hh
  10250. case 'HH' : // fall through to hh
  10251. case 'h' : // fall through to hh
  10252. case 'hh' :
  10253. datePartArray[3] = ~~input;
  10254. break;
  10255. // MINUTE
  10256. case 'm' : // fall through to mm
  10257. case 'mm' :
  10258. datePartArray[4] = ~~input;
  10259. break;
  10260. // SECOND
  10261. case 's' : // fall through to ss
  10262. case 'ss' :
  10263. datePartArray[5] = ~~input;
  10264. break;
  10265. // MILLISECOND
  10266. case 'S' :
  10267. case 'SS' :
  10268. case 'SSS' :
  10269. datePartArray[6] = ~~ (('0.' + input) * 1000);
  10270. break;
  10271. // UNIX TIMESTAMP WITH MS
  10272. case 'X':
  10273. config._d = new Date(parseFloat(input) * 1000);
  10274. break;
  10275. // TIMEZONE
  10276. case 'Z' : // fall through to ZZ
  10277. case 'ZZ' :
  10278. config._useUTC = true;
  10279. a = (input + '').match(parseTimezoneChunker);
  10280. if (a && a[1]) {
  10281. config._tzh = ~~a[1];
  10282. }
  10283. if (a && a[2]) {
  10284. config._tzm = ~~a[2];
  10285. }
  10286. // reverse offsets
  10287. if (a && a[0] === '+') {
  10288. config._tzh = -config._tzh;
  10289. config._tzm = -config._tzm;
  10290. }
  10291. break;
  10292. }
  10293. // if the input is null, the date is not valid
  10294. if (input == null) {
  10295. config._isValid = false;
  10296. }
  10297. }
  10298. // convert an array to a date.
  10299. // the array should mirror the parameters below
  10300. // note: all values past the year are optional and will default to the lowest possible value.
  10301. // [year, month, day , hour, minute, second, millisecond]
  10302. function dateFromArray(config) {
  10303. var i, date, input = [];
  10304. if (config._d) {
  10305. return;
  10306. }
  10307. for (i = 0; i < 7; i++) {
  10308. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  10309. }
  10310. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  10311. input[3] += config._tzh || 0;
  10312. input[4] += config._tzm || 0;
  10313. date = new Date(0);
  10314. if (config._useUTC) {
  10315. date.setUTCFullYear(input[0], input[1], input[2]);
  10316. date.setUTCHours(input[3], input[4], input[5], input[6]);
  10317. } else {
  10318. date.setFullYear(input[0], input[1], input[2]);
  10319. date.setHours(input[3], input[4], input[5], input[6]);
  10320. }
  10321. config._d = date;
  10322. }
  10323. // date from string and format string
  10324. function makeDateFromStringAndFormat(config) {
  10325. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  10326. var tokens = config._f.match(formattingTokens),
  10327. string = config._i,
  10328. i, parsedInput;
  10329. config._a = [];
  10330. for (i = 0; i < tokens.length; i++) {
  10331. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  10332. if (parsedInput) {
  10333. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  10334. }
  10335. // don't parse if its not a known token
  10336. if (formatTokenFunctions[tokens[i]]) {
  10337. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  10338. }
  10339. }
  10340. // handle am pm
  10341. if (config._isPm && config._a[3] < 12) {
  10342. config._a[3] += 12;
  10343. }
  10344. // if is 12 am, change hours to 0
  10345. if (config._isPm === false && config._a[3] === 12) {
  10346. config._a[3] = 0;
  10347. }
  10348. // return
  10349. dateFromArray(config);
  10350. }
  10351. // date from string and array of format strings
  10352. function makeDateFromStringAndArray(config) {
  10353. var tempConfig,
  10354. tempMoment,
  10355. bestMoment,
  10356. scoreToBeat = 99,
  10357. i,
  10358. currentDate,
  10359. currentScore;
  10360. while (config._f.length) {
  10361. tempConfig = extend({}, config);
  10362. tempConfig._f = config._f.pop();
  10363. makeDateFromStringAndFormat(tempConfig);
  10364. tempMoment = new Moment(tempConfig);
  10365. if (tempMoment.isValid()) {
  10366. bestMoment = tempMoment;
  10367. break;
  10368. }
  10369. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  10370. if (currentScore < scoreToBeat) {
  10371. scoreToBeat = currentScore;
  10372. bestMoment = tempMoment;
  10373. }
  10374. }
  10375. extend(config, bestMoment);
  10376. }
  10377. // date from iso format
  10378. function makeDateFromString(config) {
  10379. var i,
  10380. string = config._i;
  10381. if (isoRegex.exec(string)) {
  10382. config._f = 'YYYY-MM-DDT';
  10383. for (i = 0; i < 4; i++) {
  10384. if (isoTimes[i][1].exec(string)) {
  10385. config._f += isoTimes[i][0];
  10386. break;
  10387. }
  10388. }
  10389. if (parseTokenTimezone.exec(string)) {
  10390. config._f += " Z";
  10391. }
  10392. makeDateFromStringAndFormat(config);
  10393. } else {
  10394. config._d = new Date(string);
  10395. }
  10396. }
  10397. function makeDateFromInput(config) {
  10398. var input = config._i,
  10399. matched = aspNetJsonRegex.exec(input);
  10400. if (input === undefined) {
  10401. config._d = new Date();
  10402. } else if (matched) {
  10403. config._d = new Date(+matched[1]);
  10404. } else if (typeof input === 'string') {
  10405. makeDateFromString(config);
  10406. } else if (isArray(input)) {
  10407. config._a = input.slice(0);
  10408. dateFromArray(config);
  10409. } else {
  10410. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  10411. }
  10412. }
  10413. /************************************
  10414. Relative Time
  10415. ************************************/
  10416. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  10417. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  10418. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  10419. }
  10420. function relativeTime(milliseconds, withoutSuffix, lang) {
  10421. var seconds = round(Math.abs(milliseconds) / 1000),
  10422. minutes = round(seconds / 60),
  10423. hours = round(minutes / 60),
  10424. days = round(hours / 24),
  10425. years = round(days / 365),
  10426. args = seconds < 45 && ['s', seconds] ||
  10427. minutes === 1 && ['m'] ||
  10428. minutes < 45 && ['mm', minutes] ||
  10429. hours === 1 && ['h'] ||
  10430. hours < 22 && ['hh', hours] ||
  10431. days === 1 && ['d'] ||
  10432. days <= 25 && ['dd', days] ||
  10433. days <= 45 && ['M'] ||
  10434. days < 345 && ['MM', round(days / 30)] ||
  10435. years === 1 && ['y'] || ['yy', years];
  10436. args[2] = withoutSuffix;
  10437. args[3] = milliseconds > 0;
  10438. args[4] = lang;
  10439. return substituteTimeAgo.apply({}, args);
  10440. }
  10441. /************************************
  10442. Week of Year
  10443. ************************************/
  10444. // firstDayOfWeek 0 = sun, 6 = sat
  10445. // the day of the week that starts the week
  10446. // (usually sunday or monday)
  10447. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  10448. // the first week is the week that contains the first
  10449. // of this day of the week
  10450. // (eg. ISO weeks use thursday (4))
  10451. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  10452. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  10453. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  10454. if (daysToDayOfWeek > end) {
  10455. daysToDayOfWeek -= 7;
  10456. }
  10457. if (daysToDayOfWeek < end - 7) {
  10458. daysToDayOfWeek += 7;
  10459. }
  10460. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  10461. }
  10462. /************************************
  10463. Top Level Functions
  10464. ************************************/
  10465. function makeMoment(config) {
  10466. var input = config._i,
  10467. format = config._f;
  10468. if (input === null || input === '') {
  10469. return null;
  10470. }
  10471. if (typeof input === 'string') {
  10472. config._i = input = getLangDefinition().preparse(input);
  10473. }
  10474. if (moment.isMoment(input)) {
  10475. config = extend({}, input);
  10476. config._d = new Date(+input._d);
  10477. } else if (format) {
  10478. if (isArray(format)) {
  10479. makeDateFromStringAndArray(config);
  10480. } else {
  10481. makeDateFromStringAndFormat(config);
  10482. }
  10483. } else {
  10484. makeDateFromInput(config);
  10485. }
  10486. return new Moment(config);
  10487. }
  10488. moment = function (input, format, lang) {
  10489. return makeMoment({
  10490. _i : input,
  10491. _f : format,
  10492. _l : lang,
  10493. _isUTC : false
  10494. });
  10495. };
  10496. // creating with utc
  10497. moment.utc = function (input, format, lang) {
  10498. return makeMoment({
  10499. _useUTC : true,
  10500. _isUTC : true,
  10501. _l : lang,
  10502. _i : input,
  10503. _f : format
  10504. });
  10505. };
  10506. // creating with unix timestamp (in seconds)
  10507. moment.unix = function (input) {
  10508. return moment(input * 1000);
  10509. };
  10510. // duration
  10511. moment.duration = function (input, key) {
  10512. var isDuration = moment.isDuration(input),
  10513. isNumber = (typeof input === 'number'),
  10514. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  10515. ret;
  10516. if (isNumber) {
  10517. if (key) {
  10518. duration[key] = input;
  10519. } else {
  10520. duration.milliseconds = input;
  10521. }
  10522. }
  10523. ret = new Duration(duration);
  10524. if (isDuration && input.hasOwnProperty('_lang')) {
  10525. ret._lang = input._lang;
  10526. }
  10527. return ret;
  10528. };
  10529. // version number
  10530. moment.version = VERSION;
  10531. // default format
  10532. moment.defaultFormat = isoFormat;
  10533. // This function will load languages and then set the global language. If
  10534. // no arguments are passed in, it will simply return the current global
  10535. // language key.
  10536. moment.lang = function (key, values) {
  10537. var i;
  10538. if (!key) {
  10539. return moment.fn._lang._abbr;
  10540. }
  10541. if (values) {
  10542. loadLang(key, values);
  10543. } else if (!languages[key]) {
  10544. getLangDefinition(key);
  10545. }
  10546. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  10547. };
  10548. // returns language data
  10549. moment.langData = function (key) {
  10550. if (key && key._lang && key._lang._abbr) {
  10551. key = key._lang._abbr;
  10552. }
  10553. return getLangDefinition(key);
  10554. };
  10555. // compare moment object
  10556. moment.isMoment = function (obj) {
  10557. return obj instanceof Moment;
  10558. };
  10559. // for typechecking Duration objects
  10560. moment.isDuration = function (obj) {
  10561. return obj instanceof Duration;
  10562. };
  10563. /************************************
  10564. Moment Prototype
  10565. ************************************/
  10566. moment.fn = Moment.prototype = {
  10567. clone : function () {
  10568. return moment(this);
  10569. },
  10570. valueOf : function () {
  10571. return +this._d;
  10572. },
  10573. unix : function () {
  10574. return Math.floor(+this._d / 1000);
  10575. },
  10576. toString : function () {
  10577. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  10578. },
  10579. toDate : function () {
  10580. return this._d;
  10581. },
  10582. toJSON : function () {
  10583. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  10584. },
  10585. toArray : function () {
  10586. var m = this;
  10587. return [
  10588. m.year(),
  10589. m.month(),
  10590. m.date(),
  10591. m.hours(),
  10592. m.minutes(),
  10593. m.seconds(),
  10594. m.milliseconds()
  10595. ];
  10596. },
  10597. isValid : function () {
  10598. if (this._isValid == null) {
  10599. if (this._a) {
  10600. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  10601. } else {
  10602. this._isValid = !isNaN(this._d.getTime());
  10603. }
  10604. }
  10605. return !!this._isValid;
  10606. },
  10607. utc : function () {
  10608. this._isUTC = true;
  10609. return this;
  10610. },
  10611. local : function () {
  10612. this._isUTC = false;
  10613. return this;
  10614. },
  10615. format : function (inputString) {
  10616. var output = formatMoment(this, inputString || moment.defaultFormat);
  10617. return this.lang().postformat(output);
  10618. },
  10619. add : function (input, val) {
  10620. var dur;
  10621. // switch args to support add('s', 1) and add(1, 's')
  10622. if (typeof input === 'string') {
  10623. dur = moment.duration(+val, input);
  10624. } else {
  10625. dur = moment.duration(input, val);
  10626. }
  10627. addOrSubtractDurationFromMoment(this, dur, 1);
  10628. return this;
  10629. },
  10630. subtract : function (input, val) {
  10631. var dur;
  10632. // switch args to support subtract('s', 1) and subtract(1, 's')
  10633. if (typeof input === 'string') {
  10634. dur = moment.duration(+val, input);
  10635. } else {
  10636. dur = moment.duration(input, val);
  10637. }
  10638. addOrSubtractDurationFromMoment(this, dur, -1);
  10639. return this;
  10640. },
  10641. diff : function (input, units, asFloat) {
  10642. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  10643. zoneDiff = (this.zone() - that.zone()) * 6e4,
  10644. diff, output;
  10645. if (units) {
  10646. // standardize on singular form
  10647. units = units.replace(/s$/, '');
  10648. }
  10649. if (units === 'year' || units === 'month') {
  10650. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  10651. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  10652. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  10653. if (units === 'year') {
  10654. output = output / 12;
  10655. }
  10656. } else {
  10657. diff = (this - that) - zoneDiff;
  10658. output = units === 'second' ? diff / 1e3 : // 1000
  10659. units === 'minute' ? diff / 6e4 : // 1000 * 60
  10660. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  10661. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  10662. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  10663. diff;
  10664. }
  10665. return asFloat ? output : absRound(output);
  10666. },
  10667. from : function (time, withoutSuffix) {
  10668. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  10669. },
  10670. fromNow : function (withoutSuffix) {
  10671. return this.from(moment(), withoutSuffix);
  10672. },
  10673. calendar : function () {
  10674. var diff = this.diff(moment().startOf('day'), 'days', true),
  10675. format = diff < -6 ? 'sameElse' :
  10676. diff < -1 ? 'lastWeek' :
  10677. diff < 0 ? 'lastDay' :
  10678. diff < 1 ? 'sameDay' :
  10679. diff < 2 ? 'nextDay' :
  10680. diff < 7 ? 'nextWeek' : 'sameElse';
  10681. return this.format(this.lang().calendar(format, this));
  10682. },
  10683. isLeapYear : function () {
  10684. var year = this.year();
  10685. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  10686. },
  10687. isDST : function () {
  10688. return (this.zone() < moment([this.year()]).zone() ||
  10689. this.zone() < moment([this.year(), 5]).zone());
  10690. },
  10691. day : function (input) {
  10692. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  10693. return input == null ? day :
  10694. this.add({ d : input - day });
  10695. },
  10696. startOf: function (units) {
  10697. units = units.replace(/s$/, '');
  10698. // the following switch intentionally omits break keywords
  10699. // to utilize falling through the cases.
  10700. switch (units) {
  10701. case 'year':
  10702. this.month(0);
  10703. /* falls through */
  10704. case 'month':
  10705. this.date(1);
  10706. /* falls through */
  10707. case 'week':
  10708. case 'day':
  10709. this.hours(0);
  10710. /* falls through */
  10711. case 'hour':
  10712. this.minutes(0);
  10713. /* falls through */
  10714. case 'minute':
  10715. this.seconds(0);
  10716. /* falls through */
  10717. case 'second':
  10718. this.milliseconds(0);
  10719. /* falls through */
  10720. }
  10721. // weeks are a special case
  10722. if (units === 'week') {
  10723. this.day(0);
  10724. }
  10725. return this;
  10726. },
  10727. endOf: function (units) {
  10728. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  10729. },
  10730. isAfter: function (input, units) {
  10731. units = typeof units !== 'undefined' ? units : 'millisecond';
  10732. return +this.clone().startOf(units) > +moment(input).startOf(units);
  10733. },
  10734. isBefore: function (input, units) {
  10735. units = typeof units !== 'undefined' ? units : 'millisecond';
  10736. return +this.clone().startOf(units) < +moment(input).startOf(units);
  10737. },
  10738. isSame: function (input, units) {
  10739. units = typeof units !== 'undefined' ? units : 'millisecond';
  10740. return +this.clone().startOf(units) === +moment(input).startOf(units);
  10741. },
  10742. zone : function () {
  10743. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  10744. },
  10745. daysInMonth : function () {
  10746. return moment.utc([this.year(), this.month() + 1, 0]).date();
  10747. },
  10748. dayOfYear : function (input) {
  10749. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  10750. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  10751. },
  10752. isoWeek : function (input) {
  10753. var week = weekOfYear(this, 1, 4);
  10754. return input == null ? week : this.add("d", (input - week) * 7);
  10755. },
  10756. week : function (input) {
  10757. var week = this.lang().week(this);
  10758. return input == null ? week : this.add("d", (input - week) * 7);
  10759. },
  10760. // If passed a language key, it will set the language for this
  10761. // instance. Otherwise, it will return the language configuration
  10762. // variables for this instance.
  10763. lang : function (key) {
  10764. if (key === undefined) {
  10765. return this._lang;
  10766. } else {
  10767. this._lang = getLangDefinition(key);
  10768. return this;
  10769. }
  10770. }
  10771. };
  10772. // helper for adding shortcuts
  10773. function makeGetterAndSetter(name, key) {
  10774. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  10775. var utc = this._isUTC ? 'UTC' : '';
  10776. if (input != null) {
  10777. this._d['set' + utc + key](input);
  10778. return this;
  10779. } else {
  10780. return this._d['get' + utc + key]();
  10781. }
  10782. };
  10783. }
  10784. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  10785. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  10786. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  10787. }
  10788. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  10789. makeGetterAndSetter('year', 'FullYear');
  10790. // add plural methods
  10791. moment.fn.days = moment.fn.day;
  10792. moment.fn.weeks = moment.fn.week;
  10793. moment.fn.isoWeeks = moment.fn.isoWeek;
  10794. /************************************
  10795. Duration Prototype
  10796. ************************************/
  10797. moment.duration.fn = Duration.prototype = {
  10798. weeks : function () {
  10799. return absRound(this.days() / 7);
  10800. },
  10801. valueOf : function () {
  10802. return this._milliseconds +
  10803. this._days * 864e5 +
  10804. this._months * 2592e6;
  10805. },
  10806. humanize : function (withSuffix) {
  10807. var difference = +this,
  10808. output = relativeTime(difference, !withSuffix, this.lang());
  10809. if (withSuffix) {
  10810. output = this.lang().pastFuture(difference, output);
  10811. }
  10812. return this.lang().postformat(output);
  10813. },
  10814. lang : moment.fn.lang
  10815. };
  10816. function makeDurationGetter(name) {
  10817. moment.duration.fn[name] = function () {
  10818. return this._data[name];
  10819. };
  10820. }
  10821. function makeDurationAsGetter(name, factor) {
  10822. moment.duration.fn['as' + name] = function () {
  10823. return +this / factor;
  10824. };
  10825. }
  10826. for (i in unitMillisecondFactors) {
  10827. if (unitMillisecondFactors.hasOwnProperty(i)) {
  10828. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  10829. makeDurationGetter(i.toLowerCase());
  10830. }
  10831. }
  10832. makeDurationAsGetter('Weeks', 6048e5);
  10833. /************************************
  10834. Default Lang
  10835. ************************************/
  10836. // Set default language, other languages will inherit from English.
  10837. moment.lang('en', {
  10838. ordinal : function (number) {
  10839. var b = number % 10,
  10840. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  10841. (b === 1) ? 'st' :
  10842. (b === 2) ? 'nd' :
  10843. (b === 3) ? 'rd' : 'th';
  10844. return number + output;
  10845. }
  10846. });
  10847. /************************************
  10848. Exposing Moment
  10849. ************************************/
  10850. // CommonJS module is defined
  10851. if (hasModule) {
  10852. module.exports = moment;
  10853. }
  10854. /*global ender:false */
  10855. if (typeof ender === 'undefined') {
  10856. // here, `this` means `window` in the browser, or `global` on the server
  10857. // add `moment` as a global object via a string identifier,
  10858. // for Closure Compiler "advanced" mode
  10859. this['moment'] = moment;
  10860. }
  10861. /*global define:false */
  10862. if (typeof define === "function" && define.amd) {
  10863. define("moment", [], function () {
  10864. return moment;
  10865. });
  10866. }
  10867. }).call(this);
  10868. })()
  10869. },{}]},{},[1])(1)
  10870. });
  10871. ;