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.

12868 lines
385 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.2.0-SNAPSHOT
  8. * @date 2013-09-02
  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. * Convert 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.convert = function convert(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.valueOf());
  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. match = ASPDateRegex.exec(object);
  166. if (match) {
  167. // object is an ASP date
  168. return new Date(Number(match[1])); // parse number
  169. }
  170. else {
  171. return moment(object).toDate(); // parse string
  172. }
  173. }
  174. else {
  175. throw new Error(
  176. 'Cannot convert object of type ' + util.getType(object) +
  177. ' to type Date');
  178. }
  179. case 'Moment':
  180. if (util.isNumber(object)) {
  181. return moment(object);
  182. }
  183. if (object instanceof Date) {
  184. return moment(object.valueOf());
  185. }
  186. else if (moment.isMoment(object)) {
  187. return moment.clone();
  188. }
  189. if (util.isString(object)) {
  190. match = ASPDateRegex.exec(object);
  191. if (match) {
  192. // object is an ASP date
  193. return moment(Number(match[1])); // parse number
  194. }
  195. else {
  196. return moment(object); // parse string
  197. }
  198. }
  199. else {
  200. throw new Error(
  201. 'Cannot convert object of type ' + util.getType(object) +
  202. ' to type Date');
  203. }
  204. case 'ISODate':
  205. if (util.isNumber(object)) {
  206. return new Date(object);
  207. }
  208. else if (object instanceof Date) {
  209. return object.toISOString();
  210. }
  211. else if (moment.isMoment(object)) {
  212. return object.toDate().toISOString();
  213. }
  214. else if (util.isString(object)) {
  215. match = ASPDateRegex.exec(object);
  216. if (match) {
  217. // object is an ASP date
  218. return new Date(Number(match[1])).toISOString(); // parse number
  219. }
  220. else {
  221. return new Date(object).toISOString(); // parse string
  222. }
  223. }
  224. else {
  225. throw new Error(
  226. 'Cannot convert object of type ' + util.getType(object) +
  227. ' to type ISODate');
  228. }
  229. case 'ASPDate':
  230. if (util.isNumber(object)) {
  231. return '/Date(' + object + ')/';
  232. }
  233. else if (object instanceof Date) {
  234. return '/Date(' + object.valueOf() + ')/';
  235. }
  236. else if (util.isString(object)) {
  237. match = ASPDateRegex.exec(object);
  238. var value;
  239. if (match) {
  240. // object is an ASP date
  241. value = new Date(Number(match[1])).valueOf(); // parse number
  242. }
  243. else {
  244. value = new Date(object).valueOf(); // parse string
  245. }
  246. return '/Date(' + value + ')/';
  247. }
  248. else {
  249. throw new Error(
  250. 'Cannot convert object of type ' + util.getType(object) +
  251. ' to type ASPDate');
  252. }
  253. default:
  254. throw new Error('Cannot convert object of type ' + util.getType(object) +
  255. ' to type "' + type + '"');
  256. }
  257. };
  258. // parse ASP.Net Date pattern,
  259. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  260. // code from http://momentjs.com/
  261. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  262. /**
  263. * Get the type of an object, for example util.getType([]) returns 'Array'
  264. * @param {*} object
  265. * @return {String} type
  266. */
  267. util.getType = function getType(object) {
  268. var type = typeof object;
  269. if (type == 'object') {
  270. if (object == null) {
  271. return 'null';
  272. }
  273. if (object instanceof Boolean) {
  274. return 'Boolean';
  275. }
  276. if (object instanceof Number) {
  277. return 'Number';
  278. }
  279. if (object instanceof String) {
  280. return 'String';
  281. }
  282. if (object instanceof Array) {
  283. return 'Array';
  284. }
  285. if (object instanceof Date) {
  286. return 'Date';
  287. }
  288. return 'Object';
  289. }
  290. else if (type == 'number') {
  291. return 'Number';
  292. }
  293. else if (type == 'boolean') {
  294. return 'Boolean';
  295. }
  296. else if (type == 'string') {
  297. return 'String';
  298. }
  299. return type;
  300. };
  301. /**
  302. * Retrieve the absolute left value of a DOM element
  303. * @param {Element} elem A dom element, for example a div
  304. * @return {number} left The absolute left position of this element
  305. * in the browser page.
  306. */
  307. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  308. var doc = document.documentElement;
  309. var body = document.body;
  310. var left = elem.offsetLeft;
  311. var e = elem.offsetParent;
  312. while (e != null && e != body && e != doc) {
  313. left += e.offsetLeft;
  314. left -= e.scrollLeft;
  315. e = e.offsetParent;
  316. }
  317. return left;
  318. };
  319. /**
  320. * Retrieve the absolute top value of a DOM element
  321. * @param {Element} elem A dom element, for example a div
  322. * @return {number} top The absolute top position of this element
  323. * in the browser page.
  324. */
  325. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  326. var doc = document.documentElement;
  327. var body = document.body;
  328. var top = elem.offsetTop;
  329. var e = elem.offsetParent;
  330. while (e != null && e != body && e != doc) {
  331. top += e.offsetTop;
  332. top -= e.scrollTop;
  333. e = e.offsetParent;
  334. }
  335. return top;
  336. };
  337. /**
  338. * Get the absolute, vertical mouse position from an event.
  339. * @param {Event} event
  340. * @return {Number} pageY
  341. */
  342. util.getPageY = function getPageY (event) {
  343. if ('pageY' in event) {
  344. return event.pageY;
  345. }
  346. else {
  347. var clientY;
  348. if (('targetTouches' in event) && event.targetTouches.length) {
  349. clientY = event.targetTouches[0].clientY;
  350. }
  351. else {
  352. clientY = event.clientY;
  353. }
  354. var doc = document.documentElement;
  355. var body = document.body;
  356. return clientY +
  357. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  358. ( doc && doc.clientTop || body && body.clientTop || 0 );
  359. }
  360. };
  361. /**
  362. * Get the absolute, horizontal mouse position from an event.
  363. * @param {Event} event
  364. * @return {Number} pageX
  365. */
  366. util.getPageX = function getPageX (event) {
  367. if ('pageY' in event) {
  368. return event.pageX;
  369. }
  370. else {
  371. var clientX;
  372. if (('targetTouches' in event) && event.targetTouches.length) {
  373. clientX = event.targetTouches[0].clientX;
  374. }
  375. else {
  376. clientX = event.clientX;
  377. }
  378. var doc = document.documentElement;
  379. var body = document.body;
  380. return clientX +
  381. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  382. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  383. }
  384. };
  385. /**
  386. * add a className to the given elements style
  387. * @param {Element} elem
  388. * @param {String} className
  389. */
  390. util.addClassName = function addClassName(elem, className) {
  391. var classes = elem.className.split(' ');
  392. if (classes.indexOf(className) == -1) {
  393. classes.push(className); // add the class to the array
  394. elem.className = classes.join(' ');
  395. }
  396. };
  397. /**
  398. * add a className to the given elements style
  399. * @param {Element} elem
  400. * @param {String} className
  401. */
  402. util.removeClassName = function removeClassname(elem, className) {
  403. var classes = elem.className.split(' ');
  404. var index = classes.indexOf(className);
  405. if (index != -1) {
  406. classes.splice(index, 1); // remove the class from the array
  407. elem.className = classes.join(' ');
  408. }
  409. };
  410. /**
  411. * For each method for both arrays and objects.
  412. * In case of an array, the built-in Array.forEach() is applied.
  413. * In case of an Object, the method loops over all properties of the object.
  414. * @param {Object | Array} object An Object or Array
  415. * @param {function} callback Callback method, called for each item in
  416. * the object or array with three parameters:
  417. * callback(value, index, object)
  418. */
  419. util.forEach = function forEach (object, callback) {
  420. var i,
  421. len;
  422. if (object instanceof Array) {
  423. // array
  424. for (i = 0, len = object.length; i < len; i++) {
  425. callback(object[i], i, object);
  426. }
  427. }
  428. else {
  429. // object
  430. for (i in object) {
  431. if (object.hasOwnProperty(i)) {
  432. callback(object[i], i, object);
  433. }
  434. }
  435. }
  436. };
  437. /**
  438. * Update a property in an object
  439. * @param {Object} object
  440. * @param {String} key
  441. * @param {*} value
  442. * @return {Boolean} changed
  443. */
  444. util.updateProperty = function updateProp (object, key, value) {
  445. if (object[key] !== value) {
  446. object[key] = value;
  447. return true;
  448. }
  449. else {
  450. return false;
  451. }
  452. };
  453. /**
  454. * Add and event listener. Works for all browsers
  455. * @param {Element} element An html element
  456. * @param {string} action The action, for example "click",
  457. * without the prefix "on"
  458. * @param {function} listener The callback function to be executed
  459. * @param {boolean} [useCapture]
  460. */
  461. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  462. if (element.addEventListener) {
  463. if (useCapture === undefined)
  464. useCapture = false;
  465. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  466. action = "DOMMouseScroll"; // For Firefox
  467. }
  468. element.addEventListener(action, listener, useCapture);
  469. } else {
  470. element.attachEvent("on" + action, listener); // IE browsers
  471. }
  472. };
  473. /**
  474. * Remove an event listener from an element
  475. * @param {Element} element An html dom element
  476. * @param {string} action The name of the event, for example "mousedown"
  477. * @param {function} listener The listener function
  478. * @param {boolean} [useCapture]
  479. */
  480. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  481. if (element.removeEventListener) {
  482. // non-IE browsers
  483. if (useCapture === undefined)
  484. useCapture = false;
  485. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  486. action = "DOMMouseScroll"; // For Firefox
  487. }
  488. element.removeEventListener(action, listener, useCapture);
  489. } else {
  490. // IE browsers
  491. element.detachEvent("on" + action, listener);
  492. }
  493. };
  494. /**
  495. * Get HTML element which is the target of the event
  496. * @param {Event} event
  497. * @return {Element} target element
  498. */
  499. util.getTarget = function getTarget(event) {
  500. // code from http://www.quirksmode.org/js/events_properties.html
  501. if (!event) {
  502. event = window.event;
  503. }
  504. var target;
  505. if (event.target) {
  506. target = event.target;
  507. }
  508. else if (event.srcElement) {
  509. target = event.srcElement;
  510. }
  511. if (target.nodeType != undefined && target.nodeType == 3) {
  512. // defeat Safari bug
  513. target = target.parentNode;
  514. }
  515. return target;
  516. };
  517. /**
  518. * Stop event propagation
  519. */
  520. util.stopPropagation = function stopPropagation(event) {
  521. if (!event)
  522. event = window.event;
  523. if (event.stopPropagation) {
  524. event.stopPropagation(); // non-IE browsers
  525. }
  526. else {
  527. event.cancelBubble = true; // IE browsers
  528. }
  529. };
  530. /**
  531. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  532. */
  533. util.preventDefault = function preventDefault (event) {
  534. if (!event)
  535. event = window.event;
  536. if (event.preventDefault) {
  537. event.preventDefault(); // non-IE browsers
  538. }
  539. else {
  540. event.returnValue = false; // IE browsers
  541. }
  542. };
  543. util.option = {};
  544. /**
  545. * Convert a value into a boolean
  546. * @param {Boolean | function | undefined} value
  547. * @param {Boolean} [defaultValue]
  548. * @returns {Boolean} bool
  549. */
  550. util.option.asBoolean = function (value, defaultValue) {
  551. if (typeof value == 'function') {
  552. value = value();
  553. }
  554. if (value != null) {
  555. return (value != false);
  556. }
  557. return defaultValue || null;
  558. };
  559. /**
  560. * Convert a value into a number
  561. * @param {Boolean | function | undefined} value
  562. * @param {Number} [defaultValue]
  563. * @returns {Number} number
  564. */
  565. util.option.asNumber = function (value, defaultValue) {
  566. if (typeof value == 'function') {
  567. value = value();
  568. }
  569. if (value != null) {
  570. return Number(value) || defaultValue || null;
  571. }
  572. return defaultValue || null;
  573. };
  574. /**
  575. * Convert a value into a string
  576. * @param {String | function | undefined} value
  577. * @param {String} [defaultValue]
  578. * @returns {String} str
  579. */
  580. util.option.asString = function (value, defaultValue) {
  581. if (typeof value == 'function') {
  582. value = value();
  583. }
  584. if (value != null) {
  585. return String(value);
  586. }
  587. return defaultValue || null;
  588. };
  589. /**
  590. * Convert a size or location into a string with pixels or a percentage
  591. * @param {String | Number | function | undefined} value
  592. * @param {String} [defaultValue]
  593. * @returns {String} size
  594. */
  595. util.option.asSize = function (value, defaultValue) {
  596. if (typeof value == 'function') {
  597. value = value();
  598. }
  599. if (util.isString(value)) {
  600. return value;
  601. }
  602. else if (util.isNumber(value)) {
  603. return value + 'px';
  604. }
  605. else {
  606. return defaultValue || null;
  607. }
  608. };
  609. /**
  610. * Convert a value into a DOM element
  611. * @param {HTMLElement | function | undefined} value
  612. * @param {HTMLElement} [defaultValue]
  613. * @returns {HTMLElement | null} dom
  614. */
  615. util.option.asElement = function (value, defaultValue) {
  616. if (typeof value == 'function') {
  617. value = value();
  618. }
  619. return value || defaultValue || null;
  620. };
  621. /**
  622. * load css from text
  623. * @param {String} css Text containing css
  624. */
  625. util.loadCss = function (css) {
  626. if (typeof document === 'undefined') {
  627. return;
  628. }
  629. // get the script location, and built the css file name from the js file name
  630. // http://stackoverflow.com/a/2161748/1262753
  631. // var scripts = document.getElementsByTagName('script');
  632. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  633. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  634. // inject css
  635. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  636. var style = document.createElement('style');
  637. style.type = 'text/css';
  638. if (style.styleSheet){
  639. style.styleSheet.cssText = css;
  640. } else {
  641. style.appendChild(document.createTextNode(css));
  642. }
  643. document.getElementsByTagName('head')[0].appendChild(style);
  644. };
  645. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  646. // it here in that case.
  647. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  648. if(!Array.prototype.indexOf) {
  649. Array.prototype.indexOf = function(obj){
  650. for(var i = 0; i < this.length; i++){
  651. if(this[i] == obj){
  652. return i;
  653. }
  654. }
  655. return -1;
  656. };
  657. try {
  658. console.log("Warning: Ancient browser detected. Please update your browser");
  659. }
  660. catch (err) {
  661. }
  662. }
  663. // Internet Explorer 8 and older does not support Array.forEach, so we define
  664. // it here in that case.
  665. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  666. if (!Array.prototype.forEach) {
  667. Array.prototype.forEach = function(fn, scope) {
  668. for(var i = 0, len = this.length; i < len; ++i) {
  669. fn.call(scope || this, this[i], i, this);
  670. }
  671. }
  672. }
  673. // Internet Explorer 8 and older does not support Array.map, so we define it
  674. // here in that case.
  675. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  676. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  677. // Reference: http://es5.github.com/#x15.4.4.19
  678. if (!Array.prototype.map) {
  679. Array.prototype.map = function(callback, thisArg) {
  680. var T, A, k;
  681. if (this == null) {
  682. throw new TypeError(" this is null or not defined");
  683. }
  684. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  685. var O = Object(this);
  686. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  687. // 3. Let len be ToUint32(lenValue).
  688. var len = O.length >>> 0;
  689. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  690. // See: http://es5.github.com/#x9.11
  691. if (typeof callback !== "function") {
  692. throw new TypeError(callback + " is not a function");
  693. }
  694. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  695. if (thisArg) {
  696. T = thisArg;
  697. }
  698. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  699. // the standard built-in constructor with that name and len is the value of len.
  700. A = new Array(len);
  701. // 7. Let k be 0
  702. k = 0;
  703. // 8. Repeat, while k < len
  704. while(k < len) {
  705. var kValue, mappedValue;
  706. // a. Let Pk be ToString(k).
  707. // This is implicit for LHS operands of the in operator
  708. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  709. // This step can be combined with c
  710. // c. If kPresent is true, then
  711. if (k in O) {
  712. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  713. kValue = O[ k ];
  714. // ii. Let mappedValue be the result of calling the Call internal method of callback
  715. // with T as the this value and argument list containing kValue, k, and O.
  716. mappedValue = callback.call(T, kValue, k, O);
  717. // iii. Call the DefineOwnProperty internal method of A with arguments
  718. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  719. // and false.
  720. // In browsers that support Object.defineProperty, use the following:
  721. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  722. // For best browser support, use the following:
  723. A[ k ] = mappedValue;
  724. }
  725. // d. Increase k by 1.
  726. k++;
  727. }
  728. // 9. return A
  729. return A;
  730. };
  731. }
  732. // Internet Explorer 8 and older does not support Array.filter, so we define it
  733. // here in that case.
  734. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  735. if (!Array.prototype.filter) {
  736. Array.prototype.filter = function(fun /*, thisp */) {
  737. "use strict";
  738. if (this == null) {
  739. throw new TypeError();
  740. }
  741. var t = Object(this);
  742. var len = t.length >>> 0;
  743. if (typeof fun != "function") {
  744. throw new TypeError();
  745. }
  746. var res = [];
  747. var thisp = arguments[1];
  748. for (var i = 0; i < len; i++) {
  749. if (i in t) {
  750. var val = t[i]; // in case fun mutates this
  751. if (fun.call(thisp, val, i, t))
  752. res.push(val);
  753. }
  754. }
  755. return res;
  756. };
  757. }
  758. // Internet Explorer 8 and older does not support Object.keys, so we define it
  759. // here in that case.
  760. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  761. if (!Object.keys) {
  762. Object.keys = (function () {
  763. var hasOwnProperty = Object.prototype.hasOwnProperty,
  764. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  765. dontEnums = [
  766. 'toString',
  767. 'toLocaleString',
  768. 'valueOf',
  769. 'hasOwnProperty',
  770. 'isPrototypeOf',
  771. 'propertyIsEnumerable',
  772. 'constructor'
  773. ],
  774. dontEnumsLength = dontEnums.length;
  775. return function (obj) {
  776. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  777. throw new TypeError('Object.keys called on non-object');
  778. }
  779. var result = [];
  780. for (var prop in obj) {
  781. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  782. }
  783. if (hasDontEnumBug) {
  784. for (var i=0; i < dontEnumsLength; i++) {
  785. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  786. }
  787. }
  788. return result;
  789. }
  790. })()
  791. }
  792. // Internet Explorer 8 and older does not support Array.isArray,
  793. // so we define it here in that case.
  794. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  795. if(!Array.isArray) {
  796. Array.isArray = function (vArg) {
  797. return Object.prototype.toString.call(vArg) === "[object Array]";
  798. };
  799. }
  800. // Internet Explorer 8 and older does not support Function.bind,
  801. // so we define it here in that case.
  802. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  803. if (!Function.prototype.bind) {
  804. Function.prototype.bind = function (oThis) {
  805. if (typeof this !== "function") {
  806. // closest thing possible to the ECMAScript 5 internal IsCallable function
  807. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  808. }
  809. var aArgs = Array.prototype.slice.call(arguments, 1),
  810. fToBind = this,
  811. fNOP = function () {},
  812. fBound = function () {
  813. return fToBind.apply(this instanceof fNOP && oThis
  814. ? this
  815. : oThis,
  816. aArgs.concat(Array.prototype.slice.call(arguments)));
  817. };
  818. fNOP.prototype = this.prototype;
  819. fBound.prototype = new fNOP();
  820. return fBound;
  821. };
  822. }
  823. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  824. if (!Object.create) {
  825. Object.create = function (o) {
  826. if (arguments.length > 1) {
  827. throw new Error('Object.create implementation only accepts the first parameter.');
  828. }
  829. function F() {}
  830. F.prototype = o;
  831. return new F();
  832. };
  833. }
  834. /**
  835. * Event listener (singleton)
  836. */
  837. // TODO: replace usage of the event listener for the EventBus
  838. var events = {
  839. 'listeners': [],
  840. /**
  841. * Find a single listener by its object
  842. * @param {Object} object
  843. * @return {Number} index -1 when not found
  844. */
  845. 'indexOf': function (object) {
  846. var listeners = this.listeners;
  847. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  848. var listener = listeners[i];
  849. if (listener && listener.object == object) {
  850. return i;
  851. }
  852. }
  853. return -1;
  854. },
  855. /**
  856. * Add an event listener
  857. * @param {Object} object
  858. * @param {String} event The name of an event, for example 'select'
  859. * @param {function} callback The callback method, called when the
  860. * event takes place
  861. */
  862. 'addListener': function (object, event, callback) {
  863. var index = this.indexOf(object);
  864. var listener = this.listeners[index];
  865. if (!listener) {
  866. listener = {
  867. 'object': object,
  868. 'events': {}
  869. };
  870. this.listeners.push(listener);
  871. }
  872. var callbacks = listener.events[event];
  873. if (!callbacks) {
  874. callbacks = [];
  875. listener.events[event] = callbacks;
  876. }
  877. // add the callback if it does not yet exist
  878. if (callbacks.indexOf(callback) == -1) {
  879. callbacks.push(callback);
  880. }
  881. },
  882. /**
  883. * Remove an event listener
  884. * @param {Object} object
  885. * @param {String} event The name of an event, for example 'select'
  886. * @param {function} callback The registered callback method
  887. */
  888. 'removeListener': function (object, event, callback) {
  889. var index = this.indexOf(object);
  890. var listener = this.listeners[index];
  891. if (listener) {
  892. var callbacks = listener.events[event];
  893. if (callbacks) {
  894. index = callbacks.indexOf(callback);
  895. if (index != -1) {
  896. callbacks.splice(index, 1);
  897. }
  898. // remove the array when empty
  899. if (callbacks.length == 0) {
  900. delete listener.events[event];
  901. }
  902. }
  903. // count the number of registered events. remove listener when empty
  904. var count = 0;
  905. var events = listener.events;
  906. for (var e in events) {
  907. if (events.hasOwnProperty(e)) {
  908. count++;
  909. }
  910. }
  911. if (count == 0) {
  912. delete this.listeners[index];
  913. }
  914. }
  915. },
  916. /**
  917. * Remove all registered event listeners
  918. */
  919. 'removeAllListeners': function () {
  920. this.listeners = [];
  921. },
  922. /**
  923. * Trigger an event. All registered event handlers will be called
  924. * @param {Object} object
  925. * @param {String} event
  926. * @param {Object} properties (optional)
  927. */
  928. 'trigger': function (object, event, properties) {
  929. var index = this.indexOf(object);
  930. var listener = this.listeners[index];
  931. if (listener) {
  932. var callbacks = listener.events[event];
  933. if (callbacks) {
  934. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  935. callbacks[i](properties);
  936. }
  937. }
  938. }
  939. }
  940. };
  941. /**
  942. * An event bus can be used to emit events, and to subscribe to events
  943. * @constructor EventBus
  944. */
  945. function EventBus() {
  946. this.subscriptions = [];
  947. }
  948. /**
  949. * Subscribe to an event
  950. * @param {String | RegExp} event The event can be a regular expression, or
  951. * a string with wildcards, like 'server.*'.
  952. * @param {function} callback. Callback are called with three parameters:
  953. * {String} event, {*} [data], {*} [source]
  954. * @param {*} [target]
  955. * @returns {String} id A subscription id
  956. */
  957. EventBus.prototype.on = function (event, callback, target) {
  958. var regexp = (event instanceof RegExp) ?
  959. event :
  960. new RegExp(event.replace('*', '\\w+'));
  961. var subscription = {
  962. id: util.randomUUID(),
  963. event: event,
  964. regexp: regexp,
  965. callback: (typeof callback === 'function') ? callback : null,
  966. target: target
  967. };
  968. this.subscriptions.push(subscription);
  969. return subscription.id;
  970. };
  971. /**
  972. * Unsubscribe from an event
  973. * @param {String | Object} filter Filter for subscriptions to be removed
  974. * Filter can be a string containing a
  975. * subscription id, or an object containing
  976. * one or more of the fields id, event,
  977. * callback, and target.
  978. */
  979. EventBus.prototype.off = function (filter) {
  980. var i = 0;
  981. while (i < this.subscriptions.length) {
  982. var subscription = this.subscriptions[i];
  983. var match = true;
  984. if (filter instanceof Object) {
  985. // filter is an object. All fields must match
  986. for (var prop in filter) {
  987. if (filter.hasOwnProperty(prop)) {
  988. if (filter[prop] !== subscription[prop]) {
  989. match = false;
  990. }
  991. }
  992. }
  993. }
  994. else {
  995. // filter is a string, filter on id
  996. match = (subscription.id == filter);
  997. }
  998. if (match) {
  999. this.subscriptions.splice(i, 1);
  1000. }
  1001. else {
  1002. i++;
  1003. }
  1004. }
  1005. };
  1006. /**
  1007. * Emit an event
  1008. * @param {String} event
  1009. * @param {*} [data]
  1010. * @param {*} [source]
  1011. */
  1012. EventBus.prototype.emit = function (event, data, source) {
  1013. for (var i =0; i < this.subscriptions.length; i++) {
  1014. var subscription = this.subscriptions[i];
  1015. if (subscription.regexp.test(event)) {
  1016. if (subscription.callback) {
  1017. subscription.callback(event, data, source);
  1018. }
  1019. }
  1020. }
  1021. };
  1022. /**
  1023. * DataSet
  1024. *
  1025. * Usage:
  1026. * var dataSet = new DataSet({
  1027. * fieldId: '_id',
  1028. * convert: {
  1029. * // ...
  1030. * }
  1031. * });
  1032. *
  1033. * dataSet.add(item);
  1034. * dataSet.add(data);
  1035. * dataSet.update(item);
  1036. * dataSet.update(data);
  1037. * dataSet.remove(id);
  1038. * dataSet.remove(ids);
  1039. * var data = dataSet.get();
  1040. * var data = dataSet.get(id);
  1041. * var data = dataSet.get(ids);
  1042. * var data = dataSet.get(ids, options, data);
  1043. * dataSet.clear();
  1044. *
  1045. * A data set can:
  1046. * - add/remove/update data
  1047. * - gives triggers upon changes in the data
  1048. * - can import/export data in various data formats
  1049. *
  1050. * @param {Object} [options] Available options:
  1051. * {String} fieldId Field name of the id in the
  1052. * items, 'id' by default.
  1053. * {Object.<String, String} convert
  1054. * A map with field names as key,
  1055. * and the field type as value.
  1056. * @constructor DataSet
  1057. */
  1058. // TODO: add a DataSet constructor DataSet(data, options)
  1059. function DataSet (options) {
  1060. this.id = util.randomUUID();
  1061. this.options = options || {};
  1062. this.data = {}; // map with data indexed by id
  1063. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1064. this.convert = {}; // field types by field name
  1065. if (this.options.convert) {
  1066. for (var field in this.options.convert) {
  1067. if (this.options.convert.hasOwnProperty(field)) {
  1068. var value = this.options.convert[field];
  1069. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1070. this.convert[field] = 'Date';
  1071. }
  1072. else {
  1073. this.convert[field] = value;
  1074. }
  1075. }
  1076. }
  1077. }
  1078. // event subscribers
  1079. this.subscribers = {};
  1080. this.internalIds = {}; // internally generated id's
  1081. }
  1082. /**
  1083. * Subscribe to an event, add an event listener
  1084. * @param {String} event Event name. Available events: 'put', 'update',
  1085. * 'remove'
  1086. * @param {function} callback Callback method. Called with three parameters:
  1087. * {String} event
  1088. * {Object | null} params
  1089. * {String | Number} senderId
  1090. */
  1091. DataSet.prototype.subscribe = function (event, callback) {
  1092. var subscribers = this.subscribers[event];
  1093. if (!subscribers) {
  1094. subscribers = [];
  1095. this.subscribers[event] = subscribers;
  1096. }
  1097. subscribers.push({
  1098. callback: callback
  1099. });
  1100. };
  1101. /**
  1102. * Unsubscribe from an event, remove an event listener
  1103. * @param {String} event
  1104. * @param {function} callback
  1105. */
  1106. DataSet.prototype.unsubscribe = function (event, callback) {
  1107. var subscribers = this.subscribers[event];
  1108. if (subscribers) {
  1109. this.subscribers[event] = subscribers.filter(function (listener) {
  1110. return (listener.callback != callback);
  1111. });
  1112. }
  1113. };
  1114. /**
  1115. * Trigger an event
  1116. * @param {String} event
  1117. * @param {Object | null} params
  1118. * @param {String} [senderId] Optional id of the sender.
  1119. * @private
  1120. */
  1121. DataSet.prototype._trigger = function (event, params, senderId) {
  1122. if (event == '*') {
  1123. throw new Error('Cannot trigger event *');
  1124. }
  1125. var subscribers = [];
  1126. if (event in this.subscribers) {
  1127. subscribers = subscribers.concat(this.subscribers[event]);
  1128. }
  1129. if ('*' in this.subscribers) {
  1130. subscribers = subscribers.concat(this.subscribers['*']);
  1131. }
  1132. for (var i = 0; i < subscribers.length; i++) {
  1133. var subscriber = subscribers[i];
  1134. if (subscriber.callback) {
  1135. subscriber.callback(event, params, senderId || null);
  1136. }
  1137. }
  1138. };
  1139. /**
  1140. * Add data.
  1141. * Adding an item will fail when there already is an item with the same id.
  1142. * @param {Object | Array | DataTable} data
  1143. * @param {String} [senderId] Optional sender id
  1144. * @return {Array} addedIds Array with the ids of the added items
  1145. */
  1146. DataSet.prototype.add = function (data, senderId) {
  1147. var addedIds = [],
  1148. id,
  1149. me = this;
  1150. if (data instanceof Array) {
  1151. // Array
  1152. for (var i = 0, len = data.length; i < len; i++) {
  1153. id = me._addItem(data[i]);
  1154. addedIds.push(id);
  1155. }
  1156. }
  1157. else if (util.isDataTable(data)) {
  1158. // Google DataTable
  1159. var columns = this._getColumnNames(data);
  1160. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1161. var item = {};
  1162. for (var col = 0, cols = columns.length; col < cols; col++) {
  1163. var field = columns[col];
  1164. item[field] = data.getValue(row, col);
  1165. }
  1166. id = me._addItem(item);
  1167. addedIds.push(id);
  1168. }
  1169. }
  1170. else if (data instanceof Object) {
  1171. // Single item
  1172. id = me._addItem(data);
  1173. addedIds.push(id);
  1174. }
  1175. else {
  1176. throw new Error('Unknown dataType');
  1177. }
  1178. if (addedIds.length) {
  1179. this._trigger('add', {items: addedIds}, senderId);
  1180. }
  1181. return addedIds;
  1182. };
  1183. /**
  1184. * Update existing items. When an item does not exist, it will be created
  1185. * @param {Object | Array | DataTable} data
  1186. * @param {String} [senderId] Optional sender id
  1187. * @return {Array} updatedIds The ids of the added or updated items
  1188. */
  1189. DataSet.prototype.update = function (data, senderId) {
  1190. var addedIds = [],
  1191. updatedIds = [],
  1192. me = this,
  1193. fieldId = me.fieldId;
  1194. var addOrUpdate = function (item) {
  1195. var id = item[fieldId];
  1196. if (me.data[id]) {
  1197. // update item
  1198. id = me._updateItem(item);
  1199. updatedIds.push(id);
  1200. }
  1201. else {
  1202. // add new item
  1203. id = me._addItem(item);
  1204. addedIds.push(id);
  1205. }
  1206. };
  1207. if (data instanceof Array) {
  1208. // Array
  1209. for (var i = 0, len = data.length; i < len; i++) {
  1210. addOrUpdate(data[i]);
  1211. }
  1212. }
  1213. else if (util.isDataTable(data)) {
  1214. // Google DataTable
  1215. var columns = this._getColumnNames(data);
  1216. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1217. var item = {};
  1218. for (var col = 0, cols = columns.length; col < cols; col++) {
  1219. var field = columns[col];
  1220. item[field] = data.getValue(row, col);
  1221. }
  1222. addOrUpdate(item);
  1223. }
  1224. }
  1225. else if (data instanceof Object) {
  1226. // Single item
  1227. addOrUpdate(data);
  1228. }
  1229. else {
  1230. throw new Error('Unknown dataType');
  1231. }
  1232. if (addedIds.length) {
  1233. this._trigger('add', {items: addedIds}, senderId);
  1234. }
  1235. if (updatedIds.length) {
  1236. this._trigger('update', {items: updatedIds}, senderId);
  1237. }
  1238. return addedIds.concat(updatedIds);
  1239. };
  1240. /**
  1241. * Get a data item or multiple items.
  1242. *
  1243. * Usage:
  1244. *
  1245. * get()
  1246. * get(options: Object)
  1247. * get(options: Object, data: Array | DataTable)
  1248. *
  1249. * get(id: Number | String)
  1250. * get(id: Number | String, options: Object)
  1251. * get(id: Number | String, options: Object, data: Array | DataTable)
  1252. *
  1253. * get(ids: Number[] | String[])
  1254. * get(ids: Number[] | String[], options: Object)
  1255. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1256. *
  1257. * Where:
  1258. *
  1259. * {Number | String} id The id of an item
  1260. * {Number[] | String{}} ids An array with ids of items
  1261. * {Object} options An Object with options. Available options:
  1262. * {String} [type] Type of data to be returned. Can
  1263. * be 'DataTable' or 'Array' (default)
  1264. * {Object.<String, String>} [convert]
  1265. * {String[]} [fields] field names to be returned
  1266. * {function} [filter] filter items
  1267. * {String | function} [order] Order the items by
  1268. * a field name or custom sort function.
  1269. * {Array | DataTable} [data] If provided, items will be appended to this
  1270. * array or table. Required in case of Google
  1271. * DataTable.
  1272. *
  1273. * @throws Error
  1274. */
  1275. DataSet.prototype.get = function (args) {
  1276. var me = this;
  1277. // parse the arguments
  1278. var id, ids, options, data;
  1279. var firstType = util.getType(arguments[0]);
  1280. if (firstType == 'String' || firstType == 'Number') {
  1281. // get(id [, options] [, data])
  1282. id = arguments[0];
  1283. options = arguments[1];
  1284. data = arguments[2];
  1285. }
  1286. else if (firstType == 'Array') {
  1287. // get(ids [, options] [, data])
  1288. ids = arguments[0];
  1289. options = arguments[1];
  1290. data = arguments[2];
  1291. }
  1292. else {
  1293. // get([, options] [, data])
  1294. options = arguments[0];
  1295. data = arguments[1];
  1296. }
  1297. // determine the return type
  1298. var type;
  1299. if (options && options.type) {
  1300. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1301. if (data && (type != util.getType(data))) {
  1302. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1303. 'does not correspond with specified options.type (' + options.type + ')');
  1304. }
  1305. if (type == 'DataTable' && !util.isDataTable(data)) {
  1306. throw new Error('Parameter "data" must be a DataTable ' +
  1307. 'when options.type is "DataTable"');
  1308. }
  1309. }
  1310. else if (data) {
  1311. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1312. }
  1313. else {
  1314. type = 'Array';
  1315. }
  1316. // build options
  1317. var convert = options && options.convert || this.options.convert;
  1318. var filter = options && options.filter;
  1319. var items = [], item, itemId, i, len;
  1320. // convert items
  1321. if (id != undefined) {
  1322. // return a single item
  1323. item = me._getItem(id, convert);
  1324. if (filter && !filter(item)) {
  1325. item = null;
  1326. }
  1327. }
  1328. else if (ids != undefined) {
  1329. // return a subset of items
  1330. for (i = 0, len = ids.length; i < len; i++) {
  1331. item = me._getItem(ids[i], convert);
  1332. if (!filter || filter(item)) {
  1333. items.push(item);
  1334. }
  1335. }
  1336. }
  1337. else {
  1338. // return all items
  1339. for (itemId in this.data) {
  1340. if (this.data.hasOwnProperty(itemId)) {
  1341. item = me._getItem(itemId, convert);
  1342. if (!filter || filter(item)) {
  1343. items.push(item);
  1344. }
  1345. }
  1346. }
  1347. }
  1348. // order the results
  1349. if (options && options.order && id == undefined) {
  1350. this._sort(items, options.order);
  1351. }
  1352. // filter fields of the items
  1353. if (options && options.fields) {
  1354. var fields = options.fields;
  1355. if (id != undefined) {
  1356. item = this._filterFields(item, fields);
  1357. }
  1358. else {
  1359. for (i = 0, len = items.length; i < len; i++) {
  1360. items[i] = this._filterFields(items[i], fields);
  1361. }
  1362. }
  1363. }
  1364. // return the results
  1365. if (type == 'DataTable') {
  1366. var columns = this._getColumnNames(data);
  1367. if (id != undefined) {
  1368. // append a single item to the data table
  1369. me._appendRow(data, columns, item);
  1370. }
  1371. else {
  1372. // copy the items to the provided data table
  1373. for (i = 0, len = items.length; i < len; i++) {
  1374. me._appendRow(data, columns, items[i]);
  1375. }
  1376. }
  1377. return data;
  1378. }
  1379. else {
  1380. // return an array
  1381. if (id != undefined) {
  1382. // a single item
  1383. return item;
  1384. }
  1385. else {
  1386. // multiple items
  1387. if (data) {
  1388. // copy the items to the provided array
  1389. for (i = 0, len = items.length; i < len; i++) {
  1390. data.push(items[i]);
  1391. }
  1392. return data;
  1393. }
  1394. else {
  1395. // just return our array
  1396. return items;
  1397. }
  1398. }
  1399. }
  1400. };
  1401. /**
  1402. * Get ids of all items or from a filtered set of items.
  1403. * @param {Object} [options] An Object with options. Available options:
  1404. * {function} [filter] filter items
  1405. * {String | function} [order] Order the items by
  1406. * a field name or custom sort function.
  1407. * @return {Array} ids
  1408. */
  1409. DataSet.prototype.getIds = function (options) {
  1410. var data = this.data,
  1411. filter = options && options.filter,
  1412. order = options && options.order,
  1413. convert = options && options.convert || this.options.convert,
  1414. i,
  1415. len,
  1416. id,
  1417. item,
  1418. items,
  1419. ids = [];
  1420. if (filter) {
  1421. // get filtered items
  1422. if (order) {
  1423. // create ordered list
  1424. items = [];
  1425. for (id in data) {
  1426. if (data.hasOwnProperty(id)) {
  1427. item = this._getItem(id, convert);
  1428. if (filter(item)) {
  1429. items.push(item);
  1430. }
  1431. }
  1432. }
  1433. this._sort(items, order);
  1434. for (i = 0, len = items.length; i < len; i++) {
  1435. ids[i] = items[i][this.fieldId];
  1436. }
  1437. }
  1438. else {
  1439. // create unordered list
  1440. for (id in data) {
  1441. if (data.hasOwnProperty(id)) {
  1442. item = this._getItem(id, convert);
  1443. if (filter(item)) {
  1444. ids.push(item[this.fieldId]);
  1445. }
  1446. }
  1447. }
  1448. }
  1449. }
  1450. else {
  1451. // get all items
  1452. if (order) {
  1453. // create an ordered list
  1454. items = [];
  1455. for (id in data) {
  1456. if (data.hasOwnProperty(id)) {
  1457. items.push(data[id]);
  1458. }
  1459. }
  1460. this._sort(items, order);
  1461. for (i = 0, len = items.length; i < len; i++) {
  1462. ids[i] = items[i][this.fieldId];
  1463. }
  1464. }
  1465. else {
  1466. // create unordered list
  1467. for (id in data) {
  1468. if (data.hasOwnProperty(id)) {
  1469. item = data[id];
  1470. ids.push(item[this.fieldId]);
  1471. }
  1472. }
  1473. }
  1474. }
  1475. return ids;
  1476. };
  1477. /**
  1478. * Execute a callback function for every item in the dataset.
  1479. * The order of the items is not determined.
  1480. * @param {function} callback
  1481. * @param {Object} [options] Available options:
  1482. * {Object.<String, String>} [convert]
  1483. * {String[]} [fields] filter fields
  1484. * {function} [filter] filter items
  1485. * {String | function} [order] Order the items by
  1486. * a field name or custom sort function.
  1487. */
  1488. DataSet.prototype.forEach = function (callback, options) {
  1489. var filter = options && options.filter,
  1490. convert = options && options.convert || this.options.convert,
  1491. data = this.data,
  1492. item,
  1493. id;
  1494. if (options && options.order) {
  1495. // execute forEach on ordered list
  1496. var items = this.get(options);
  1497. for (var i = 0, len = items.length; i < len; i++) {
  1498. item = items[i];
  1499. id = item[this.fieldId];
  1500. callback(item, id);
  1501. }
  1502. }
  1503. else {
  1504. // unordered
  1505. for (id in data) {
  1506. if (data.hasOwnProperty(id)) {
  1507. item = this._getItem(id, convert);
  1508. if (!filter || filter(item)) {
  1509. callback(item, id);
  1510. }
  1511. }
  1512. }
  1513. }
  1514. };
  1515. /**
  1516. * Map every item in the dataset.
  1517. * @param {function} callback
  1518. * @param {Object} [options] Available options:
  1519. * {Object.<String, String>} [convert]
  1520. * {String[]} [fields] filter fields
  1521. * {function} [filter] filter items
  1522. * {String | function} [order] Order the items by
  1523. * a field name or custom sort function.
  1524. * @return {Object[]} mappedItems
  1525. */
  1526. DataSet.prototype.map = function (callback, options) {
  1527. var filter = options && options.filter,
  1528. convert = options && options.convert || this.options.convert,
  1529. mappedItems = [],
  1530. data = this.data,
  1531. item;
  1532. // convert and filter items
  1533. for (var id in data) {
  1534. if (data.hasOwnProperty(id)) {
  1535. item = this._getItem(id, convert);
  1536. if (!filter || filter(item)) {
  1537. mappedItems.push(callback(item, id));
  1538. }
  1539. }
  1540. }
  1541. // order items
  1542. if (options && options.order) {
  1543. this._sort(mappedItems, options.order);
  1544. }
  1545. return mappedItems;
  1546. };
  1547. /**
  1548. * Filter the fields of an item
  1549. * @param {Object} item
  1550. * @param {String[]} fields Field names
  1551. * @return {Object} filteredItem
  1552. * @private
  1553. */
  1554. DataSet.prototype._filterFields = function (item, fields) {
  1555. var filteredItem = {};
  1556. for (var field in item) {
  1557. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1558. filteredItem[field] = item[field];
  1559. }
  1560. }
  1561. return filteredItem;
  1562. };
  1563. /**
  1564. * Sort the provided array with items
  1565. * @param {Object[]} items
  1566. * @param {String | function} order A field name or custom sort function.
  1567. * @private
  1568. */
  1569. DataSet.prototype._sort = function (items, order) {
  1570. if (util.isString(order)) {
  1571. // order by provided field name
  1572. var name = order; // field name
  1573. items.sort(function (a, b) {
  1574. var av = a[name];
  1575. var bv = b[name];
  1576. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1577. });
  1578. }
  1579. else if (typeof order === 'function') {
  1580. // order by sort function
  1581. items.sort(order);
  1582. }
  1583. // TODO: extend order by an Object {field:String, direction:String}
  1584. // where direction can be 'asc' or 'desc'
  1585. else {
  1586. throw new TypeError('Order must be a function or a string');
  1587. }
  1588. };
  1589. /**
  1590. * Remove an object by pointer or by id
  1591. * @param {String | Number | Object | Array} id Object or id, or an array with
  1592. * objects or ids to be removed
  1593. * @param {String} [senderId] Optional sender id
  1594. * @return {Array} removedIds
  1595. */
  1596. DataSet.prototype.remove = function (id, senderId) {
  1597. var removedIds = [],
  1598. i, len, removedId;
  1599. if (id instanceof Array) {
  1600. for (i = 0, len = id.length; i < len; i++) {
  1601. removedId = this._remove(id[i]);
  1602. if (removedId != null) {
  1603. removedIds.push(removedId);
  1604. }
  1605. }
  1606. }
  1607. else {
  1608. removedId = this._remove(id);
  1609. if (removedId != null) {
  1610. removedIds.push(removedId);
  1611. }
  1612. }
  1613. if (removedIds.length) {
  1614. this._trigger('remove', {items: removedIds}, senderId);
  1615. }
  1616. return removedIds;
  1617. };
  1618. /**
  1619. * Remove an item by its id
  1620. * @param {Number | String | Object} id id or item
  1621. * @returns {Number | String | null} id
  1622. * @private
  1623. */
  1624. DataSet.prototype._remove = function (id) {
  1625. if (util.isNumber(id) || util.isString(id)) {
  1626. if (this.data[id]) {
  1627. delete this.data[id];
  1628. delete this.internalIds[id];
  1629. return id;
  1630. }
  1631. }
  1632. else if (id instanceof Object) {
  1633. var itemId = id[this.fieldId];
  1634. if (itemId && this.data[itemId]) {
  1635. delete this.data[itemId];
  1636. delete this.internalIds[itemId];
  1637. return itemId;
  1638. }
  1639. }
  1640. return null;
  1641. };
  1642. /**
  1643. * Clear the data
  1644. * @param {String} [senderId] Optional sender id
  1645. * @return {Array} removedIds The ids of all removed items
  1646. */
  1647. DataSet.prototype.clear = function (senderId) {
  1648. var ids = Object.keys(this.data);
  1649. this.data = {};
  1650. this.internalIds = {};
  1651. this._trigger('remove', {items: ids}, senderId);
  1652. return ids;
  1653. };
  1654. /**
  1655. * Find the item with maximum value of a specified field
  1656. * @param {String} field
  1657. * @return {Object | null} item Item containing max value, or null if no items
  1658. */
  1659. DataSet.prototype.max = function (field) {
  1660. var data = this.data,
  1661. max = null,
  1662. maxField = null;
  1663. for (var id in data) {
  1664. if (data.hasOwnProperty(id)) {
  1665. var item = data[id];
  1666. var itemField = item[field];
  1667. if (itemField != null && (!max || itemField > maxField)) {
  1668. max = item;
  1669. maxField = itemField;
  1670. }
  1671. }
  1672. }
  1673. return max;
  1674. };
  1675. /**
  1676. * Find the item with minimum value of a specified field
  1677. * @param {String} field
  1678. * @return {Object | null} item Item containing max value, or null if no items
  1679. */
  1680. DataSet.prototype.min = function (field) {
  1681. var data = this.data,
  1682. min = null,
  1683. minField = null;
  1684. for (var id in data) {
  1685. if (data.hasOwnProperty(id)) {
  1686. var item = data[id];
  1687. var itemField = item[field];
  1688. if (itemField != null && (!min || itemField < minField)) {
  1689. min = item;
  1690. minField = itemField;
  1691. }
  1692. }
  1693. }
  1694. return min;
  1695. };
  1696. /**
  1697. * Find all distinct values of a specified field
  1698. * @param {String} field
  1699. * @return {Array} values Array containing all distinct values. If the data
  1700. * items do not contain the specified field, an array
  1701. * containing a single value undefined is returned.
  1702. * The returned array is unordered.
  1703. */
  1704. DataSet.prototype.distinct = function (field) {
  1705. var data = this.data,
  1706. values = [],
  1707. fieldType = this.options.convert[field],
  1708. count = 0;
  1709. for (var prop in data) {
  1710. if (data.hasOwnProperty(prop)) {
  1711. var item = data[prop];
  1712. var value = util.convert(item[field], fieldType);
  1713. var exists = false;
  1714. for (var i = 0; i < count; i++) {
  1715. if (values[i] == value) {
  1716. exists = true;
  1717. break;
  1718. }
  1719. }
  1720. if (!exists) {
  1721. values[count] = value;
  1722. count++;
  1723. }
  1724. }
  1725. }
  1726. return values;
  1727. };
  1728. /**
  1729. * Add a single item. Will fail when an item with the same id already exists.
  1730. * @param {Object} item
  1731. * @return {String} id
  1732. * @private
  1733. */
  1734. DataSet.prototype._addItem = function (item) {
  1735. var id = item[this.fieldId];
  1736. if (id != undefined) {
  1737. // check whether this id is already taken
  1738. if (this.data[id]) {
  1739. // item already exists
  1740. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1741. }
  1742. }
  1743. else {
  1744. // generate an id
  1745. id = util.randomUUID();
  1746. item[this.fieldId] = id;
  1747. this.internalIds[id] = item;
  1748. }
  1749. var d = {};
  1750. for (var field in item) {
  1751. if (item.hasOwnProperty(field)) {
  1752. var fieldType = this.convert[field]; // type may be undefined
  1753. d[field] = util.convert(item[field], fieldType);
  1754. }
  1755. }
  1756. this.data[id] = d;
  1757. return id;
  1758. };
  1759. /**
  1760. * Get an item. Fields can be converted to a specific type
  1761. * @param {String} id
  1762. * @param {Object.<String, String>} [convert] field types to convert
  1763. * @return {Object | null} item
  1764. * @private
  1765. */
  1766. DataSet.prototype._getItem = function (id, convert) {
  1767. var field, value;
  1768. // get the item from the dataset
  1769. var raw = this.data[id];
  1770. if (!raw) {
  1771. return null;
  1772. }
  1773. // convert the items field types
  1774. var converted = {},
  1775. fieldId = this.fieldId,
  1776. internalIds = this.internalIds;
  1777. if (convert) {
  1778. for (field in raw) {
  1779. if (raw.hasOwnProperty(field)) {
  1780. value = raw[field];
  1781. // output all fields, except internal ids
  1782. if ((field != fieldId) || !(value in internalIds)) {
  1783. converted[field] = util.convert(value, convert[field]);
  1784. }
  1785. }
  1786. }
  1787. }
  1788. else {
  1789. // no field types specified, no converting needed
  1790. for (field in raw) {
  1791. if (raw.hasOwnProperty(field)) {
  1792. value = raw[field];
  1793. // output all fields, except internal ids
  1794. if ((field != fieldId) || !(value in internalIds)) {
  1795. converted[field] = value;
  1796. }
  1797. }
  1798. }
  1799. }
  1800. return converted;
  1801. };
  1802. /**
  1803. * Update a single item: merge with existing item.
  1804. * Will fail when the item has no id, or when there does not exist an item
  1805. * with the same id.
  1806. * @param {Object} item
  1807. * @return {String} id
  1808. * @private
  1809. */
  1810. DataSet.prototype._updateItem = function (item) {
  1811. var id = item[this.fieldId];
  1812. if (id == undefined) {
  1813. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1814. }
  1815. var d = this.data[id];
  1816. if (!d) {
  1817. // item doesn't exist
  1818. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1819. }
  1820. // merge with current item
  1821. for (var field in item) {
  1822. if (item.hasOwnProperty(field)) {
  1823. var fieldType = this.convert[field]; // type may be undefined
  1824. d[field] = util.convert(item[field], fieldType);
  1825. }
  1826. }
  1827. return id;
  1828. };
  1829. /**
  1830. * Get an array with the column names of a Google DataTable
  1831. * @param {DataTable} dataTable
  1832. * @return {String[]} columnNames
  1833. * @private
  1834. */
  1835. DataSet.prototype._getColumnNames = function (dataTable) {
  1836. var columns = [];
  1837. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1838. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1839. }
  1840. return columns;
  1841. };
  1842. /**
  1843. * Append an item as a row to the dataTable
  1844. * @param dataTable
  1845. * @param columns
  1846. * @param item
  1847. * @private
  1848. */
  1849. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1850. var row = dataTable.addRow();
  1851. for (var col = 0, cols = columns.length; col < cols; col++) {
  1852. var field = columns[col];
  1853. dataTable.setValue(row, col, item[field]);
  1854. }
  1855. };
  1856. /**
  1857. * DataView
  1858. *
  1859. * a dataview offers a filtered view on a dataset or an other dataview.
  1860. *
  1861. * @param {DataSet | DataView} data
  1862. * @param {Object} [options] Available options: see method get
  1863. *
  1864. * @constructor DataView
  1865. */
  1866. function DataView (data, options) {
  1867. this.id = util.randomUUID();
  1868. this.data = null;
  1869. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1870. this.options = options || {};
  1871. this.fieldId = 'id'; // name of the field containing id
  1872. this.subscribers = {}; // event subscribers
  1873. var me = this;
  1874. this.listener = function () {
  1875. me._onEvent.apply(me, arguments);
  1876. };
  1877. this.setData(data);
  1878. }
  1879. /**
  1880. * Set a data source for the view
  1881. * @param {DataSet | DataView} data
  1882. */
  1883. DataView.prototype.setData = function (data) {
  1884. var ids, dataItems, i, len;
  1885. if (this.data) {
  1886. // unsubscribe from current dataset
  1887. if (this.data.unsubscribe) {
  1888. this.data.unsubscribe('*', this.listener);
  1889. }
  1890. // trigger a remove of all items in memory
  1891. ids = [];
  1892. for (var id in this.ids) {
  1893. if (this.ids.hasOwnProperty(id)) {
  1894. ids.push(id);
  1895. }
  1896. }
  1897. this.ids = {};
  1898. this._trigger('remove', {items: ids});
  1899. }
  1900. this.data = data;
  1901. if (this.data) {
  1902. // update fieldId
  1903. this.fieldId = this.options.fieldId ||
  1904. (this.data && this.data.options && this.data.options.fieldId) ||
  1905. 'id';
  1906. // trigger an add of all added items
  1907. ids = this.data.getIds({filter: this.options && this.options.filter});
  1908. for (i = 0, len = ids.length; i < len; i++) {
  1909. id = ids[i];
  1910. this.ids[id] = true;
  1911. }
  1912. this._trigger('add', {items: ids});
  1913. // subscribe to new dataset
  1914. if (this.data.subscribe) {
  1915. this.data.subscribe('*', this.listener);
  1916. }
  1917. }
  1918. };
  1919. /**
  1920. * Get data from the data view
  1921. *
  1922. * Usage:
  1923. *
  1924. * get()
  1925. * get(options: Object)
  1926. * get(options: Object, data: Array | DataTable)
  1927. *
  1928. * get(id: Number)
  1929. * get(id: Number, options: Object)
  1930. * get(id: Number, options: Object, data: Array | DataTable)
  1931. *
  1932. * get(ids: Number[])
  1933. * get(ids: Number[], options: Object)
  1934. * get(ids: Number[], options: Object, data: Array | DataTable)
  1935. *
  1936. * Where:
  1937. *
  1938. * {Number | String} id The id of an item
  1939. * {Number[] | String{}} ids An array with ids of items
  1940. * {Object} options An Object with options. Available options:
  1941. * {String} [type] Type of data to be returned. Can
  1942. * be 'DataTable' or 'Array' (default)
  1943. * {Object.<String, String>} [convert]
  1944. * {String[]} [fields] field names to be returned
  1945. * {function} [filter] filter items
  1946. * {String | function} [order] Order the items by
  1947. * a field name or custom sort function.
  1948. * {Array | DataTable} [data] If provided, items will be appended to this
  1949. * array or table. Required in case of Google
  1950. * DataTable.
  1951. * @param args
  1952. */
  1953. DataView.prototype.get = function (args) {
  1954. var me = this;
  1955. // parse the arguments
  1956. var ids, options, data;
  1957. var firstType = util.getType(arguments[0]);
  1958. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1959. // get(id(s) [, options] [, data])
  1960. ids = arguments[0]; // can be a single id or an array with ids
  1961. options = arguments[1];
  1962. data = arguments[2];
  1963. }
  1964. else {
  1965. // get([, options] [, data])
  1966. options = arguments[0];
  1967. data = arguments[1];
  1968. }
  1969. // extend the options with the default options and provided options
  1970. var viewOptions = util.extend({}, this.options, options);
  1971. // create a combined filter method when needed
  1972. if (this.options.filter && options && options.filter) {
  1973. viewOptions.filter = function (item) {
  1974. return me.options.filter(item) && options.filter(item);
  1975. }
  1976. }
  1977. // build up the call to the linked data set
  1978. var getArguments = [];
  1979. if (ids != undefined) {
  1980. getArguments.push(ids);
  1981. }
  1982. getArguments.push(viewOptions);
  1983. getArguments.push(data);
  1984. return this.data && this.data.get.apply(this.data, getArguments);
  1985. };
  1986. /**
  1987. * Get ids of all items or from a filtered set of items.
  1988. * @param {Object} [options] An Object with options. Available options:
  1989. * {function} [filter] filter items
  1990. * {String | function} [order] Order the items by
  1991. * a field name or custom sort function.
  1992. * @return {Array} ids
  1993. */
  1994. DataView.prototype.getIds = function (options) {
  1995. var ids;
  1996. if (this.data) {
  1997. var defaultFilter = this.options.filter;
  1998. var filter;
  1999. if (options && options.filter) {
  2000. if (defaultFilter) {
  2001. filter = function (item) {
  2002. return defaultFilter(item) && options.filter(item);
  2003. }
  2004. }
  2005. else {
  2006. filter = options.filter;
  2007. }
  2008. }
  2009. else {
  2010. filter = defaultFilter;
  2011. }
  2012. ids = this.data.getIds({
  2013. filter: filter,
  2014. order: options && options.order
  2015. });
  2016. }
  2017. else {
  2018. ids = [];
  2019. }
  2020. return ids;
  2021. };
  2022. /**
  2023. * Event listener. Will propagate all events from the connected data set to
  2024. * the subscribers of the DataView, but will filter the items and only trigger
  2025. * when there are changes in the filtered data set.
  2026. * @param {String} event
  2027. * @param {Object | null} params
  2028. * @param {String} senderId
  2029. * @private
  2030. */
  2031. DataView.prototype._onEvent = function (event, params, senderId) {
  2032. var i, len, id, item,
  2033. ids = params && params.items,
  2034. data = this.data,
  2035. added = [],
  2036. updated = [],
  2037. removed = [];
  2038. if (ids && data) {
  2039. switch (event) {
  2040. case 'add':
  2041. // filter the ids of the added items
  2042. for (i = 0, len = ids.length; i < len; i++) {
  2043. id = ids[i];
  2044. item = this.get(id);
  2045. if (item) {
  2046. this.ids[id] = true;
  2047. added.push(id);
  2048. }
  2049. }
  2050. break;
  2051. case 'update':
  2052. // determine the event from the views viewpoint: an updated
  2053. // item can be added, updated, or removed from this view.
  2054. for (i = 0, len = ids.length; i < len; i++) {
  2055. id = ids[i];
  2056. item = this.get(id);
  2057. if (item) {
  2058. if (this.ids[id]) {
  2059. updated.push(id);
  2060. }
  2061. else {
  2062. this.ids[id] = true;
  2063. added.push(id);
  2064. }
  2065. }
  2066. else {
  2067. if (this.ids[id]) {
  2068. delete this.ids[id];
  2069. removed.push(id);
  2070. }
  2071. else {
  2072. // nothing interesting for me :-(
  2073. }
  2074. }
  2075. }
  2076. break;
  2077. case 'remove':
  2078. // filter the ids of the removed items
  2079. for (i = 0, len = ids.length; i < len; i++) {
  2080. id = ids[i];
  2081. if (this.ids[id]) {
  2082. delete this.ids[id];
  2083. removed.push(id);
  2084. }
  2085. }
  2086. break;
  2087. }
  2088. if (added.length) {
  2089. this._trigger('add', {items: added}, senderId);
  2090. }
  2091. if (updated.length) {
  2092. this._trigger('update', {items: updated}, senderId);
  2093. }
  2094. if (removed.length) {
  2095. this._trigger('remove', {items: removed}, senderId);
  2096. }
  2097. }
  2098. };
  2099. // copy subscription functionality from DataSet
  2100. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2101. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2102. DataView.prototype._trigger = DataSet.prototype._trigger;
  2103. /**
  2104. * @constructor TimeStep
  2105. * The class TimeStep is an iterator for dates. You provide a start date and an
  2106. * end date. The class itself determines the best scale (step size) based on the
  2107. * provided start Date, end Date, and minimumStep.
  2108. *
  2109. * If minimumStep is provided, the step size is chosen as close as possible
  2110. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2111. * provided, the scale is set to 1 DAY.
  2112. * The minimumStep should correspond with the onscreen size of about 6 characters
  2113. *
  2114. * Alternatively, you can set a scale by hand.
  2115. * After creation, you can initialize the class by executing first(). Then you
  2116. * can iterate from the start date to the end date via next(). You can check if
  2117. * the end date is reached with the function hasNext(). After each step, you can
  2118. * retrieve the current date via getCurrent().
  2119. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2120. * days, to years.
  2121. *
  2122. * Version: 1.2
  2123. *
  2124. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2125. * or new Date(2010, 9, 21, 23, 45, 00)
  2126. * @param {Date} [end] The end date
  2127. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2128. */
  2129. TimeStep = function(start, end, minimumStep) {
  2130. // variables
  2131. this.current = new Date();
  2132. this._start = new Date();
  2133. this._end = new Date();
  2134. this.autoScale = true;
  2135. this.scale = TimeStep.SCALE.DAY;
  2136. this.step = 1;
  2137. // initialize the range
  2138. this.setRange(start, end, minimumStep);
  2139. };
  2140. /// enum scale
  2141. TimeStep.SCALE = {
  2142. MILLISECOND: 1,
  2143. SECOND: 2,
  2144. MINUTE: 3,
  2145. HOUR: 4,
  2146. DAY: 5,
  2147. WEEKDAY: 6,
  2148. MONTH: 7,
  2149. YEAR: 8
  2150. };
  2151. /**
  2152. * Set a new range
  2153. * If minimumStep is provided, the step size is chosen as close as possible
  2154. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2155. * provided, the scale is set to 1 DAY.
  2156. * The minimumStep should correspond with the onscreen size of about 6 characters
  2157. * @param {Date} [start] The start date and time.
  2158. * @param {Date} [end] The end date and time.
  2159. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2160. */
  2161. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2162. if (!(start instanceof Date) || !(end instanceof Date)) {
  2163. //throw "No legal start or end date in method setRange";
  2164. return;
  2165. }
  2166. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2167. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2168. if (this.autoScale) {
  2169. this.setMinimumStep(minimumStep);
  2170. }
  2171. };
  2172. /**
  2173. * Set the range iterator to the start date.
  2174. */
  2175. TimeStep.prototype.first = function() {
  2176. this.current = new Date(this._start.valueOf());
  2177. this.roundToMinor();
  2178. };
  2179. /**
  2180. * Round the current date to the first minor date value
  2181. * This must be executed once when the current date is set to start Date
  2182. */
  2183. TimeStep.prototype.roundToMinor = function() {
  2184. // round to floor
  2185. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2186. //noinspection FallthroughInSwitchStatementJS
  2187. switch (this.scale) {
  2188. case TimeStep.SCALE.YEAR:
  2189. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2190. this.current.setMonth(0);
  2191. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2192. case TimeStep.SCALE.DAY: // intentional fall through
  2193. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2194. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2195. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2196. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2197. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2198. }
  2199. if (this.step != 1) {
  2200. // round down to the first minor value that is a multiple of the current step size
  2201. switch (this.scale) {
  2202. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2203. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2204. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2205. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2206. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2207. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2208. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2209. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2210. default: break;
  2211. }
  2212. }
  2213. };
  2214. /**
  2215. * Check if the there is a next step
  2216. * @return {boolean} true if the current date has not passed the end date
  2217. */
  2218. TimeStep.prototype.hasNext = function () {
  2219. return (this.current.valueOf() <= this._end.valueOf());
  2220. };
  2221. /**
  2222. * Do the next step
  2223. */
  2224. TimeStep.prototype.next = function() {
  2225. var prev = this.current.valueOf();
  2226. // Two cases, needed to prevent issues with switching daylight savings
  2227. // (end of March and end of October)
  2228. if (this.current.getMonth() < 6) {
  2229. switch (this.scale) {
  2230. case TimeStep.SCALE.MILLISECOND:
  2231. this.current = new Date(this.current.valueOf() + this.step); break;
  2232. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2233. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2234. case TimeStep.SCALE.HOUR:
  2235. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2236. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2237. var h = this.current.getHours();
  2238. this.current.setHours(h - (h % this.step));
  2239. break;
  2240. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2241. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2242. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2243. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2244. default: break;
  2245. }
  2246. }
  2247. else {
  2248. switch (this.scale) {
  2249. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2250. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2251. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2252. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2253. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2254. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2255. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2256. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2257. default: break;
  2258. }
  2259. }
  2260. if (this.step != 1) {
  2261. // round down to the correct major value
  2262. switch (this.scale) {
  2263. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2264. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2265. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2266. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2267. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2268. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2269. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2270. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2271. default: break;
  2272. }
  2273. }
  2274. // safety mechanism: if current time is still unchanged, move to the end
  2275. if (this.current.valueOf() == prev) {
  2276. this.current = new Date(this._end.valueOf());
  2277. }
  2278. };
  2279. /**
  2280. * Get the current datetime
  2281. * @return {Date} current The current date
  2282. */
  2283. TimeStep.prototype.getCurrent = function() {
  2284. return this.current;
  2285. };
  2286. /**
  2287. * Set a custom scale. Autoscaling will be disabled.
  2288. * For example setScale(SCALE.MINUTES, 5) will result
  2289. * in minor steps of 5 minutes, and major steps of an hour.
  2290. *
  2291. * @param {TimeStep.SCALE} newScale
  2292. * A scale. Choose from SCALE.MILLISECOND,
  2293. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2294. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2295. * SCALE.YEAR.
  2296. * @param {Number} newStep A step size, by default 1. Choose for
  2297. * example 1, 2, 5, or 10.
  2298. */
  2299. TimeStep.prototype.setScale = function(newScale, newStep) {
  2300. this.scale = newScale;
  2301. if (newStep > 0) {
  2302. this.step = newStep;
  2303. }
  2304. this.autoScale = false;
  2305. };
  2306. /**
  2307. * Enable or disable autoscaling
  2308. * @param {boolean} enable If true, autoascaling is set true
  2309. */
  2310. TimeStep.prototype.setAutoScale = function (enable) {
  2311. this.autoScale = enable;
  2312. };
  2313. /**
  2314. * Automatically determine the scale that bests fits the provided minimum step
  2315. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2316. */
  2317. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2318. if (minimumStep == undefined) {
  2319. return;
  2320. }
  2321. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2322. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2323. var stepDay = (1000 * 60 * 60 * 24);
  2324. var stepHour = (1000 * 60 * 60);
  2325. var stepMinute = (1000 * 60);
  2326. var stepSecond = (1000);
  2327. var stepMillisecond= (1);
  2328. // find the smallest step that is larger than the provided minimumStep
  2329. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2330. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2331. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2332. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2333. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2334. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2335. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2336. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2337. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2338. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2339. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2340. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2341. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2342. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2343. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2344. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2345. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2346. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2347. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2348. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2349. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2350. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2351. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2352. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2353. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2354. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2355. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2356. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2357. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2358. };
  2359. /**
  2360. * Snap a date to a rounded value. The snap intervals are dependent on the
  2361. * current scale and step.
  2362. * @param {Date} date the date to be snapped
  2363. */
  2364. TimeStep.prototype.snap = function(date) {
  2365. if (this.scale == TimeStep.SCALE.YEAR) {
  2366. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2367. date.setFullYear(Math.round(year / this.step) * this.step);
  2368. date.setMonth(0);
  2369. date.setDate(0);
  2370. date.setHours(0);
  2371. date.setMinutes(0);
  2372. date.setSeconds(0);
  2373. date.setMilliseconds(0);
  2374. }
  2375. else if (this.scale == TimeStep.SCALE.MONTH) {
  2376. if (date.getDate() > 15) {
  2377. date.setDate(1);
  2378. date.setMonth(date.getMonth() + 1);
  2379. // important: first set Date to 1, after that change the month.
  2380. }
  2381. else {
  2382. date.setDate(1);
  2383. }
  2384. date.setHours(0);
  2385. date.setMinutes(0);
  2386. date.setSeconds(0);
  2387. date.setMilliseconds(0);
  2388. }
  2389. else if (this.scale == TimeStep.SCALE.DAY ||
  2390. this.scale == TimeStep.SCALE.WEEKDAY) {
  2391. //noinspection FallthroughInSwitchStatementJS
  2392. switch (this.step) {
  2393. case 5:
  2394. case 2:
  2395. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2396. default:
  2397. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2398. }
  2399. date.setMinutes(0);
  2400. date.setSeconds(0);
  2401. date.setMilliseconds(0);
  2402. }
  2403. else if (this.scale == TimeStep.SCALE.HOUR) {
  2404. switch (this.step) {
  2405. case 4:
  2406. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2407. default:
  2408. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2409. }
  2410. date.setSeconds(0);
  2411. date.setMilliseconds(0);
  2412. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2413. //noinspection FallthroughInSwitchStatementJS
  2414. switch (this.step) {
  2415. case 15:
  2416. case 10:
  2417. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2418. date.setSeconds(0);
  2419. break;
  2420. case 5:
  2421. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2422. default:
  2423. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2424. }
  2425. date.setMilliseconds(0);
  2426. }
  2427. else if (this.scale == TimeStep.SCALE.SECOND) {
  2428. //noinspection FallthroughInSwitchStatementJS
  2429. switch (this.step) {
  2430. case 15:
  2431. case 10:
  2432. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2433. date.setMilliseconds(0);
  2434. break;
  2435. case 5:
  2436. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2437. default:
  2438. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2439. }
  2440. }
  2441. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2442. var step = this.step > 5 ? this.step / 2 : 1;
  2443. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2444. }
  2445. };
  2446. /**
  2447. * Check if the current value is a major value (for example when the step
  2448. * is DAY, a major value is each first day of the MONTH)
  2449. * @return {boolean} true if current date is major, else false.
  2450. */
  2451. TimeStep.prototype.isMajor = function() {
  2452. switch (this.scale) {
  2453. case TimeStep.SCALE.MILLISECOND:
  2454. return (this.current.getMilliseconds() == 0);
  2455. case TimeStep.SCALE.SECOND:
  2456. return (this.current.getSeconds() == 0);
  2457. case TimeStep.SCALE.MINUTE:
  2458. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2459. // Note: this is no bug. Major label is equal for both minute and hour scale
  2460. case TimeStep.SCALE.HOUR:
  2461. return (this.current.getHours() == 0);
  2462. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2463. case TimeStep.SCALE.DAY:
  2464. return (this.current.getDate() == 1);
  2465. case TimeStep.SCALE.MONTH:
  2466. return (this.current.getMonth() == 0);
  2467. case TimeStep.SCALE.YEAR:
  2468. return false;
  2469. default:
  2470. return false;
  2471. }
  2472. };
  2473. /**
  2474. * Returns formatted text for the minor axislabel, depending on the current
  2475. * date and the scale. For example when scale is MINUTE, the current time is
  2476. * formatted as "hh:mm".
  2477. * @param {Date} [date] custom date. if not provided, current date is taken
  2478. */
  2479. TimeStep.prototype.getLabelMinor = function(date) {
  2480. if (date == undefined) {
  2481. date = this.current;
  2482. }
  2483. switch (this.scale) {
  2484. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2485. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2486. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2487. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2488. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2489. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2490. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2491. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2492. default: return '';
  2493. }
  2494. };
  2495. /**
  2496. * Returns formatted text for the major axis label, depending on the current
  2497. * date and the scale. For example when scale is MINUTE, the major scale is
  2498. * hours, and the hour will be formatted as "hh".
  2499. * @param {Date} [date] custom date. if not provided, current date is taken
  2500. */
  2501. TimeStep.prototype.getLabelMajor = function(date) {
  2502. if (date == undefined) {
  2503. date = this.current;
  2504. }
  2505. //noinspection FallthroughInSwitchStatementJS
  2506. switch (this.scale) {
  2507. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2508. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2509. case TimeStep.SCALE.MINUTE:
  2510. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2511. case TimeStep.SCALE.WEEKDAY:
  2512. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2513. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2514. case TimeStep.SCALE.YEAR: return '';
  2515. default: return '';
  2516. }
  2517. };
  2518. /**
  2519. * @constructor Stack
  2520. * Stacks items on top of each other.
  2521. * @param {ItemSet} parent
  2522. * @param {Object} [options]
  2523. */
  2524. function Stack (parent, options) {
  2525. this.parent = parent;
  2526. this.options = options || {};
  2527. this.defaultOptions = {
  2528. order: function (a, b) {
  2529. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2530. // Order: ranges over non-ranges, ranged ordered by width, and
  2531. // lastly ordered by start.
  2532. if (a instanceof ItemRange) {
  2533. if (b instanceof ItemRange) {
  2534. var aInt = (a.data.end - a.data.start);
  2535. var bInt = (b.data.end - b.data.start);
  2536. return (aInt - bInt) || (a.data.start - b.data.start);
  2537. }
  2538. else {
  2539. return -1;
  2540. }
  2541. }
  2542. else {
  2543. if (b instanceof ItemRange) {
  2544. return 1;
  2545. }
  2546. else {
  2547. return (a.data.start - b.data.start);
  2548. }
  2549. }
  2550. },
  2551. margin: {
  2552. item: 10
  2553. }
  2554. };
  2555. this.ordered = []; // ordered items
  2556. }
  2557. /**
  2558. * Set options for the stack
  2559. * @param {Object} options Available options:
  2560. * {ItemSet} parent
  2561. * {Number} margin
  2562. * {function} order Stacking order
  2563. */
  2564. Stack.prototype.setOptions = function setOptions (options) {
  2565. util.extend(this.options, options);
  2566. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2567. };
  2568. /**
  2569. * Stack the items such that they don't overlap. The items will have a minimal
  2570. * distance equal to options.margin.item.
  2571. */
  2572. Stack.prototype.update = function update() {
  2573. this._order();
  2574. this._stack();
  2575. };
  2576. /**
  2577. * Order the items. The items are ordered by width first, and by left position
  2578. * second.
  2579. * If a custom order function has been provided via the options, then this will
  2580. * be used.
  2581. * @private
  2582. */
  2583. Stack.prototype._order = function _order () {
  2584. var items = this.parent.items;
  2585. if (!items) {
  2586. throw new Error('Cannot stack items: parent does not contain items');
  2587. }
  2588. // TODO: store the sorted items, to have less work later on
  2589. var ordered = [];
  2590. var index = 0;
  2591. // items is a map (no array)
  2592. util.forEach(items, function (item) {
  2593. if (item.visible) {
  2594. ordered[index] = item;
  2595. index++;
  2596. }
  2597. });
  2598. //if a customer stack order function exists, use it.
  2599. var order = this.options.order || this.defaultOptions.order;
  2600. if (!(typeof order === 'function')) {
  2601. throw new Error('Option order must be a function');
  2602. }
  2603. ordered.sort(order);
  2604. this.ordered = ordered;
  2605. };
  2606. /**
  2607. * Adjust vertical positions of the events such that they don't overlap each
  2608. * other.
  2609. * @private
  2610. */
  2611. Stack.prototype._stack = function _stack () {
  2612. var i,
  2613. iMax,
  2614. ordered = this.ordered,
  2615. options = this.options,
  2616. orientation = options.orientation || this.defaultOptions.orientation,
  2617. axisOnTop = (orientation == 'top'),
  2618. margin;
  2619. if (options.margin && options.margin.item !== undefined) {
  2620. margin = options.margin.item;
  2621. }
  2622. else {
  2623. margin = this.defaultOptions.margin.item
  2624. }
  2625. // calculate new, non-overlapping positions
  2626. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2627. var item = ordered[i];
  2628. var collidingItem = null;
  2629. do {
  2630. // TODO: optimize checking for overlap. when there is a gap without items,
  2631. // you only need to check for items from the next item on, not from zero
  2632. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2633. if (collidingItem != null) {
  2634. // There is a collision. Reposition the event above the colliding element
  2635. if (axisOnTop) {
  2636. item.top = collidingItem.top + collidingItem.height + margin;
  2637. }
  2638. else {
  2639. item.top = collidingItem.top - item.height - margin;
  2640. }
  2641. }
  2642. } while (collidingItem);
  2643. }
  2644. };
  2645. /**
  2646. * Check if the destiny position of given item overlaps with any
  2647. * of the other items from index itemStart to itemEnd.
  2648. * @param {Array} items Array with items
  2649. * @param {int} itemIndex Number of the item to be checked for overlap
  2650. * @param {int} itemStart First item to be checked.
  2651. * @param {int} itemEnd Last item to be checked.
  2652. * @return {Object | null} colliding item, or undefined when no collisions
  2653. * @param {Number} margin A minimum required margin.
  2654. * If margin is provided, the two items will be
  2655. * marked colliding when they overlap or
  2656. * when the margin between the two is smaller than
  2657. * the requested margin.
  2658. */
  2659. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2660. itemStart, itemEnd, margin) {
  2661. var collision = this.collision;
  2662. // we loop from end to start, as we suppose that the chance of a
  2663. // collision is larger for items at the end, so check these first.
  2664. var a = items[itemIndex];
  2665. for (var i = itemEnd; i >= itemStart; i--) {
  2666. var b = items[i];
  2667. if (collision(a, b, margin)) {
  2668. if (i != itemIndex) {
  2669. return b;
  2670. }
  2671. }
  2672. }
  2673. return null;
  2674. };
  2675. /**
  2676. * Test if the two provided items collide
  2677. * The items must have parameters left, width, top, and height.
  2678. * @param {Component} a The first item
  2679. * @param {Component} b The second item
  2680. * @param {Number} margin A minimum required margin.
  2681. * If margin is provided, the two items will be
  2682. * marked colliding when they overlap or
  2683. * when the margin between the two is smaller than
  2684. * the requested margin.
  2685. * @return {boolean} true if a and b collide, else false
  2686. */
  2687. Stack.prototype.collision = function collision (a, b, margin) {
  2688. return ((a.left - margin) < (b.left + b.width) &&
  2689. (a.left + a.width + margin) > b.left &&
  2690. (a.top - margin) < (b.top + b.height) &&
  2691. (a.top + a.height + margin) > b.top);
  2692. };
  2693. /**
  2694. * @constructor Range
  2695. * A Range controls a numeric range with a start and end value.
  2696. * The Range adjusts the range based on mouse events or programmatic changes,
  2697. * and triggers events when the range is changing or has been changed.
  2698. * @param {Object} [options] See description at Range.setOptions
  2699. * @extends Controller
  2700. */
  2701. function Range(options) {
  2702. this.id = util.randomUUID();
  2703. this.start = 0; // Number
  2704. this.end = 0; // Number
  2705. // this.options = options || {}; // TODO
  2706. this.options = {
  2707. min: null,
  2708. max: null,
  2709. zoomMin: null,
  2710. zoomMax: null
  2711. };
  2712. this.listeners = [];
  2713. this.setOptions(options);
  2714. }
  2715. /**
  2716. * Set options for the range controller
  2717. * @param {Object} options Available options:
  2718. * {Number} start Set start value of the range
  2719. * {Number} end Set end value of the range
  2720. * {Number} min Minimum value for start
  2721. * {Number} max Maximum value for end
  2722. * {Number} zoomMin Set a minimum value for
  2723. * (end - start).
  2724. * {Number} zoomMax Set a maximum value for
  2725. * (end - start).
  2726. */
  2727. Range.prototype.setOptions = function (options) {
  2728. util.extend(this.options, options);
  2729. if (options.start != null || options.end != null) {
  2730. this.setRange(options.start, options.end);
  2731. }
  2732. };
  2733. /**
  2734. * Add listeners for mouse and touch events to the component
  2735. * @param {Component} component
  2736. * @param {String} event Available events: 'move', 'zoom'
  2737. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2738. */
  2739. Range.prototype.subscribe = function (component, event, direction) {
  2740. var me = this;
  2741. var listener;
  2742. if (direction != 'horizontal' && direction != 'vertical') {
  2743. throw new TypeError('Unknown direction "' + direction + '". ' +
  2744. 'Choose "horizontal" or "vertical".');
  2745. }
  2746. //noinspection FallthroughInSwitchStatementJS
  2747. if (event == 'move') {
  2748. listener = {
  2749. component: component,
  2750. event: event,
  2751. direction: direction,
  2752. callback: function (event) {
  2753. me._onMouseDown(event, listener);
  2754. },
  2755. params: {}
  2756. };
  2757. component.on('mousedown', listener.callback);
  2758. me.listeners.push(listener);
  2759. }
  2760. else if (event == 'zoom') {
  2761. listener = {
  2762. component: component,
  2763. event: event,
  2764. direction: direction,
  2765. callback: function (event) {
  2766. me._onMouseWheel(event, listener);
  2767. },
  2768. params: {}
  2769. };
  2770. component.on('mousewheel', listener.callback);
  2771. me.listeners.push(listener);
  2772. }
  2773. else {
  2774. throw new TypeError('Unknown event "' + event + '". ' +
  2775. 'Choose "move" or "zoom".');
  2776. }
  2777. };
  2778. /**
  2779. * Event handler
  2780. * @param {String} event name of the event, for example 'click', 'mousemove'
  2781. * @param {function} callback callback handler, invoked with the raw HTML Event
  2782. * as parameter.
  2783. */
  2784. Range.prototype.on = function (event, callback) {
  2785. events.addListener(this, event, callback);
  2786. };
  2787. /**
  2788. * Trigger an event
  2789. * @param {String} event name of the event, available events: 'rangechange',
  2790. * 'rangechanged'
  2791. * @private
  2792. */
  2793. Range.prototype._trigger = function (event) {
  2794. events.trigger(this, event, {
  2795. start: this.start,
  2796. end: this.end
  2797. });
  2798. };
  2799. /**
  2800. * Set a new start and end range
  2801. * @param {Number} start
  2802. * @param {Number} end
  2803. */
  2804. Range.prototype.setRange = function(start, end) {
  2805. var changed = this._applyRange(start, end);
  2806. if (changed) {
  2807. this._trigger('rangechange');
  2808. this._trigger('rangechanged');
  2809. }
  2810. };
  2811. /**
  2812. * Set a new start and end range. This method is the same as setRange, but
  2813. * does not trigger a range change and range changed event, and it returns
  2814. * true when the range is changed
  2815. * @param {Number} start
  2816. * @param {Number} end
  2817. * @return {Boolean} changed
  2818. * @private
  2819. */
  2820. Range.prototype._applyRange = function(start, end) {
  2821. var newStart = (start != null) ? util.convert(start, 'Number') : this.start;
  2822. var newEnd = (end != null) ? util.convert(end, 'Number') : this.end;
  2823. var diff;
  2824. // check for valid number
  2825. if (isNaN(newStart)) {
  2826. throw new Error('Invalid start "' + start + '"');
  2827. }
  2828. if (isNaN(newEnd)) {
  2829. throw new Error('Invalid end "' + end + '"');
  2830. }
  2831. // prevent start < end
  2832. if (newEnd < newStart) {
  2833. newEnd = newStart;
  2834. }
  2835. // prevent start < min
  2836. if (this.options.min != null) {
  2837. var min = this.options.min.valueOf();
  2838. if (newStart < min) {
  2839. diff = (min - newStart);
  2840. newStart += diff;
  2841. newEnd += diff;
  2842. }
  2843. }
  2844. // prevent end > max
  2845. if (this.options.max != null) {
  2846. var max = this.options.max.valueOf();
  2847. if (newEnd > max) {
  2848. diff = (newEnd - max);
  2849. newStart -= diff;
  2850. newEnd -= diff;
  2851. }
  2852. }
  2853. // prevent (end-start) > zoomMin
  2854. if (this.options.zoomMin != null) {
  2855. var zoomMin = this.options.zoomMin.valueOf();
  2856. if (zoomMin < 0) {
  2857. zoomMin = 0;
  2858. }
  2859. if ((newEnd - newStart) < zoomMin) {
  2860. if ((this.end - this.start) > zoomMin) {
  2861. // zoom to the minimum
  2862. diff = (zoomMin - (newEnd - newStart));
  2863. newStart -= diff / 2;
  2864. newEnd += diff / 2;
  2865. }
  2866. else {
  2867. // ingore this action, we are already zoomed to the minimum
  2868. newStart = this.start;
  2869. newEnd = this.end;
  2870. }
  2871. }
  2872. }
  2873. // prevent (end-start) > zoomMin
  2874. if (this.options.zoomMax != null) {
  2875. var zoomMax = this.options.zoomMax.valueOf();
  2876. if (zoomMax < 0) {
  2877. zoomMax = 0;
  2878. }
  2879. if ((newEnd - newStart) > zoomMax) {
  2880. if ((this.end - this.start) < zoomMax) {
  2881. // zoom to the maximum
  2882. diff = ((newEnd - newStart) - zoomMax);
  2883. newStart += diff / 2;
  2884. newEnd -= diff / 2;
  2885. }
  2886. else {
  2887. // ingore this action, we are already zoomed to the maximum
  2888. newStart = this.start;
  2889. newEnd = this.end;
  2890. }
  2891. }
  2892. }
  2893. var changed = (this.start != newStart || this.end != newEnd);
  2894. this.start = newStart;
  2895. this.end = newEnd;
  2896. return changed;
  2897. };
  2898. /**
  2899. * Retrieve the current range.
  2900. * @return {Object} An object with start and end properties
  2901. */
  2902. Range.prototype.getRange = function() {
  2903. return {
  2904. start: this.start,
  2905. end: this.end
  2906. };
  2907. };
  2908. /**
  2909. * Calculate the conversion offset and factor for current range, based on
  2910. * the provided width
  2911. * @param {Number} width
  2912. * @returns {{offset: number, factor: number}} conversion
  2913. */
  2914. Range.prototype.conversion = function (width) {
  2915. var start = this.start;
  2916. var end = this.end;
  2917. return Range.conversion(this.start, this.end, width);
  2918. };
  2919. /**
  2920. * Static method to calculate the conversion offset and factor for a range,
  2921. * based on the provided start, end, and width
  2922. * @param {Number} start
  2923. * @param {Number} end
  2924. * @param {Number} width
  2925. * @returns {{offset: number, factor: number}} conversion
  2926. */
  2927. Range.conversion = function (start, end, width) {
  2928. if (width != 0 && (end - start != 0)) {
  2929. return {
  2930. offset: start,
  2931. factor: width / (end - start)
  2932. }
  2933. }
  2934. else {
  2935. return {
  2936. offset: 0,
  2937. factor: 1
  2938. };
  2939. }
  2940. };
  2941. /**
  2942. * Start moving horizontally or vertically
  2943. * @param {Event} event
  2944. * @param {Object} listener Listener containing the component and params
  2945. * @private
  2946. */
  2947. Range.prototype._onMouseDown = function(event, listener) {
  2948. event = event || window.event;
  2949. var params = listener.params;
  2950. // only react on left mouse button down
  2951. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  2952. if (!leftButtonDown) {
  2953. return;
  2954. }
  2955. // get mouse position
  2956. params.mouseX = util.getPageX(event);
  2957. params.mouseY = util.getPageY(event);
  2958. params.previousLeft = 0;
  2959. params.previousOffset = 0;
  2960. params.moved = false;
  2961. params.start = this.start;
  2962. params.end = this.end;
  2963. var frame = listener.component.frame;
  2964. if (frame) {
  2965. frame.style.cursor = 'move';
  2966. }
  2967. // add event listeners to handle moving the contents
  2968. // we store the function onmousemove and onmouseup in the timeaxis,
  2969. // so we can remove the eventlisteners lateron in the function onmouseup
  2970. var me = this;
  2971. if (!params.onMouseMove) {
  2972. params.onMouseMove = function (event) {
  2973. me._onMouseMove(event, listener);
  2974. };
  2975. util.addEventListener(document, "mousemove", params.onMouseMove);
  2976. }
  2977. if (!params.onMouseUp) {
  2978. params.onMouseUp = function (event) {
  2979. me._onMouseUp(event, listener);
  2980. };
  2981. util.addEventListener(document, "mouseup", params.onMouseUp);
  2982. }
  2983. util.preventDefault(event);
  2984. };
  2985. /**
  2986. * Perform moving operating.
  2987. * This function activated from within the funcion TimeAxis._onMouseDown().
  2988. * @param {Event} event
  2989. * @param {Object} listener
  2990. * @private
  2991. */
  2992. Range.prototype._onMouseMove = function (event, listener) {
  2993. event = event || window.event;
  2994. var params = listener.params;
  2995. // calculate change in mouse position
  2996. var mouseX = util.getPageX(event);
  2997. var mouseY = util.getPageY(event);
  2998. if (params.mouseX == undefined) {
  2999. params.mouseX = mouseX;
  3000. }
  3001. if (params.mouseY == undefined) {
  3002. params.mouseY = mouseY;
  3003. }
  3004. var diffX = mouseX - params.mouseX;
  3005. var diffY = mouseY - params.mouseY;
  3006. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  3007. // if mouse movement is big enough, register it as a "moved" event
  3008. if (Math.abs(diff) >= 1) {
  3009. params.moved = true;
  3010. }
  3011. var interval = (params.end - params.start);
  3012. var width = (listener.direction == 'horizontal') ?
  3013. listener.component.width : listener.component.height;
  3014. var diffRange = -diff / width * interval;
  3015. this._applyRange(params.start + diffRange, params.end + diffRange);
  3016. // fire a rangechange event
  3017. this._trigger('rangechange');
  3018. util.preventDefault(event);
  3019. };
  3020. /**
  3021. * Stop moving operating.
  3022. * This function activated from within the function Range._onMouseDown().
  3023. * @param {event} event
  3024. * @param {Object} listener
  3025. * @private
  3026. */
  3027. Range.prototype._onMouseUp = function (event, listener) {
  3028. event = event || window.event;
  3029. var params = listener.params;
  3030. if (listener.component.frame) {
  3031. listener.component.frame.style.cursor = 'auto';
  3032. }
  3033. // remove event listeners here, important for Safari
  3034. if (params.onMouseMove) {
  3035. util.removeEventListener(document, "mousemove", params.onMouseMove);
  3036. params.onMouseMove = null;
  3037. }
  3038. if (params.onMouseUp) {
  3039. util.removeEventListener(document, "mouseup", params.onMouseUp);
  3040. params.onMouseUp = null;
  3041. }
  3042. //util.preventDefault(event);
  3043. if (params.moved) {
  3044. // fire a rangechanged event
  3045. this._trigger('rangechanged');
  3046. }
  3047. };
  3048. /**
  3049. * Event handler for mouse wheel event, used to zoom
  3050. * Code from http://adomas.org/javascript-mouse-wheel/
  3051. * @param {Event} event
  3052. * @param {Object} listener
  3053. * @private
  3054. */
  3055. Range.prototype._onMouseWheel = function(event, listener) {
  3056. event = event || window.event;
  3057. // retrieve delta
  3058. var delta = 0;
  3059. if (event.wheelDelta) { /* IE/Opera. */
  3060. delta = event.wheelDelta / 120;
  3061. } else if (event.detail) { /* Mozilla case. */
  3062. // In Mozilla, sign of delta is different than in IE.
  3063. // Also, delta is multiple of 3.
  3064. delta = -event.detail / 3;
  3065. }
  3066. // If delta is nonzero, handle it.
  3067. // Basically, delta is now positive if wheel was scrolled up,
  3068. // and negative, if wheel was scrolled down.
  3069. if (delta) {
  3070. var me = this;
  3071. var zoom = function () {
  3072. // perform the zoom action. Delta is normally 1 or -1
  3073. var zoomFactor = delta / 5.0;
  3074. var zoomAround = null;
  3075. var frame = listener.component.frame;
  3076. if (frame) {
  3077. var size, conversion;
  3078. if (listener.direction == 'horizontal') {
  3079. size = listener.component.width;
  3080. conversion = me.conversion(size);
  3081. var frameLeft = util.getAbsoluteLeft(frame);
  3082. var mouseX = util.getPageX(event);
  3083. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  3084. }
  3085. else {
  3086. size = listener.component.height;
  3087. conversion = me.conversion(size);
  3088. var frameTop = util.getAbsoluteTop(frame);
  3089. var mouseY = util.getPageY(event);
  3090. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  3091. }
  3092. }
  3093. me.zoom(zoomFactor, zoomAround);
  3094. };
  3095. zoom();
  3096. }
  3097. // Prevent default actions caused by mouse wheel.
  3098. // That might be ugly, but we handle scrolls somehow
  3099. // anyway, so don't bother here...
  3100. util.preventDefault(event);
  3101. };
  3102. /**
  3103. * Zoom the range the given zoomfactor in or out. Start and end date will
  3104. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3105. * date around which to zoom.
  3106. * For example, try zoomfactor = 0.1 or -0.1
  3107. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  3108. * negative value will zoom out
  3109. * @param {Number} zoomAround Value around which will be zoomed. Optional
  3110. */
  3111. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  3112. // if zoomAroundDate is not provided, take it half between start Date and end Date
  3113. if (zoomAround == null) {
  3114. zoomAround = (this.start + this.end) / 2;
  3115. }
  3116. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  3117. // result in a start>=end )
  3118. if (zoomFactor >= 1) {
  3119. zoomFactor = 0.9;
  3120. }
  3121. if (zoomFactor <= -1) {
  3122. zoomFactor = -0.9;
  3123. }
  3124. // adjust a negative factor such that zooming in with 0.1 equals zooming
  3125. // out with a factor -0.1
  3126. if (zoomFactor < 0) {
  3127. zoomFactor = zoomFactor / (1 + zoomFactor);
  3128. }
  3129. // zoom start and end relative to the zoomAround value
  3130. var startDiff = (this.start - zoomAround);
  3131. var endDiff = (this.end - zoomAround);
  3132. // calculate new start and end
  3133. var newStart = this.start - startDiff * zoomFactor;
  3134. var newEnd = this.end - endDiff * zoomFactor;
  3135. this.setRange(newStart, newEnd);
  3136. };
  3137. /**
  3138. * Move the range with a given factor to the left or right. Start and end
  3139. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  3140. * @param {Number} moveFactor Moving amount. Positive value will move right,
  3141. * negative value will move left
  3142. */
  3143. Range.prototype.move = function(moveFactor) {
  3144. // zoom start Date and end Date relative to the zoomAroundDate
  3145. var diff = (this.end - this.start);
  3146. // apply new values
  3147. var newStart = this.start + diff * moveFactor;
  3148. var newEnd = this.end + diff * moveFactor;
  3149. // TODO: reckon with min and max range
  3150. this.start = newStart;
  3151. this.end = newEnd;
  3152. };
  3153. /**
  3154. * Move the range to a new center point
  3155. * @param {Number} moveTo New center point of the range
  3156. */
  3157. Range.prototype.moveTo = function(moveTo) {
  3158. var center = (this.start + this.end) / 2;
  3159. var diff = center - moveTo;
  3160. // calculate new start and end
  3161. var newStart = this.start - diff;
  3162. var newEnd = this.end - diff;
  3163. this.setRange(newStart, newEnd);
  3164. }
  3165. /**
  3166. * @constructor Controller
  3167. *
  3168. * A Controller controls the reflows and repaints of all visual components
  3169. */
  3170. function Controller () {
  3171. this.id = util.randomUUID();
  3172. this.components = {};
  3173. this.repaintTimer = undefined;
  3174. this.reflowTimer = undefined;
  3175. }
  3176. /**
  3177. * Add a component to the controller
  3178. * @param {Component} component
  3179. */
  3180. Controller.prototype.add = function add(component) {
  3181. // validate the component
  3182. if (component.id == undefined) {
  3183. throw new Error('Component has no field id');
  3184. }
  3185. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3186. throw new TypeError('Component must be an instance of ' +
  3187. 'prototype Component or Controller');
  3188. }
  3189. // add the component
  3190. component.controller = this;
  3191. this.components[component.id] = component;
  3192. };
  3193. /**
  3194. * Remove a component from the controller
  3195. * @param {Component | String} component
  3196. */
  3197. Controller.prototype.remove = function remove(component) {
  3198. var id;
  3199. for (id in this.components) {
  3200. if (this.components.hasOwnProperty(id)) {
  3201. if (id == component || this.components[id] == component) {
  3202. break;
  3203. }
  3204. }
  3205. }
  3206. if (id) {
  3207. delete this.components[id];
  3208. }
  3209. };
  3210. /**
  3211. * Request a reflow. The controller will schedule a reflow
  3212. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3213. * is false.
  3214. */
  3215. Controller.prototype.requestReflow = function requestReflow(force) {
  3216. if (force) {
  3217. this.reflow();
  3218. }
  3219. else {
  3220. if (!this.reflowTimer) {
  3221. var me = this;
  3222. this.reflowTimer = setTimeout(function () {
  3223. me.reflowTimer = undefined;
  3224. me.reflow();
  3225. }, 0);
  3226. }
  3227. }
  3228. };
  3229. /**
  3230. * Request a repaint. The controller will schedule a repaint
  3231. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3232. * is false.
  3233. */
  3234. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3235. if (force) {
  3236. this.repaint();
  3237. }
  3238. else {
  3239. if (!this.repaintTimer) {
  3240. var me = this;
  3241. this.repaintTimer = setTimeout(function () {
  3242. me.repaintTimer = undefined;
  3243. me.repaint();
  3244. }, 0);
  3245. }
  3246. }
  3247. };
  3248. /**
  3249. * Repaint all components
  3250. */
  3251. Controller.prototype.repaint = function repaint() {
  3252. var changed = false;
  3253. // cancel any running repaint request
  3254. if (this.repaintTimer) {
  3255. clearTimeout(this.repaintTimer);
  3256. this.repaintTimer = undefined;
  3257. }
  3258. var done = {};
  3259. function repaint(component, id) {
  3260. if (!(id in done)) {
  3261. // first repaint the components on which this component is dependent
  3262. if (component.depends) {
  3263. component.depends.forEach(function (dep) {
  3264. repaint(dep, dep.id);
  3265. });
  3266. }
  3267. if (component.parent) {
  3268. repaint(component.parent, component.parent.id);
  3269. }
  3270. // repaint the component itself and mark as done
  3271. changed = component.repaint() || changed;
  3272. done[id] = true;
  3273. }
  3274. }
  3275. util.forEach(this.components, repaint);
  3276. // immediately reflow when needed
  3277. if (changed) {
  3278. this.reflow();
  3279. }
  3280. // TODO: limit the number of nested reflows/repaints, prevent loop
  3281. };
  3282. /**
  3283. * Reflow all components
  3284. */
  3285. Controller.prototype.reflow = function reflow() {
  3286. var resized = false;
  3287. // cancel any running repaint request
  3288. if (this.reflowTimer) {
  3289. clearTimeout(this.reflowTimer);
  3290. this.reflowTimer = undefined;
  3291. }
  3292. var done = {};
  3293. function reflow(component, id) {
  3294. if (!(id in done)) {
  3295. // first reflow the components on which this component is dependent
  3296. if (component.depends) {
  3297. component.depends.forEach(function (dep) {
  3298. reflow(dep, dep.id);
  3299. });
  3300. }
  3301. if (component.parent) {
  3302. reflow(component.parent, component.parent.id);
  3303. }
  3304. // reflow the component itself and mark as done
  3305. resized = component.reflow() || resized;
  3306. done[id] = true;
  3307. }
  3308. }
  3309. util.forEach(this.components, reflow);
  3310. // immediately repaint when needed
  3311. if (resized) {
  3312. this.repaint();
  3313. }
  3314. // TODO: limit the number of nested reflows/repaints, prevent loop
  3315. };
  3316. /**
  3317. * Prototype for visual components
  3318. */
  3319. function Component () {
  3320. this.id = null;
  3321. this.parent = null;
  3322. this.depends = null;
  3323. this.controller = null;
  3324. this.options = null;
  3325. this.frame = null; // main DOM element
  3326. this.top = 0;
  3327. this.left = 0;
  3328. this.width = 0;
  3329. this.height = 0;
  3330. }
  3331. /**
  3332. * Set parameters for the frame. Parameters will be merged in current parameter
  3333. * set.
  3334. * @param {Object} options Available parameters:
  3335. * {String | function} [className]
  3336. * {EventBus} [eventBus]
  3337. * {String | Number | function} [left]
  3338. * {String | Number | function} [top]
  3339. * {String | Number | function} [width]
  3340. * {String | Number | function} [height]
  3341. */
  3342. Component.prototype.setOptions = function setOptions(options) {
  3343. if (options) {
  3344. util.extend(this.options, options);
  3345. if (this.controller) {
  3346. this.requestRepaint();
  3347. this.requestReflow();
  3348. }
  3349. }
  3350. };
  3351. /**
  3352. * Get an option value by name
  3353. * The function will first check this.options object, and else will check
  3354. * this.defaultOptions.
  3355. * @param {String} name
  3356. * @return {*} value
  3357. */
  3358. Component.prototype.getOption = function getOption(name) {
  3359. var value;
  3360. if (this.options) {
  3361. value = this.options[name];
  3362. }
  3363. if (value === undefined && this.defaultOptions) {
  3364. value = this.defaultOptions[name];
  3365. }
  3366. return value;
  3367. };
  3368. /**
  3369. * Get the container element of the component, which can be used by a child to
  3370. * add its own widgets. Not all components do have a container for childs, in
  3371. * that case null is returned.
  3372. * @returns {HTMLElement | null} container
  3373. */
  3374. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3375. Component.prototype.getContainer = function getContainer() {
  3376. // should be implemented by the component
  3377. return null;
  3378. };
  3379. /**
  3380. * Get the frame element of the component, the outer HTML DOM element.
  3381. * @returns {HTMLElement | null} frame
  3382. */
  3383. Component.prototype.getFrame = function getFrame() {
  3384. return this.frame;
  3385. };
  3386. /**
  3387. * Repaint the component
  3388. * @return {Boolean} changed
  3389. */
  3390. Component.prototype.repaint = function repaint() {
  3391. // should be implemented by the component
  3392. return false;
  3393. };
  3394. /**
  3395. * Reflow the component
  3396. * @return {Boolean} resized
  3397. */
  3398. Component.prototype.reflow = function reflow() {
  3399. // should be implemented by the component
  3400. return false;
  3401. };
  3402. /**
  3403. * Hide the component from the DOM
  3404. * @return {Boolean} changed
  3405. */
  3406. Component.prototype.hide = function hide() {
  3407. if (this.frame && this.frame.parentNode) {
  3408. this.frame.parentNode.removeChild(this.frame);
  3409. return true;
  3410. }
  3411. else {
  3412. return false;
  3413. }
  3414. };
  3415. /**
  3416. * Show the component in the DOM (when not already visible).
  3417. * A repaint will be executed when the component is not visible
  3418. * @return {Boolean} changed
  3419. */
  3420. Component.prototype.show = function show() {
  3421. if (!this.frame || !this.frame.parentNode) {
  3422. return this.repaint();
  3423. }
  3424. else {
  3425. return false;
  3426. }
  3427. };
  3428. /**
  3429. * Request a repaint. The controller will schedule a repaint
  3430. */
  3431. Component.prototype.requestRepaint = function requestRepaint() {
  3432. if (this.controller) {
  3433. this.controller.requestRepaint();
  3434. }
  3435. else {
  3436. throw new Error('Cannot request a repaint: no controller configured');
  3437. // TODO: just do a repaint when no parent is configured?
  3438. }
  3439. };
  3440. /**
  3441. * Request a reflow. The controller will schedule a reflow
  3442. */
  3443. Component.prototype.requestReflow = function requestReflow() {
  3444. if (this.controller) {
  3445. this.controller.requestReflow();
  3446. }
  3447. else {
  3448. throw new Error('Cannot request a reflow: no controller configured');
  3449. // TODO: just do a reflow when no parent is configured?
  3450. }
  3451. };
  3452. /**
  3453. * A panel can contain components
  3454. * @param {Component} [parent]
  3455. * @param {Component[]} [depends] Components on which this components depends
  3456. * (except for the parent)
  3457. * @param {Object} [options] Available parameters:
  3458. * {String | Number | function} [left]
  3459. * {String | Number | function} [top]
  3460. * {String | Number | function} [width]
  3461. * {String | Number | function} [height]
  3462. * {String | function} [className]
  3463. * @constructor Panel
  3464. * @extends Component
  3465. */
  3466. function Panel(parent, depends, options) {
  3467. this.id = util.randomUUID();
  3468. this.parent = parent;
  3469. this.depends = depends;
  3470. this.options = options || {};
  3471. }
  3472. Panel.prototype = new Component();
  3473. /**
  3474. * Set options. Will extend the current options.
  3475. * @param {Object} [options] Available parameters:
  3476. * {String | function} [className]
  3477. * {String | Number | function} [left]
  3478. * {String | Number | function} [top]
  3479. * {String | Number | function} [width]
  3480. * {String | Number | function} [height]
  3481. */
  3482. Panel.prototype.setOptions = Component.prototype.setOptions;
  3483. /**
  3484. * Get the container element of the panel, which can be used by a child to
  3485. * add its own widgets.
  3486. * @returns {HTMLElement} container
  3487. */
  3488. Panel.prototype.getContainer = function () {
  3489. return this.frame;
  3490. };
  3491. /**
  3492. * Repaint the component
  3493. * @return {Boolean} changed
  3494. */
  3495. Panel.prototype.repaint = function () {
  3496. var changed = 0,
  3497. update = util.updateProperty,
  3498. asSize = util.option.asSize,
  3499. options = this.options,
  3500. frame = this.frame;
  3501. if (!frame) {
  3502. frame = document.createElement('div');
  3503. frame.className = 'panel';
  3504. var className = options.className;
  3505. if (className) {
  3506. if (typeof className == 'function') {
  3507. util.addClassName(frame, String(className()));
  3508. }
  3509. else {
  3510. util.addClassName(frame, String(className));
  3511. }
  3512. }
  3513. this.frame = frame;
  3514. changed += 1;
  3515. }
  3516. if (!frame.parentNode) {
  3517. if (!this.parent) {
  3518. throw new Error('Cannot repaint panel: no parent attached');
  3519. }
  3520. var parentContainer = this.parent.getContainer();
  3521. if (!parentContainer) {
  3522. throw new Error('Cannot repaint panel: parent has no container element');
  3523. }
  3524. parentContainer.appendChild(frame);
  3525. changed += 1;
  3526. }
  3527. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3528. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3529. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3530. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3531. return (changed > 0);
  3532. };
  3533. /**
  3534. * Reflow the component
  3535. * @return {Boolean} resized
  3536. */
  3537. Panel.prototype.reflow = function () {
  3538. var changed = 0,
  3539. update = util.updateProperty,
  3540. frame = this.frame;
  3541. if (frame) {
  3542. changed += update(this, 'top', frame.offsetTop);
  3543. changed += update(this, 'left', frame.offsetLeft);
  3544. changed += update(this, 'width', frame.offsetWidth);
  3545. changed += update(this, 'height', frame.offsetHeight);
  3546. }
  3547. else {
  3548. changed += 1;
  3549. }
  3550. return (changed > 0);
  3551. };
  3552. /**
  3553. * A root panel can hold components. The root panel must be initialized with
  3554. * a DOM element as container.
  3555. * @param {HTMLElement} container
  3556. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3557. * @constructor RootPanel
  3558. * @extends Panel
  3559. */
  3560. function RootPanel(container, options) {
  3561. this.id = util.randomUUID();
  3562. this.container = container;
  3563. this.options = options || {};
  3564. this.defaultOptions = {
  3565. autoResize: true
  3566. };
  3567. this.listeners = {}; // event listeners
  3568. }
  3569. RootPanel.prototype = new Panel();
  3570. /**
  3571. * Set options. Will extend the current options.
  3572. * @param {Object} [options] Available parameters:
  3573. * {String | function} [className]
  3574. * {String | Number | function} [left]
  3575. * {String | Number | function} [top]
  3576. * {String | Number | function} [width]
  3577. * {String | Number | function} [height]
  3578. * {Boolean | function} [autoResize]
  3579. */
  3580. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3581. /**
  3582. * Repaint the component
  3583. * @return {Boolean} changed
  3584. */
  3585. RootPanel.prototype.repaint = function () {
  3586. var changed = 0,
  3587. update = util.updateProperty,
  3588. asSize = util.option.asSize,
  3589. options = this.options,
  3590. frame = this.frame;
  3591. if (!frame) {
  3592. frame = document.createElement('div');
  3593. frame.className = 'vis timeline rootpanel';
  3594. var className = options.className;
  3595. if (className) {
  3596. util.addClassName(frame, util.option.asString(className));
  3597. }
  3598. this.frame = frame;
  3599. changed += 1;
  3600. }
  3601. if (!frame.parentNode) {
  3602. if (!this.container) {
  3603. throw new Error('Cannot repaint root panel: no container attached');
  3604. }
  3605. this.container.appendChild(frame);
  3606. changed += 1;
  3607. }
  3608. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3609. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3610. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3611. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3612. this._updateEventEmitters();
  3613. this._updateWatch();
  3614. return (changed > 0);
  3615. };
  3616. /**
  3617. * Reflow the component
  3618. * @return {Boolean} resized
  3619. */
  3620. RootPanel.prototype.reflow = function () {
  3621. var changed = 0,
  3622. update = util.updateProperty,
  3623. frame = this.frame;
  3624. if (frame) {
  3625. changed += update(this, 'top', frame.offsetTop);
  3626. changed += update(this, 'left', frame.offsetLeft);
  3627. changed += update(this, 'width', frame.offsetWidth);
  3628. changed += update(this, 'height', frame.offsetHeight);
  3629. }
  3630. else {
  3631. changed += 1;
  3632. }
  3633. return (changed > 0);
  3634. };
  3635. /**
  3636. * Update watching for resize, depending on the current option
  3637. * @private
  3638. */
  3639. RootPanel.prototype._updateWatch = function () {
  3640. var autoResize = this.getOption('autoResize');
  3641. if (autoResize) {
  3642. this._watch();
  3643. }
  3644. else {
  3645. this._unwatch();
  3646. }
  3647. };
  3648. /**
  3649. * Watch for changes in the size of the frame. On resize, the Panel will
  3650. * automatically redraw itself.
  3651. * @private
  3652. */
  3653. RootPanel.prototype._watch = function () {
  3654. var me = this;
  3655. this._unwatch();
  3656. var checkSize = function () {
  3657. var autoResize = me.getOption('autoResize');
  3658. if (!autoResize) {
  3659. // stop watching when the option autoResize is changed to false
  3660. me._unwatch();
  3661. return;
  3662. }
  3663. if (me.frame) {
  3664. // check whether the frame is resized
  3665. if ((me.frame.clientWidth != me.width) ||
  3666. (me.frame.clientHeight != me.height)) {
  3667. me.requestReflow();
  3668. }
  3669. }
  3670. };
  3671. // TODO: automatically cleanup the event listener when the frame is deleted
  3672. util.addEventListener(window, 'resize', checkSize);
  3673. this.watchTimer = setInterval(checkSize, 1000);
  3674. };
  3675. /**
  3676. * Stop watching for a resize of the frame.
  3677. * @private
  3678. */
  3679. RootPanel.prototype._unwatch = function () {
  3680. if (this.watchTimer) {
  3681. clearInterval(this.watchTimer);
  3682. this.watchTimer = undefined;
  3683. }
  3684. // TODO: remove event listener on window.resize
  3685. };
  3686. /**
  3687. * Event handler
  3688. * @param {String} event name of the event, for example 'click', 'mousemove'
  3689. * @param {function} callback callback handler, invoked with the raw HTML Event
  3690. * as parameter.
  3691. */
  3692. RootPanel.prototype.on = function (event, callback) {
  3693. // register the listener at this component
  3694. var arr = this.listeners[event];
  3695. if (!arr) {
  3696. arr = [];
  3697. this.listeners[event] = arr;
  3698. }
  3699. arr.push(callback);
  3700. this._updateEventEmitters();
  3701. };
  3702. /**
  3703. * Update the event listeners for all event emitters
  3704. * @private
  3705. */
  3706. RootPanel.prototype._updateEventEmitters = function () {
  3707. if (this.listeners) {
  3708. var me = this;
  3709. util.forEach(this.listeners, function (listeners, event) {
  3710. if (!me.emitters) {
  3711. me.emitters = {};
  3712. }
  3713. if (!(event in me.emitters)) {
  3714. // create event
  3715. var frame = me.frame;
  3716. if (frame) {
  3717. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3718. var callback = function(event) {
  3719. listeners.forEach(function (listener) {
  3720. // TODO: filter on event target!
  3721. listener(event);
  3722. });
  3723. };
  3724. me.emitters[event] = callback;
  3725. util.addEventListener(frame, event, callback);
  3726. }
  3727. }
  3728. });
  3729. // TODO: be able to delete event listeners
  3730. // TODO: be able to move event listeners to a parent when available
  3731. }
  3732. };
  3733. /**
  3734. * A horizontal time axis
  3735. * @param {Component} parent
  3736. * @param {Component[]} [depends] Components on which this components depends
  3737. * (except for the parent)
  3738. * @param {Object} [options] See TimeAxis.setOptions for the available
  3739. * options.
  3740. * @constructor TimeAxis
  3741. * @extends Component
  3742. */
  3743. function TimeAxis (parent, depends, options) {
  3744. this.id = util.randomUUID();
  3745. this.parent = parent;
  3746. this.depends = depends;
  3747. this.dom = {
  3748. majorLines: [],
  3749. majorTexts: [],
  3750. minorLines: [],
  3751. minorTexts: [],
  3752. redundant: {
  3753. majorLines: [],
  3754. majorTexts: [],
  3755. minorLines: [],
  3756. minorTexts: []
  3757. }
  3758. };
  3759. this.props = {
  3760. range: {
  3761. start: 0,
  3762. end: 0,
  3763. minimumStep: 0
  3764. },
  3765. lineTop: 0
  3766. };
  3767. this.options = options || {};
  3768. this.defaultOptions = {
  3769. orientation: 'bottom', // supported: 'top', 'bottom'
  3770. // TODO: implement timeaxis orientations 'left' and 'right'
  3771. showMinorLabels: true,
  3772. showMajorLabels: true
  3773. };
  3774. this.conversion = null;
  3775. this.range = null;
  3776. }
  3777. TimeAxis.prototype = new Component();
  3778. // TODO: comment options
  3779. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3780. /**
  3781. * Set a range (start and end)
  3782. * @param {Range | Object} range A Range or an object containing start and end.
  3783. */
  3784. TimeAxis.prototype.setRange = function (range) {
  3785. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3786. throw new TypeError('Range must be an instance of Range, ' +
  3787. 'or an object containing start and end.');
  3788. }
  3789. this.range = range;
  3790. };
  3791. /**
  3792. * Convert a position on screen (pixels) to a datetime
  3793. * @param {int} x Position on the screen in pixels
  3794. * @return {Date} time The datetime the corresponds with given position x
  3795. */
  3796. TimeAxis.prototype.toTime = function(x) {
  3797. var conversion = this.conversion;
  3798. return new Date(x / conversion.factor + conversion.offset);
  3799. };
  3800. /**
  3801. * Convert a datetime (Date object) into a position on the screen
  3802. * @param {Date} time A date
  3803. * @return {int} x The position on the screen in pixels which corresponds
  3804. * with the given date.
  3805. * @private
  3806. */
  3807. TimeAxis.prototype.toScreen = function(time) {
  3808. var conversion = this.conversion;
  3809. return (time.valueOf() - conversion.offset) * conversion.factor;
  3810. };
  3811. /**
  3812. * Repaint the component
  3813. * @return {Boolean} changed
  3814. */
  3815. TimeAxis.prototype.repaint = function () {
  3816. var changed = 0,
  3817. update = util.updateProperty,
  3818. asSize = util.option.asSize,
  3819. options = this.options,
  3820. orientation = this.getOption('orientation'),
  3821. props = this.props,
  3822. step = this.step;
  3823. var frame = this.frame;
  3824. if (!frame) {
  3825. frame = document.createElement('div');
  3826. this.frame = frame;
  3827. changed += 1;
  3828. }
  3829. frame.className = 'axis ' + orientation;
  3830. // TODO: custom className?
  3831. if (!frame.parentNode) {
  3832. if (!this.parent) {
  3833. throw new Error('Cannot repaint time axis: no parent attached');
  3834. }
  3835. var parentContainer = this.parent.getContainer();
  3836. if (!parentContainer) {
  3837. throw new Error('Cannot repaint time axis: parent has no container element');
  3838. }
  3839. parentContainer.appendChild(frame);
  3840. changed += 1;
  3841. }
  3842. var parent = frame.parentNode;
  3843. if (parent) {
  3844. var beforeChild = frame.nextSibling;
  3845. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3846. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3847. (this.props.parentHeight - this.height) + 'px' :
  3848. '0px';
  3849. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3850. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3851. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3852. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3853. // get characters width and height
  3854. this._repaintMeasureChars();
  3855. if (this.step) {
  3856. this._repaintStart();
  3857. step.first();
  3858. var xFirstMajorLabel = undefined;
  3859. var max = 0;
  3860. while (step.hasNext() && max < 1000) {
  3861. max++;
  3862. var cur = step.getCurrent(),
  3863. x = this.toScreen(cur),
  3864. isMajor = step.isMajor();
  3865. // TODO: lines must have a width, such that we can create css backgrounds
  3866. if (this.getOption('showMinorLabels')) {
  3867. this._repaintMinorText(x, step.getLabelMinor());
  3868. }
  3869. if (isMajor && this.getOption('showMajorLabels')) {
  3870. if (x > 0) {
  3871. if (xFirstMajorLabel == undefined) {
  3872. xFirstMajorLabel = x;
  3873. }
  3874. this._repaintMajorText(x, step.getLabelMajor());
  3875. }
  3876. this._repaintMajorLine(x);
  3877. }
  3878. else {
  3879. this._repaintMinorLine(x);
  3880. }
  3881. step.next();
  3882. }
  3883. // create a major label on the left when needed
  3884. if (this.getOption('showMajorLabels')) {
  3885. var leftTime = this.toTime(0),
  3886. leftText = step.getLabelMajor(leftTime),
  3887. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3888. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3889. this._repaintMajorText(0, leftText);
  3890. }
  3891. }
  3892. this._repaintEnd();
  3893. }
  3894. this._repaintLine();
  3895. // put frame online again
  3896. if (beforeChild) {
  3897. parent.insertBefore(frame, beforeChild);
  3898. }
  3899. else {
  3900. parent.appendChild(frame)
  3901. }
  3902. }
  3903. return (changed > 0);
  3904. };
  3905. /**
  3906. * Start a repaint. Move all DOM elements to a redundant list, where they
  3907. * can be picked for re-use, or can be cleaned up in the end
  3908. * @private
  3909. */
  3910. TimeAxis.prototype._repaintStart = function () {
  3911. var dom = this.dom,
  3912. redundant = dom.redundant;
  3913. redundant.majorLines = dom.majorLines;
  3914. redundant.majorTexts = dom.majorTexts;
  3915. redundant.minorLines = dom.minorLines;
  3916. redundant.minorTexts = dom.minorTexts;
  3917. dom.majorLines = [];
  3918. dom.majorTexts = [];
  3919. dom.minorLines = [];
  3920. dom.minorTexts = [];
  3921. };
  3922. /**
  3923. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3924. * @private
  3925. */
  3926. TimeAxis.prototype._repaintEnd = function () {
  3927. util.forEach(this.dom.redundant, function (arr) {
  3928. while (arr.length) {
  3929. var elem = arr.pop();
  3930. if (elem && elem.parentNode) {
  3931. elem.parentNode.removeChild(elem);
  3932. }
  3933. }
  3934. });
  3935. };
  3936. /**
  3937. * Create a minor label for the axis at position x
  3938. * @param {Number} x
  3939. * @param {String} text
  3940. * @private
  3941. */
  3942. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3943. // reuse redundant label
  3944. var label = this.dom.redundant.minorTexts.shift();
  3945. if (!label) {
  3946. // create new label
  3947. var content = document.createTextNode('');
  3948. label = document.createElement('div');
  3949. label.appendChild(content);
  3950. label.className = 'text minor';
  3951. this.frame.appendChild(label);
  3952. }
  3953. this.dom.minorTexts.push(label);
  3954. label.childNodes[0].nodeValue = text;
  3955. label.style.left = x + 'px';
  3956. label.style.top = this.props.minorLabelTop + 'px';
  3957. //label.title = title; // TODO: this is a heavy operation
  3958. };
  3959. /**
  3960. * Create a Major label for the axis at position x
  3961. * @param {Number} x
  3962. * @param {String} text
  3963. * @private
  3964. */
  3965. TimeAxis.prototype._repaintMajorText = function (x, text) {
  3966. // reuse redundant label
  3967. var label = this.dom.redundant.majorTexts.shift();
  3968. if (!label) {
  3969. // create label
  3970. var content = document.createTextNode(text);
  3971. label = document.createElement('div');
  3972. label.className = 'text major';
  3973. label.appendChild(content);
  3974. this.frame.appendChild(label);
  3975. }
  3976. this.dom.majorTexts.push(label);
  3977. label.childNodes[0].nodeValue = text;
  3978. label.style.top = this.props.majorLabelTop + 'px';
  3979. label.style.left = x + 'px';
  3980. //label.title = title; // TODO: this is a heavy operation
  3981. };
  3982. /**
  3983. * Create a minor line for the axis at position x
  3984. * @param {Number} x
  3985. * @private
  3986. */
  3987. TimeAxis.prototype._repaintMinorLine = function (x) {
  3988. // reuse redundant line
  3989. var line = this.dom.redundant.minorLines.shift();
  3990. if (!line) {
  3991. // create vertical line
  3992. line = document.createElement('div');
  3993. line.className = 'grid vertical minor';
  3994. this.frame.appendChild(line);
  3995. }
  3996. this.dom.minorLines.push(line);
  3997. var props = this.props;
  3998. line.style.top = props.minorLineTop + 'px';
  3999. line.style.height = props.minorLineHeight + 'px';
  4000. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4001. };
  4002. /**
  4003. * Create a Major line for the axis at position x
  4004. * @param {Number} x
  4005. * @private
  4006. */
  4007. TimeAxis.prototype._repaintMajorLine = function (x) {
  4008. // reuse redundant line
  4009. var line = this.dom.redundant.majorLines.shift();
  4010. if (!line) {
  4011. // create vertical line
  4012. line = document.createElement('DIV');
  4013. line.className = 'grid vertical major';
  4014. this.frame.appendChild(line);
  4015. }
  4016. this.dom.majorLines.push(line);
  4017. var props = this.props;
  4018. line.style.top = props.majorLineTop + 'px';
  4019. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4020. line.style.height = props.majorLineHeight + 'px';
  4021. };
  4022. /**
  4023. * Repaint the horizontal line for the axis
  4024. * @private
  4025. */
  4026. TimeAxis.prototype._repaintLine = function() {
  4027. var line = this.dom.line,
  4028. frame = this.frame,
  4029. options = this.options;
  4030. // line before all axis elements
  4031. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4032. if (line) {
  4033. // put this line at the end of all childs
  4034. frame.removeChild(line);
  4035. frame.appendChild(line);
  4036. }
  4037. else {
  4038. // create the axis line
  4039. line = document.createElement('div');
  4040. line.className = 'grid horizontal major';
  4041. frame.appendChild(line);
  4042. this.dom.line = line;
  4043. }
  4044. line.style.top = this.props.lineTop + 'px';
  4045. }
  4046. else {
  4047. if (line && axis.parentElement) {
  4048. frame.removeChild(axis.line);
  4049. delete this.dom.line;
  4050. }
  4051. }
  4052. };
  4053. /**
  4054. * Create characters used to determine the size of text on the axis
  4055. * @private
  4056. */
  4057. TimeAxis.prototype._repaintMeasureChars = function () {
  4058. // calculate the width and height of a single character
  4059. // this is used to calculate the step size, and also the positioning of the
  4060. // axis
  4061. var dom = this.dom,
  4062. text;
  4063. if (!dom.measureCharMinor) {
  4064. text = document.createTextNode('0');
  4065. var measureCharMinor = document.createElement('DIV');
  4066. measureCharMinor.className = 'text minor measure';
  4067. measureCharMinor.appendChild(text);
  4068. this.frame.appendChild(measureCharMinor);
  4069. dom.measureCharMinor = measureCharMinor;
  4070. }
  4071. if (!dom.measureCharMajor) {
  4072. text = document.createTextNode('0');
  4073. var measureCharMajor = document.createElement('DIV');
  4074. measureCharMajor.className = 'text major measure';
  4075. measureCharMajor.appendChild(text);
  4076. this.frame.appendChild(measureCharMajor);
  4077. dom.measureCharMajor = measureCharMajor;
  4078. }
  4079. };
  4080. /**
  4081. * Reflow the component
  4082. * @return {Boolean} resized
  4083. */
  4084. TimeAxis.prototype.reflow = function () {
  4085. var changed = 0,
  4086. update = util.updateProperty,
  4087. frame = this.frame,
  4088. range = this.range;
  4089. if (!range) {
  4090. throw new Error('Cannot repaint time axis: no range configured');
  4091. }
  4092. if (frame) {
  4093. changed += update(this, 'top', frame.offsetTop);
  4094. changed += update(this, 'left', frame.offsetLeft);
  4095. // calculate size of a character
  4096. var props = this.props,
  4097. showMinorLabels = this.getOption('showMinorLabels'),
  4098. showMajorLabels = this.getOption('showMajorLabels'),
  4099. measureCharMinor = this.dom.measureCharMinor,
  4100. measureCharMajor = this.dom.measureCharMajor;
  4101. if (measureCharMinor) {
  4102. props.minorCharHeight = measureCharMinor.clientHeight;
  4103. props.minorCharWidth = measureCharMinor.clientWidth;
  4104. }
  4105. if (measureCharMajor) {
  4106. props.majorCharHeight = measureCharMajor.clientHeight;
  4107. props.majorCharWidth = measureCharMajor.clientWidth;
  4108. }
  4109. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4110. if (parentHeight != props.parentHeight) {
  4111. props.parentHeight = parentHeight;
  4112. changed += 1;
  4113. }
  4114. switch (this.getOption('orientation')) {
  4115. case 'bottom':
  4116. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4117. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4118. props.minorLabelTop = 0;
  4119. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4120. props.minorLineTop = -this.top;
  4121. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4122. props.minorLineWidth = 1; // TODO: really calculate width
  4123. props.majorLineTop = -this.top;
  4124. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4125. props.majorLineWidth = 1; // TODO: really calculate width
  4126. props.lineTop = 0;
  4127. break;
  4128. case 'top':
  4129. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4130. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4131. props.majorLabelTop = 0;
  4132. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4133. props.minorLineTop = props.minorLabelTop;
  4134. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4135. props.minorLineWidth = 1; // TODO: really calculate width
  4136. props.majorLineTop = 0;
  4137. props.majorLineHeight = Math.max(parentHeight - this.top);
  4138. props.majorLineWidth = 1; // TODO: really calculate width
  4139. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4140. break;
  4141. default:
  4142. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4143. }
  4144. var height = props.minorLabelHeight + props.majorLabelHeight;
  4145. changed += update(this, 'width', frame.offsetWidth);
  4146. changed += update(this, 'height', height);
  4147. // calculate range and step
  4148. this._updateConversion();
  4149. var start = util.convert(range.start, 'Date'),
  4150. end = util.convert(range.end, 'Date'),
  4151. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  4152. this.step = new TimeStep(start, end, minimumStep);
  4153. changed += update(props.range, 'start', start.valueOf());
  4154. changed += update(props.range, 'end', end.valueOf());
  4155. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4156. }
  4157. return (changed > 0);
  4158. };
  4159. /**
  4160. * Calculate the factor and offset to convert a position on screen to the
  4161. * corresponding date and vice versa.
  4162. * After the method _updateConversion is executed once, the methods toTime
  4163. * and toScreen can be used.
  4164. * @private
  4165. */
  4166. TimeAxis.prototype._updateConversion = function() {
  4167. var range = this.range;
  4168. if (!range) {
  4169. throw new Error('No range configured');
  4170. }
  4171. if (range.conversion) {
  4172. this.conversion = range.conversion(this.width);
  4173. }
  4174. else {
  4175. this.conversion = Range.conversion(range.start, range.end, this.width);
  4176. }
  4177. };
  4178. /**
  4179. * An ItemSet holds a set of items and ranges which can be displayed in a
  4180. * range. The width is determined by the parent of the ItemSet, and the height
  4181. * is determined by the size of the items.
  4182. * @param {Component} parent
  4183. * @param {Component[]} [depends] Components on which this components depends
  4184. * (except for the parent)
  4185. * @param {Object} [options] See ItemSet.setOptions for the available
  4186. * options.
  4187. * @constructor ItemSet
  4188. * @extends Panel
  4189. */
  4190. // TODO: improve performance by replacing all Array.forEach with a for loop
  4191. function ItemSet(parent, depends, options) {
  4192. this.id = util.randomUUID();
  4193. this.parent = parent;
  4194. this.depends = depends;
  4195. // one options object is shared by this itemset and all its items
  4196. this.options = options || {};
  4197. this.defaultOptions = {
  4198. type: 'box',
  4199. align: 'center',
  4200. orientation: 'bottom',
  4201. margin: {
  4202. axis: 20,
  4203. item: 10
  4204. },
  4205. padding: 5
  4206. };
  4207. this.dom = {};
  4208. var me = this;
  4209. this.itemsData = null; // DataSet
  4210. this.range = null; // Range or Object {start: number, end: number}
  4211. this.listeners = {
  4212. 'add': function (event, params, senderId) {
  4213. if (senderId != me.id) {
  4214. me._onAdd(params.items);
  4215. }
  4216. },
  4217. 'update': function (event, params, senderId) {
  4218. if (senderId != me.id) {
  4219. me._onUpdate(params.items);
  4220. }
  4221. },
  4222. 'remove': function (event, params, senderId) {
  4223. if (senderId != me.id) {
  4224. me._onRemove(params.items);
  4225. }
  4226. }
  4227. };
  4228. this.items = {}; // object with an Item for every data item
  4229. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4230. this.stack = new Stack(this, Object.create(this.options));
  4231. this.conversion = null;
  4232. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4233. }
  4234. ItemSet.prototype = new Panel();
  4235. // available item types will be registered here
  4236. ItemSet.types = {
  4237. box: ItemBox,
  4238. range: ItemRange,
  4239. point: ItemPoint
  4240. };
  4241. /**
  4242. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4243. * @param {Object} [options] The following options are available:
  4244. * {String | function} [className]
  4245. * class name for the itemset
  4246. * {String} [type]
  4247. * Default type for the items. Choose from 'box'
  4248. * (default), 'point', or 'range'. The default
  4249. * Style can be overwritten by individual items.
  4250. * {String} align
  4251. * Alignment for the items, only applicable for
  4252. * ItemBox. Choose 'center' (default), 'left', or
  4253. * 'right'.
  4254. * {String} orientation
  4255. * Orientation of the item set. Choose 'top' or
  4256. * 'bottom' (default).
  4257. * {Number} margin.axis
  4258. * Margin between the axis and the items in pixels.
  4259. * Default is 20.
  4260. * {Number} margin.item
  4261. * Margin between items in pixels. Default is 10.
  4262. * {Number} padding
  4263. * Padding of the contents of an item in pixels.
  4264. * Must correspond with the items css. Default is 5.
  4265. */
  4266. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4267. /**
  4268. * Set range (start and end).
  4269. * @param {Range | Object} range A Range or an object containing start and end.
  4270. */
  4271. ItemSet.prototype.setRange = function setRange(range) {
  4272. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4273. throw new TypeError('Range must be an instance of Range, ' +
  4274. 'or an object containing start and end.');
  4275. }
  4276. this.range = range;
  4277. };
  4278. /**
  4279. * Repaint the component
  4280. * @return {Boolean} changed
  4281. */
  4282. ItemSet.prototype.repaint = function repaint() {
  4283. var changed = 0,
  4284. update = util.updateProperty,
  4285. asSize = util.option.asSize,
  4286. options = this.options,
  4287. orientation = this.getOption('orientation'),
  4288. defaultOptions = this.defaultOptions,
  4289. frame = this.frame;
  4290. if (!frame) {
  4291. frame = document.createElement('div');
  4292. frame.className = 'itemset';
  4293. var className = options.className;
  4294. if (className) {
  4295. util.addClassName(frame, util.option.asString(className));
  4296. }
  4297. // create background panel
  4298. var background = document.createElement('div');
  4299. background.className = 'background';
  4300. frame.appendChild(background);
  4301. this.dom.background = background;
  4302. // create foreground panel
  4303. var foreground = document.createElement('div');
  4304. foreground.className = 'foreground';
  4305. frame.appendChild(foreground);
  4306. this.dom.foreground = foreground;
  4307. // create axis panel
  4308. var axis = document.createElement('div');
  4309. axis.className = 'itemset-axis';
  4310. //frame.appendChild(axis);
  4311. this.dom.axis = axis;
  4312. this.frame = frame;
  4313. changed += 1;
  4314. }
  4315. if (!this.parent) {
  4316. throw new Error('Cannot repaint itemset: no parent attached');
  4317. }
  4318. var parentContainer = this.parent.getContainer();
  4319. if (!parentContainer) {
  4320. throw new Error('Cannot repaint itemset: parent has no container element');
  4321. }
  4322. if (!frame.parentNode) {
  4323. parentContainer.appendChild(frame);
  4324. changed += 1;
  4325. }
  4326. if (!this.dom.axis.parentNode) {
  4327. parentContainer.appendChild(this.dom.axis);
  4328. changed += 1;
  4329. }
  4330. // reposition frame
  4331. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4332. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4333. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4334. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4335. // reposition axis
  4336. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4337. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4338. if (orientation == 'bottom') {
  4339. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4340. }
  4341. else { // orientation == 'top'
  4342. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4343. }
  4344. this._updateConversion();
  4345. var me = this,
  4346. queue = this.queue,
  4347. itemsData = this.itemsData,
  4348. items = this.items,
  4349. dataOptions = {
  4350. // TODO: cleanup
  4351. //fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type']
  4352. };
  4353. // show/hide added/changed/removed items
  4354. Object.keys(queue).forEach(function (id) {
  4355. //var entry = queue[id];
  4356. var action = queue[id];
  4357. var item = items[id];
  4358. //var item = entry.item;
  4359. //noinspection FallthroughInSwitchStatementJS
  4360. switch (action) {
  4361. case 'add':
  4362. case 'update':
  4363. var itemData = itemsData && itemsData.get(id, dataOptions);
  4364. if (itemData) {
  4365. var type = itemData.type ||
  4366. (itemData.start && itemData.end && 'range') ||
  4367. options.type ||
  4368. 'box';
  4369. var constructor = ItemSet.types[type];
  4370. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4371. if (item) {
  4372. // update item
  4373. if (!constructor || !(item instanceof constructor)) {
  4374. // item type has changed, hide and delete the item
  4375. changed += item.hide();
  4376. item = null;
  4377. }
  4378. else {
  4379. item.data = itemData; // TODO: create a method item.setData ?
  4380. changed++;
  4381. }
  4382. }
  4383. if (!item) {
  4384. // create item
  4385. if (constructor) {
  4386. item = new constructor(me, itemData, options, defaultOptions);
  4387. changed++;
  4388. }
  4389. else {
  4390. throw new TypeError('Unknown item type "' + type + '"');
  4391. }
  4392. }
  4393. // force a repaint (not only a reposition)
  4394. item.repaint();
  4395. items[id] = item;
  4396. }
  4397. // update queue
  4398. delete queue[id];
  4399. break;
  4400. case 'remove':
  4401. if (item) {
  4402. // remove DOM of the item
  4403. changed += item.hide();
  4404. }
  4405. // update lists
  4406. delete items[id];
  4407. delete queue[id];
  4408. break;
  4409. default:
  4410. console.log('Error: unknown action "' + action + '"');
  4411. }
  4412. });
  4413. // reposition all items. Show items only when in the visible area
  4414. util.forEach(this.items, function (item) {
  4415. if (item.visible) {
  4416. changed += item.show();
  4417. item.reposition();
  4418. }
  4419. else {
  4420. changed += item.hide();
  4421. }
  4422. });
  4423. return (changed > 0);
  4424. };
  4425. /**
  4426. * Get the foreground container element
  4427. * @return {HTMLElement} foreground
  4428. */
  4429. ItemSet.prototype.getForeground = function getForeground() {
  4430. return this.dom.foreground;
  4431. };
  4432. /**
  4433. * Get the background container element
  4434. * @return {HTMLElement} background
  4435. */
  4436. ItemSet.prototype.getBackground = function getBackground() {
  4437. return this.dom.background;
  4438. };
  4439. /**
  4440. * Get the axis container element
  4441. * @return {HTMLElement} axis
  4442. */
  4443. ItemSet.prototype.getAxis = function getAxis() {
  4444. return this.dom.axis;
  4445. };
  4446. /**
  4447. * Reflow the component
  4448. * @return {Boolean} resized
  4449. */
  4450. ItemSet.prototype.reflow = function reflow () {
  4451. var changed = 0,
  4452. options = this.options,
  4453. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4454. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4455. update = util.updateProperty,
  4456. asNumber = util.option.asNumber,
  4457. asSize = util.option.asSize,
  4458. frame = this.frame;
  4459. if (frame) {
  4460. this._updateConversion();
  4461. util.forEach(this.items, function (item) {
  4462. changed += item.reflow();
  4463. });
  4464. // TODO: stack.update should be triggered via an event, in stack itself
  4465. // TODO: only update the stack when there are changed items
  4466. this.stack.update();
  4467. var maxHeight = asNumber(options.maxHeight);
  4468. var fixedHeight = (asSize(options.height) != null);
  4469. var height;
  4470. if (fixedHeight) {
  4471. height = frame.offsetHeight;
  4472. }
  4473. else {
  4474. // height is not specified, determine the height from the height and positioned items
  4475. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4476. if (visibleItems.length) {
  4477. var min = visibleItems[0].top;
  4478. var max = visibleItems[0].top + visibleItems[0].height;
  4479. util.forEach(visibleItems, function (item) {
  4480. min = Math.min(min, item.top);
  4481. max = Math.max(max, (item.top + item.height));
  4482. });
  4483. height = (max - min) + marginAxis + marginItem;
  4484. }
  4485. else {
  4486. height = marginAxis + marginItem;
  4487. }
  4488. }
  4489. if (maxHeight != null) {
  4490. height = Math.min(height, maxHeight);
  4491. }
  4492. changed += update(this, 'height', height);
  4493. // calculate height from items
  4494. changed += update(this, 'top', frame.offsetTop);
  4495. changed += update(this, 'left', frame.offsetLeft);
  4496. changed += update(this, 'width', frame.offsetWidth);
  4497. }
  4498. else {
  4499. changed += 1;
  4500. }
  4501. return (changed > 0);
  4502. };
  4503. /**
  4504. * Hide this component from the DOM
  4505. * @return {Boolean} changed
  4506. */
  4507. ItemSet.prototype.hide = function hide() {
  4508. var changed = false;
  4509. // remove the DOM
  4510. if (this.frame && this.frame.parentNode) {
  4511. this.frame.parentNode.removeChild(this.frame);
  4512. changed = true;
  4513. }
  4514. if (this.dom.axis && this.dom.axis.parentNode) {
  4515. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4516. changed = true;
  4517. }
  4518. return changed;
  4519. };
  4520. /**
  4521. * Set items
  4522. * @param {vis.DataSet | null} items
  4523. */
  4524. ItemSet.prototype.setItems = function setItems(items) {
  4525. var me = this,
  4526. ids,
  4527. oldItemsData = this.itemsData;
  4528. // replace the dataset
  4529. if (!items) {
  4530. this.itemsData = null;
  4531. }
  4532. else if (items instanceof DataSet || items instanceof DataView) {
  4533. this.itemsData = items;
  4534. }
  4535. else {
  4536. throw new TypeError('Data must be an instance of DataSet');
  4537. }
  4538. if (oldItemsData) {
  4539. // unsubscribe from old dataset
  4540. util.forEach(this.listeners, function (callback, event) {
  4541. oldItemsData.unsubscribe(event, callback);
  4542. });
  4543. // remove all drawn items
  4544. ids = oldItemsData.getIds();
  4545. this._onRemove(ids);
  4546. }
  4547. if (this.itemsData) {
  4548. // subscribe to new dataset
  4549. var id = this.id;
  4550. util.forEach(this.listeners, function (callback, event) {
  4551. me.itemsData.subscribe(event, callback, id);
  4552. });
  4553. // draw all new items
  4554. ids = this.itemsData.getIds();
  4555. this._onAdd(ids);
  4556. }
  4557. };
  4558. /**
  4559. * Get the current items items
  4560. * @returns {vis.DataSet | null}
  4561. */
  4562. ItemSet.prototype.getItems = function getItems() {
  4563. return this.itemsData;
  4564. };
  4565. /**
  4566. * Handle updated items
  4567. * @param {Number[]} ids
  4568. * @private
  4569. */
  4570. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4571. this._toQueue('update', ids);
  4572. };
  4573. /**
  4574. * Handle changed items
  4575. * @param {Number[]} ids
  4576. * @private
  4577. */
  4578. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4579. this._toQueue('add', ids);
  4580. };
  4581. /**
  4582. * Handle removed items
  4583. * @param {Number[]} ids
  4584. * @private
  4585. */
  4586. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4587. this._toQueue('remove', ids);
  4588. };
  4589. /**
  4590. * Put items in the queue to be added/updated/remove
  4591. * @param {String} action can be 'add', 'update', 'remove'
  4592. * @param {Number[]} ids
  4593. */
  4594. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  4595. var queue = this.queue;
  4596. ids.forEach(function (id) {
  4597. queue[id] = action;
  4598. });
  4599. if (this.controller) {
  4600. //this.requestReflow();
  4601. this.requestRepaint();
  4602. }
  4603. };
  4604. /**
  4605. * Calculate the factor and offset to convert a position on screen to the
  4606. * corresponding date and vice versa.
  4607. * After the method _updateConversion is executed once, the methods toTime
  4608. * and toScreen can be used.
  4609. * @private
  4610. */
  4611. ItemSet.prototype._updateConversion = function _updateConversion() {
  4612. var range = this.range;
  4613. if (!range) {
  4614. throw new Error('No range configured');
  4615. }
  4616. if (range.conversion) {
  4617. this.conversion = range.conversion(this.width);
  4618. }
  4619. else {
  4620. this.conversion = Range.conversion(range.start, range.end, this.width);
  4621. }
  4622. };
  4623. /**
  4624. * Convert a position on screen (pixels) to a datetime
  4625. * Before this method can be used, the method _updateConversion must be
  4626. * executed once.
  4627. * @param {int} x Position on the screen in pixels
  4628. * @return {Date} time The datetime the corresponds with given position x
  4629. */
  4630. ItemSet.prototype.toTime = function toTime(x) {
  4631. var conversion = this.conversion;
  4632. return new Date(x / conversion.factor + conversion.offset);
  4633. };
  4634. /**
  4635. * Convert a datetime (Date object) into a position on the screen
  4636. * Before this method can be used, the method _updateConversion must be
  4637. * executed once.
  4638. * @param {Date} time A date
  4639. * @return {int} x The position on the screen in pixels which corresponds
  4640. * with the given date.
  4641. */
  4642. ItemSet.prototype.toScreen = function toScreen(time) {
  4643. var conversion = this.conversion;
  4644. return (time.valueOf() - conversion.offset) * conversion.factor;
  4645. };
  4646. /**
  4647. * @constructor Item
  4648. * @param {ItemSet} parent
  4649. * @param {Object} data Object containing (optional) parameters type,
  4650. * start, end, content, group, className.
  4651. * @param {Object} [options] Options to set initial property values
  4652. * @param {Object} [defaultOptions] default options
  4653. * // TODO: describe available options
  4654. */
  4655. function Item (parent, data, options, defaultOptions) {
  4656. this.parent = parent;
  4657. this.data = data;
  4658. this.dom = null;
  4659. this.options = options || {};
  4660. this.defaultOptions = defaultOptions || {};
  4661. this.selected = false;
  4662. this.visible = false;
  4663. this.top = 0;
  4664. this.left = 0;
  4665. this.width = 0;
  4666. this.height = 0;
  4667. }
  4668. /**
  4669. * Select current item
  4670. */
  4671. Item.prototype.select = function select() {
  4672. this.selected = true;
  4673. };
  4674. /**
  4675. * Unselect current item
  4676. */
  4677. Item.prototype.unselect = function unselect() {
  4678. this.selected = false;
  4679. };
  4680. /**
  4681. * Show the Item in the DOM (when not already visible)
  4682. * @return {Boolean} changed
  4683. */
  4684. Item.prototype.show = function show() {
  4685. return false;
  4686. };
  4687. /**
  4688. * Hide the Item from the DOM (when visible)
  4689. * @return {Boolean} changed
  4690. */
  4691. Item.prototype.hide = function hide() {
  4692. return false;
  4693. };
  4694. /**
  4695. * Repaint the item
  4696. * @return {Boolean} changed
  4697. */
  4698. Item.prototype.repaint = function repaint() {
  4699. // should be implemented by the item
  4700. return false;
  4701. };
  4702. /**
  4703. * Reflow the item
  4704. * @return {Boolean} resized
  4705. */
  4706. Item.prototype.reflow = function reflow() {
  4707. // should be implemented by the item
  4708. return false;
  4709. };
  4710. /**
  4711. * @constructor ItemBox
  4712. * @extends Item
  4713. * @param {ItemSet} parent
  4714. * @param {Object} data Object containing parameters start
  4715. * content, className.
  4716. * @param {Object} [options] Options to set initial property values
  4717. * @param {Object} [defaultOptions] default options
  4718. * // TODO: describe available options
  4719. */
  4720. function ItemBox (parent, data, options, defaultOptions) {
  4721. this.props = {
  4722. dot: {
  4723. left: 0,
  4724. top: 0,
  4725. width: 0,
  4726. height: 0
  4727. },
  4728. line: {
  4729. top: 0,
  4730. left: 0,
  4731. width: 0,
  4732. height: 0
  4733. }
  4734. };
  4735. Item.call(this, parent, data, options, defaultOptions);
  4736. }
  4737. ItemBox.prototype = new Item (null, null);
  4738. /**
  4739. * Select the item
  4740. * @override
  4741. */
  4742. ItemBox.prototype.select = function select() {
  4743. this.selected = true;
  4744. // TODO: select and unselect
  4745. };
  4746. /**
  4747. * Unselect the item
  4748. * @override
  4749. */
  4750. ItemBox.prototype.unselect = function unselect() {
  4751. this.selected = false;
  4752. // TODO: select and unselect
  4753. };
  4754. /**
  4755. * Repaint the item
  4756. * @return {Boolean} changed
  4757. */
  4758. ItemBox.prototype.repaint = function repaint() {
  4759. // TODO: make an efficient repaint
  4760. var changed = false;
  4761. var dom = this.dom;
  4762. if (!dom) {
  4763. this._create();
  4764. dom = this.dom;
  4765. changed = true;
  4766. }
  4767. if (dom) {
  4768. if (!this.parent) {
  4769. throw new Error('Cannot repaint item: no parent attached');
  4770. }
  4771. var foreground = this.parent.getForeground();
  4772. if (!foreground) {
  4773. throw new Error('Cannot repaint time axis: ' +
  4774. 'parent has no foreground container element');
  4775. }
  4776. var background = this.parent.getBackground();
  4777. if (!background) {
  4778. throw new Error('Cannot repaint time axis: ' +
  4779. 'parent has no background container element');
  4780. }
  4781. var axis = this.parent.getAxis();
  4782. if (!background) {
  4783. throw new Error('Cannot repaint time axis: ' +
  4784. 'parent has no axis container element');
  4785. }
  4786. if (!dom.box.parentNode) {
  4787. foreground.appendChild(dom.box);
  4788. changed = true;
  4789. }
  4790. if (!dom.line.parentNode) {
  4791. background.appendChild(dom.line);
  4792. changed = true;
  4793. }
  4794. if (!dom.dot.parentNode) {
  4795. axis.appendChild(dom.dot);
  4796. changed = true;
  4797. }
  4798. // update contents
  4799. if (this.data.content != this.content) {
  4800. this.content = this.data.content;
  4801. if (this.content instanceof Element) {
  4802. dom.content.innerHTML = '';
  4803. dom.content.appendChild(this.content);
  4804. }
  4805. else if (this.data.content != undefined) {
  4806. dom.content.innerHTML = this.content;
  4807. }
  4808. else {
  4809. throw new Error('Property "content" missing in item ' + this.data.id);
  4810. }
  4811. changed = true;
  4812. }
  4813. // update class
  4814. var className = (this.data.className? ' ' + this.data.className : '') +
  4815. (this.selected ? ' selected' : '');
  4816. if (this.className != className) {
  4817. this.className = className;
  4818. dom.box.className = 'item box' + className;
  4819. dom.line.className = 'item line' + className;
  4820. dom.dot.className = 'item dot' + className;
  4821. changed = true;
  4822. }
  4823. }
  4824. return changed;
  4825. };
  4826. /**
  4827. * Show the item in the DOM (when not already visible). The items DOM will
  4828. * be created when needed.
  4829. * @return {Boolean} changed
  4830. */
  4831. ItemBox.prototype.show = function show() {
  4832. if (!this.dom || !this.dom.box.parentNode) {
  4833. return this.repaint();
  4834. }
  4835. else {
  4836. return false;
  4837. }
  4838. };
  4839. /**
  4840. * Hide the item from the DOM (when visible)
  4841. * @return {Boolean} changed
  4842. */
  4843. ItemBox.prototype.hide = function hide() {
  4844. var changed = false,
  4845. dom = this.dom;
  4846. if (dom) {
  4847. if (dom.box.parentNode) {
  4848. dom.box.parentNode.removeChild(dom.box);
  4849. changed = true;
  4850. }
  4851. if (dom.line.parentNode) {
  4852. dom.line.parentNode.removeChild(dom.line);
  4853. }
  4854. if (dom.dot.parentNode) {
  4855. dom.dot.parentNode.removeChild(dom.dot);
  4856. }
  4857. }
  4858. return changed;
  4859. };
  4860. /**
  4861. * Reflow the item: calculate its actual size and position from the DOM
  4862. * @return {boolean} resized returns true if the axis is resized
  4863. * @override
  4864. */
  4865. ItemBox.prototype.reflow = function reflow() {
  4866. var changed = 0,
  4867. update,
  4868. dom,
  4869. props,
  4870. options,
  4871. margin,
  4872. start,
  4873. align,
  4874. orientation,
  4875. top,
  4876. left,
  4877. data,
  4878. range;
  4879. if (this.data.start == undefined) {
  4880. throw new Error('Property "start" missing in item ' + this.data.id);
  4881. }
  4882. data = this.data;
  4883. range = this.parent && this.parent.range;
  4884. if (data && range) {
  4885. // TODO: account for the width of the item
  4886. var interval = (range.end - range.start);
  4887. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  4888. }
  4889. else {
  4890. this.visible = false;
  4891. }
  4892. if (this.visible) {
  4893. dom = this.dom;
  4894. if (dom) {
  4895. update = util.updateProperty;
  4896. props = this.props;
  4897. options = this.options;
  4898. start = this.parent.toScreen(this.data.start);
  4899. align = options.align || this.defaultOptions.align;
  4900. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  4901. orientation = options.orientation || this.defaultOptions.orientation;
  4902. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  4903. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  4904. changed += update(props.line, 'width', dom.line.offsetWidth);
  4905. changed += update(props.line, 'height', dom.line.offsetHeight);
  4906. changed += update(props.line, 'top', dom.line.offsetTop);
  4907. changed += update(this, 'width', dom.box.offsetWidth);
  4908. changed += update(this, 'height', dom.box.offsetHeight);
  4909. if (align == 'right') {
  4910. left = start - this.width;
  4911. }
  4912. else if (align == 'left') {
  4913. left = start;
  4914. }
  4915. else {
  4916. // default or 'center'
  4917. left = start - this.width / 2;
  4918. }
  4919. changed += update(this, 'left', left);
  4920. changed += update(props.line, 'left', start - props.line.width / 2);
  4921. changed += update(props.dot, 'left', start - props.dot.width / 2);
  4922. changed += update(props.dot, 'top', -props.dot.height / 2);
  4923. if (orientation == 'top') {
  4924. top = margin;
  4925. changed += update(this, 'top', top);
  4926. }
  4927. else {
  4928. // default or 'bottom'
  4929. var parentHeight = this.parent.height;
  4930. top = parentHeight - this.height - margin;
  4931. changed += update(this, 'top', top);
  4932. }
  4933. }
  4934. else {
  4935. changed += 1;
  4936. }
  4937. }
  4938. return (changed > 0);
  4939. };
  4940. /**
  4941. * Create an items DOM
  4942. * @private
  4943. */
  4944. ItemBox.prototype._create = function _create() {
  4945. var dom = this.dom;
  4946. if (!dom) {
  4947. this.dom = dom = {};
  4948. // create the box
  4949. dom.box = document.createElement('DIV');
  4950. // className is updated in repaint()
  4951. // contents box (inside the background box). used for making margins
  4952. dom.content = document.createElement('DIV');
  4953. dom.content.className = 'content';
  4954. dom.box.appendChild(dom.content);
  4955. // line to axis
  4956. dom.line = document.createElement('DIV');
  4957. dom.line.className = 'line';
  4958. // dot on axis
  4959. dom.dot = document.createElement('DIV');
  4960. dom.dot.className = 'dot';
  4961. }
  4962. };
  4963. /**
  4964. * Reposition the item, recalculate its left, top, and width, using the current
  4965. * range and size of the items itemset
  4966. * @override
  4967. */
  4968. ItemBox.prototype.reposition = function reposition() {
  4969. var dom = this.dom,
  4970. props = this.props,
  4971. orientation = this.options.orientation || this.defaultOptions.orientation;
  4972. if (dom) {
  4973. var box = dom.box,
  4974. line = dom.line,
  4975. dot = dom.dot;
  4976. box.style.left = this.left + 'px';
  4977. box.style.top = this.top + 'px';
  4978. line.style.left = props.line.left + 'px';
  4979. if (orientation == 'top') {
  4980. line.style.top = 0 + 'px';
  4981. line.style.height = this.top + 'px';
  4982. }
  4983. else {
  4984. // orientation 'bottom'
  4985. line.style.top = (this.top + this.height) + 'px';
  4986. line.style.height = Math.max(this.parent.height - this.top - this.height +
  4987. this.props.dot.height / 2, 0) + 'px';
  4988. }
  4989. dot.style.left = props.dot.left + 'px';
  4990. dot.style.top = props.dot.top + 'px';
  4991. }
  4992. };
  4993. /**
  4994. * @constructor ItemPoint
  4995. * @extends Item
  4996. * @param {ItemSet} parent
  4997. * @param {Object} data Object containing parameters start
  4998. * content, className.
  4999. * @param {Object} [options] Options to set initial property values
  5000. * @param {Object} [defaultOptions] default options
  5001. * // TODO: describe available options
  5002. */
  5003. function ItemPoint (parent, data, options, defaultOptions) {
  5004. this.props = {
  5005. dot: {
  5006. top: 0,
  5007. width: 0,
  5008. height: 0
  5009. },
  5010. content: {
  5011. height: 0,
  5012. marginLeft: 0
  5013. }
  5014. };
  5015. Item.call(this, parent, data, options, defaultOptions);
  5016. }
  5017. ItemPoint.prototype = new Item (null, null);
  5018. /**
  5019. * Select the item
  5020. * @override
  5021. */
  5022. ItemPoint.prototype.select = function select() {
  5023. this.selected = true;
  5024. // TODO: select and unselect
  5025. };
  5026. /**
  5027. * Unselect the item
  5028. * @override
  5029. */
  5030. ItemPoint.prototype.unselect = function unselect() {
  5031. this.selected = false;
  5032. // TODO: select and unselect
  5033. };
  5034. /**
  5035. * Repaint the item
  5036. * @return {Boolean} changed
  5037. */
  5038. ItemPoint.prototype.repaint = function repaint() {
  5039. // TODO: make an efficient repaint
  5040. var changed = false;
  5041. var dom = this.dom;
  5042. if (!dom) {
  5043. this._create();
  5044. dom = this.dom;
  5045. changed = true;
  5046. }
  5047. if (dom) {
  5048. if (!this.parent) {
  5049. throw new Error('Cannot repaint item: no parent attached');
  5050. }
  5051. var foreground = this.parent.getForeground();
  5052. if (!foreground) {
  5053. throw new Error('Cannot repaint time axis: ' +
  5054. 'parent has no foreground container element');
  5055. }
  5056. if (!dom.point.parentNode) {
  5057. foreground.appendChild(dom.point);
  5058. foreground.appendChild(dom.point);
  5059. changed = true;
  5060. }
  5061. // update contents
  5062. if (this.data.content != this.content) {
  5063. this.content = this.data.content;
  5064. if (this.content instanceof Element) {
  5065. dom.content.innerHTML = '';
  5066. dom.content.appendChild(this.content);
  5067. }
  5068. else if (this.data.content != undefined) {
  5069. dom.content.innerHTML = this.content;
  5070. }
  5071. else {
  5072. throw new Error('Property "content" missing in item ' + this.data.id);
  5073. }
  5074. changed = true;
  5075. }
  5076. // update class
  5077. var className = (this.data.className? ' ' + this.data.className : '') +
  5078. (this.selected ? ' selected' : '');
  5079. if (this.className != className) {
  5080. this.className = className;
  5081. dom.point.className = 'item point' + className;
  5082. changed = true;
  5083. }
  5084. }
  5085. return changed;
  5086. };
  5087. /**
  5088. * Show the item in the DOM (when not already visible). The items DOM will
  5089. * be created when needed.
  5090. * @return {Boolean} changed
  5091. */
  5092. ItemPoint.prototype.show = function show() {
  5093. if (!this.dom || !this.dom.point.parentNode) {
  5094. return this.repaint();
  5095. }
  5096. else {
  5097. return false;
  5098. }
  5099. };
  5100. /**
  5101. * Hide the item from the DOM (when visible)
  5102. * @return {Boolean} changed
  5103. */
  5104. ItemPoint.prototype.hide = function hide() {
  5105. var changed = false,
  5106. dom = this.dom;
  5107. if (dom) {
  5108. if (dom.point.parentNode) {
  5109. dom.point.parentNode.removeChild(dom.point);
  5110. changed = true;
  5111. }
  5112. }
  5113. return changed;
  5114. };
  5115. /**
  5116. * Reflow the item: calculate its actual size from the DOM
  5117. * @return {boolean} resized returns true if the axis is resized
  5118. * @override
  5119. */
  5120. ItemPoint.prototype.reflow = function reflow() {
  5121. var changed = 0,
  5122. update,
  5123. dom,
  5124. props,
  5125. options,
  5126. margin,
  5127. orientation,
  5128. start,
  5129. top,
  5130. data,
  5131. range;
  5132. if (this.data.start == undefined) {
  5133. throw new Error('Property "start" missing in item ' + this.data.id);
  5134. }
  5135. data = this.data;
  5136. range = this.parent && this.parent.range;
  5137. if (data && range) {
  5138. // TODO: account for the width of the item
  5139. var interval = (range.end - range.start);
  5140. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5141. }
  5142. else {
  5143. this.visible = false;
  5144. }
  5145. if (this.visible) {
  5146. dom = this.dom;
  5147. if (dom) {
  5148. update = util.updateProperty;
  5149. props = this.props;
  5150. options = this.options;
  5151. orientation = options.orientation || this.defaultOptions.orientation;
  5152. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5153. start = this.parent.toScreen(this.data.start);
  5154. changed += update(this, 'width', dom.point.offsetWidth);
  5155. changed += update(this, 'height', dom.point.offsetHeight);
  5156. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5157. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5158. changed += update(props.content, 'height', dom.content.offsetHeight);
  5159. if (orientation == 'top') {
  5160. top = margin;
  5161. }
  5162. else {
  5163. // default or 'bottom'
  5164. var parentHeight = this.parent.height;
  5165. top = Math.max(parentHeight - this.height - margin, 0);
  5166. }
  5167. changed += update(this, 'top', top);
  5168. changed += update(this, 'left', start - props.dot.width / 2);
  5169. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5170. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5171. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5172. }
  5173. else {
  5174. changed += 1;
  5175. }
  5176. }
  5177. return (changed > 0);
  5178. };
  5179. /**
  5180. * Create an items DOM
  5181. * @private
  5182. */
  5183. ItemPoint.prototype._create = function _create() {
  5184. var dom = this.dom;
  5185. if (!dom) {
  5186. this.dom = dom = {};
  5187. // background box
  5188. dom.point = document.createElement('div');
  5189. // className is updated in repaint()
  5190. // contents box, right from the dot
  5191. dom.content = document.createElement('div');
  5192. dom.content.className = 'content';
  5193. dom.point.appendChild(dom.content);
  5194. // dot at start
  5195. dom.dot = document.createElement('div');
  5196. dom.dot.className = 'dot';
  5197. dom.point.appendChild(dom.dot);
  5198. }
  5199. };
  5200. /**
  5201. * Reposition the item, recalculate its left, top, and width, using the current
  5202. * range and size of the items itemset
  5203. * @override
  5204. */
  5205. ItemPoint.prototype.reposition = function reposition() {
  5206. var dom = this.dom,
  5207. props = this.props;
  5208. if (dom) {
  5209. dom.point.style.top = this.top + 'px';
  5210. dom.point.style.left = this.left + 'px';
  5211. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5212. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5213. dom.dot.style.top = props.dot.top + 'px';
  5214. }
  5215. };
  5216. /**
  5217. * @constructor ItemRange
  5218. * @extends Item
  5219. * @param {ItemSet} parent
  5220. * @param {Object} data Object containing parameters start, end
  5221. * content, className.
  5222. * @param {Object} [options] Options to set initial property values
  5223. * @param {Object} [defaultOptions] default options
  5224. * // TODO: describe available options
  5225. */
  5226. function ItemRange (parent, data, options, defaultOptions) {
  5227. this.props = {
  5228. content: {
  5229. left: 0,
  5230. width: 0
  5231. }
  5232. };
  5233. Item.call(this, parent, data, options, defaultOptions);
  5234. }
  5235. ItemRange.prototype = new Item (null, null);
  5236. /**
  5237. * Select the item
  5238. * @override
  5239. */
  5240. ItemRange.prototype.select = function select() {
  5241. this.selected = true;
  5242. // TODO: select and unselect
  5243. };
  5244. /**
  5245. * Unselect the item
  5246. * @override
  5247. */
  5248. ItemRange.prototype.unselect = function unselect() {
  5249. this.selected = false;
  5250. // TODO: select and unselect
  5251. };
  5252. /**
  5253. * Repaint the item
  5254. * @return {Boolean} changed
  5255. */
  5256. ItemRange.prototype.repaint = function repaint() {
  5257. // TODO: make an efficient repaint
  5258. var changed = false;
  5259. var dom = this.dom;
  5260. if (!dom) {
  5261. this._create();
  5262. dom = this.dom;
  5263. changed = true;
  5264. }
  5265. if (dom) {
  5266. if (!this.parent) {
  5267. throw new Error('Cannot repaint item: no parent attached');
  5268. }
  5269. var foreground = this.parent.getForeground();
  5270. if (!foreground) {
  5271. throw new Error('Cannot repaint time axis: ' +
  5272. 'parent has no foreground container element');
  5273. }
  5274. if (!dom.box.parentNode) {
  5275. foreground.appendChild(dom.box);
  5276. changed = true;
  5277. }
  5278. // update content
  5279. if (this.data.content != this.content) {
  5280. this.content = this.data.content;
  5281. if (this.content instanceof Element) {
  5282. dom.content.innerHTML = '';
  5283. dom.content.appendChild(this.content);
  5284. }
  5285. else if (this.data.content != undefined) {
  5286. dom.content.innerHTML = this.content;
  5287. }
  5288. else {
  5289. throw new Error('Property "content" missing in item ' + this.data.id);
  5290. }
  5291. changed = true;
  5292. }
  5293. // update class
  5294. var className = this.data.className ? (' ' + this.data.className) : '';
  5295. if (this.className != className) {
  5296. this.className = className;
  5297. dom.box.className = 'item range' + className;
  5298. changed = true;
  5299. }
  5300. }
  5301. return changed;
  5302. };
  5303. /**
  5304. * Show the item in the DOM (when not already visible). The items DOM will
  5305. * be created when needed.
  5306. * @return {Boolean} changed
  5307. */
  5308. ItemRange.prototype.show = function show() {
  5309. if (!this.dom || !this.dom.box.parentNode) {
  5310. return this.repaint();
  5311. }
  5312. else {
  5313. return false;
  5314. }
  5315. };
  5316. /**
  5317. * Hide the item from the DOM (when visible)
  5318. * @return {Boolean} changed
  5319. */
  5320. ItemRange.prototype.hide = function hide() {
  5321. var changed = false,
  5322. dom = this.dom;
  5323. if (dom) {
  5324. if (dom.box.parentNode) {
  5325. dom.box.parentNode.removeChild(dom.box);
  5326. changed = true;
  5327. }
  5328. }
  5329. return changed;
  5330. };
  5331. /**
  5332. * Reflow the item: calculate its actual size from the DOM
  5333. * @return {boolean} resized returns true if the axis is resized
  5334. * @override
  5335. */
  5336. ItemRange.prototype.reflow = function reflow() {
  5337. var changed = 0,
  5338. dom,
  5339. props,
  5340. options,
  5341. margin,
  5342. padding,
  5343. parent,
  5344. start,
  5345. end,
  5346. data,
  5347. range,
  5348. update,
  5349. box,
  5350. parentWidth,
  5351. contentLeft,
  5352. orientation,
  5353. top;
  5354. if (this.data.start == undefined) {
  5355. throw new Error('Property "start" missing in item ' + this.data.id);
  5356. }
  5357. if (this.data.end == undefined) {
  5358. throw new Error('Property "end" missing in item ' + this.data.id);
  5359. }
  5360. data = this.data;
  5361. range = this.parent && this.parent.range;
  5362. if (data && range) {
  5363. // TODO: account for the width of the item. Take some margin
  5364. this.visible = (data.start < range.end) && (data.end > range.start);
  5365. }
  5366. else {
  5367. this.visible = false;
  5368. }
  5369. if (this.visible) {
  5370. dom = this.dom;
  5371. if (dom) {
  5372. props = this.props;
  5373. options = this.options;
  5374. parent = this.parent;
  5375. start = parent.toScreen(this.data.start);
  5376. end = parent.toScreen(this.data.end);
  5377. update = util.updateProperty;
  5378. box = dom.box;
  5379. parentWidth = parent.width;
  5380. orientation = options.orientation || this.defaultOptions.orientation;
  5381. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5382. padding = options.padding || this.defaultOptions.padding;
  5383. changed += update(props.content, 'width', dom.content.offsetWidth);
  5384. changed += update(this, 'height', box.offsetHeight);
  5385. // limit the width of the this, as browsers cannot draw very wide divs
  5386. if (start < -parentWidth) {
  5387. start = -parentWidth;
  5388. }
  5389. if (end > 2 * parentWidth) {
  5390. end = 2 * parentWidth;
  5391. }
  5392. // when range exceeds left of the window, position the contents at the left of the visible area
  5393. if (start < 0) {
  5394. contentLeft = Math.min(-start,
  5395. (end - start - props.content.width - 2 * padding));
  5396. // TODO: remove the need for options.padding. it's terrible.
  5397. }
  5398. else {
  5399. contentLeft = 0;
  5400. }
  5401. changed += update(props.content, 'left', contentLeft);
  5402. if (orientation == 'top') {
  5403. top = margin;
  5404. changed += update(this, 'top', top);
  5405. }
  5406. else {
  5407. // default or 'bottom'
  5408. top = parent.height - this.height - margin;
  5409. changed += update(this, 'top', top);
  5410. }
  5411. changed += update(this, 'left', start);
  5412. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5413. }
  5414. else {
  5415. changed += 1;
  5416. }
  5417. }
  5418. return (changed > 0);
  5419. };
  5420. /**
  5421. * Create an items DOM
  5422. * @private
  5423. */
  5424. ItemRange.prototype._create = function _create() {
  5425. var dom = this.dom;
  5426. if (!dom) {
  5427. this.dom = dom = {};
  5428. // background box
  5429. dom.box = document.createElement('div');
  5430. // className is updated in repaint()
  5431. // contents box
  5432. dom.content = document.createElement('div');
  5433. dom.content.className = 'content';
  5434. dom.box.appendChild(dom.content);
  5435. }
  5436. };
  5437. /**
  5438. * Reposition the item, recalculate its left, top, and width, using the current
  5439. * range and size of the items itemset
  5440. * @override
  5441. */
  5442. ItemRange.prototype.reposition = function reposition() {
  5443. var dom = this.dom,
  5444. props = this.props;
  5445. if (dom) {
  5446. dom.box.style.top = this.top + 'px';
  5447. dom.box.style.left = this.left + 'px';
  5448. dom.box.style.width = this.width + 'px';
  5449. dom.content.style.left = props.content.left + 'px';
  5450. }
  5451. };
  5452. /**
  5453. * @constructor Group
  5454. * @param {GroupSet} parent
  5455. * @param {Number | String} groupId
  5456. * @param {Object} [options] Options to set initial property values
  5457. * // TODO: describe available options
  5458. * @extends Component
  5459. */
  5460. function Group (parent, groupId, options) {
  5461. this.id = util.randomUUID();
  5462. this.parent = parent;
  5463. this.groupId = groupId;
  5464. this.itemsData = null; // DataSet
  5465. this.itemset = null; // ItemSet
  5466. this.options = options || {};
  5467. this.options.top = 0;
  5468. this.props = {
  5469. label: {
  5470. width: 0,
  5471. height: 0
  5472. }
  5473. };
  5474. this.top = 0;
  5475. this.left = 0;
  5476. this.width = 0;
  5477. this.height = 0;
  5478. }
  5479. Group.prototype = new Component();
  5480. // TODO: comment
  5481. Group.prototype.setOptions = Component.prototype.setOptions;
  5482. /**
  5483. * Get the container element of the panel, which can be used by a child to
  5484. * add its own widgets.
  5485. * @returns {HTMLElement} container
  5486. */
  5487. Group.prototype.getContainer = function () {
  5488. return this.parent.getContainer();
  5489. };
  5490. /**
  5491. * Set item set for the group. The group will create a view on the itemset,
  5492. * filtered by the groups id.
  5493. * @param {DataSet | DataView} items
  5494. */
  5495. Group.prototype.setItems = function setItems(items) {
  5496. if (this.itemset) {
  5497. // remove current item set
  5498. this.itemset.hide();
  5499. this.itemset.setItems();
  5500. this.parent.controller.remove(this.itemset);
  5501. this.itemset = null;
  5502. }
  5503. if (items) {
  5504. var groupId = this.groupId;
  5505. var itemsetOptions = Object.create(this.options);
  5506. this.itemset = new ItemSet(this, null, itemsetOptions);
  5507. this.itemset.setRange(this.parent.range);
  5508. this.view = new DataView(items, {
  5509. filter: function (item) {
  5510. return item.group == groupId;
  5511. }
  5512. });
  5513. this.itemset.setItems(this.view);
  5514. this.parent.controller.add(this.itemset);
  5515. }
  5516. };
  5517. /**
  5518. * Repaint the item
  5519. * @return {Boolean} changed
  5520. */
  5521. Group.prototype.repaint = function repaint() {
  5522. return false;
  5523. };
  5524. /**
  5525. * Reflow the item
  5526. * @return {Boolean} resized
  5527. */
  5528. Group.prototype.reflow = function reflow() {
  5529. var changed = 0,
  5530. update = util.updateProperty;
  5531. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5532. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5533. // TODO: reckon with the height of the group label
  5534. if (this.label) {
  5535. var inner = this.label.firstChild;
  5536. changed += update(this.props.label, 'width', inner.clientWidth);
  5537. changed += update(this.props.label, 'height', inner.clientHeight);
  5538. }
  5539. else {
  5540. changed += update(this.props.label, 'width', 0);
  5541. changed += update(this.props.label, 'height', 0);
  5542. }
  5543. return (changed > 0);
  5544. };
  5545. /**
  5546. * An GroupSet holds a set of groups
  5547. * @param {Component} parent
  5548. * @param {Component[]} [depends] Components on which this components depends
  5549. * (except for the parent)
  5550. * @param {Object} [options] See GroupSet.setOptions for the available
  5551. * options.
  5552. * @constructor GroupSet
  5553. * @extends Panel
  5554. */
  5555. function GroupSet(parent, depends, options) {
  5556. this.id = util.randomUUID();
  5557. this.parent = parent;
  5558. this.depends = depends;
  5559. this.options = options || {};
  5560. this.range = null; // Range or Object {start: number, end: number}
  5561. this.itemsData = null; // DataSet with items
  5562. this.groupsData = null; // DataSet with groups
  5563. this.groups = {}; // map with groups
  5564. this.dom = {};
  5565. this.props = {
  5566. labels: {
  5567. width: 0
  5568. }
  5569. };
  5570. // TODO: implement right orientation of the labels
  5571. // changes in groups are queued key/value map containing id/action
  5572. this.queue = {};
  5573. var me = this;
  5574. this.listeners = {
  5575. 'add': function (event, params) {
  5576. me._onAdd(params.items);
  5577. },
  5578. 'update': function (event, params) {
  5579. me._onUpdate(params.items);
  5580. },
  5581. 'remove': function (event, params) {
  5582. me._onRemove(params.items);
  5583. }
  5584. };
  5585. }
  5586. GroupSet.prototype = new Panel();
  5587. /**
  5588. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5589. * @param {Object} [options] The following options are available:
  5590. * {String | function} groupsOrder
  5591. * TODO: describe options
  5592. */
  5593. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5594. GroupSet.prototype.setRange = function (range) {
  5595. // TODO: implement setRange
  5596. };
  5597. /**
  5598. * Set items
  5599. * @param {vis.DataSet | null} items
  5600. */
  5601. GroupSet.prototype.setItems = function setItems(items) {
  5602. this.itemsData = items;
  5603. for (var id in this.groups) {
  5604. if (this.groups.hasOwnProperty(id)) {
  5605. var group = this.groups[id];
  5606. group.setItems(items);
  5607. }
  5608. }
  5609. };
  5610. /**
  5611. * Get items
  5612. * @return {vis.DataSet | null} items
  5613. */
  5614. GroupSet.prototype.getItems = function getItems() {
  5615. return this.itemsData;
  5616. };
  5617. /**
  5618. * Set range (start and end).
  5619. * @param {Range | Object} range A Range or an object containing start and end.
  5620. */
  5621. GroupSet.prototype.setRange = function setRange(range) {
  5622. this.range = range;
  5623. };
  5624. /**
  5625. * Set groups
  5626. * @param {vis.DataSet} groups
  5627. */
  5628. GroupSet.prototype.setGroups = function setGroups(groups) {
  5629. var me = this,
  5630. ids;
  5631. // unsubscribe from current dataset
  5632. if (this.groupsData) {
  5633. util.forEach(this.listeners, function (callback, event) {
  5634. me.groupsData.unsubscribe(event, callback);
  5635. });
  5636. // remove all drawn groups
  5637. ids = this.groupsData.getIds();
  5638. this._onRemove(ids);
  5639. }
  5640. // replace the dataset
  5641. if (!groups) {
  5642. this.groupsData = null;
  5643. }
  5644. else if (groups instanceof DataSet) {
  5645. this.groupsData = groups;
  5646. }
  5647. else {
  5648. this.groupsData = new DataSet({
  5649. convert: {
  5650. start: 'Date',
  5651. end: 'Date'
  5652. }
  5653. });
  5654. this.groupsData.add(groups);
  5655. }
  5656. if (this.groupsData) {
  5657. // subscribe to new dataset
  5658. var id = this.id;
  5659. util.forEach(this.listeners, function (callback, event) {
  5660. me.groupsData.subscribe(event, callback, id);
  5661. });
  5662. // draw all new groups
  5663. ids = this.groupsData.getIds();
  5664. this._onAdd(ids);
  5665. }
  5666. };
  5667. /**
  5668. * Get groups
  5669. * @return {vis.DataSet | null} groups
  5670. */
  5671. GroupSet.prototype.getGroups = function getGroups() {
  5672. return this.groupsData;
  5673. };
  5674. /**
  5675. * Repaint the component
  5676. * @return {Boolean} changed
  5677. */
  5678. GroupSet.prototype.repaint = function repaint() {
  5679. var changed = 0,
  5680. i, id, group, label,
  5681. update = util.updateProperty,
  5682. asSize = util.option.asSize,
  5683. asElement = util.option.asElement,
  5684. options = this.options,
  5685. frame = this.dom.frame,
  5686. labels = this.dom.labels;
  5687. // create frame
  5688. if (!this.parent) {
  5689. throw new Error('Cannot repaint groupset: no parent attached');
  5690. }
  5691. var parentContainer = this.parent.getContainer();
  5692. if (!parentContainer) {
  5693. throw new Error('Cannot repaint groupset: parent has no container element');
  5694. }
  5695. if (!frame) {
  5696. frame = document.createElement('div');
  5697. frame.className = 'groupset';
  5698. this.dom.frame = frame;
  5699. var className = options.className;
  5700. if (className) {
  5701. util.addClassName(frame, util.option.asString(className));
  5702. }
  5703. changed += 1;
  5704. }
  5705. if (!frame.parentNode) {
  5706. parentContainer.appendChild(frame);
  5707. changed += 1;
  5708. }
  5709. // create labels
  5710. var labelContainer = asElement(options.labelContainer);
  5711. if (!labelContainer) {
  5712. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  5713. }
  5714. if (!labels) {
  5715. labels = document.createElement('div');
  5716. labels.className = 'labels';
  5717. //frame.appendChild(labels);
  5718. this.dom.labels = labels;
  5719. }
  5720. if (!labels.parentNode || labels.parentNode != labelContainer) {
  5721. if (labels.parentNode) {
  5722. labels.parentNode.removeChild(labels.parentNode);
  5723. }
  5724. labelContainer.appendChild(labels);
  5725. }
  5726. // reposition frame
  5727. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  5728. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  5729. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  5730. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  5731. // reposition labels
  5732. changed += update(labels.style, 'top', asSize(options.top, '0px'));
  5733. var me = this,
  5734. queue = this.queue,
  5735. groups = this.groups,
  5736. groupsData = this.groupsData;
  5737. // show/hide added/changed/removed groups
  5738. var ids = Object.keys(queue);
  5739. if (ids.length) {
  5740. ids.forEach(function (id) {
  5741. var action = queue[id];
  5742. var group = groups[id];
  5743. //noinspection FallthroughInSwitchStatementJS
  5744. switch (action) {
  5745. case 'add':
  5746. case 'update':
  5747. if (!group) {
  5748. var groupOptions = Object.create(me.options);
  5749. group = new Group(me, id, groupOptions);
  5750. group.setItems(me.itemsData); // attach items data
  5751. groups[id] = group;
  5752. me.controller.add(group);
  5753. }
  5754. // TODO: update group data
  5755. group.data = groupsData.get(id);
  5756. delete queue[id];
  5757. break;
  5758. case 'remove':
  5759. if (group) {
  5760. group.setItems(); // detach items data
  5761. delete groups[id];
  5762. me.controller.remove(group);
  5763. }
  5764. // update lists
  5765. delete queue[id];
  5766. break;
  5767. default:
  5768. console.log('Error: unknown action "' + action + '"');
  5769. }
  5770. });
  5771. // the groupset depends on each of the groups
  5772. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  5773. // TODO: apply dependencies of the groupset
  5774. // update the top positions of the groups in the correct order
  5775. var orderedGroups = this.groupsData.getIds({
  5776. order: this.options.groupsOrder
  5777. });
  5778. for (i = 0; i < orderedGroups.length; i++) {
  5779. (function (group, prevGroup) {
  5780. var top = 0;
  5781. if (prevGroup) {
  5782. top = function () {
  5783. // TODO: top must reckon with options.maxHeight
  5784. return prevGroup.top + prevGroup.height;
  5785. }
  5786. }
  5787. group.setOptions({
  5788. top: top
  5789. });
  5790. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  5791. }
  5792. // (re)create the labels
  5793. while (labels.firstChild) {
  5794. labels.removeChild(labels.firstChild);
  5795. }
  5796. for (i = 0; i < orderedGroups.length; i++) {
  5797. id = orderedGroups[i];
  5798. label = this._createLabel(id);
  5799. labels.appendChild(label);
  5800. }
  5801. changed++;
  5802. }
  5803. // reposition the labels
  5804. // TODO: labels are not displayed correctly when orientation=='top'
  5805. // TODO: width of labelPanel is not immediately updated on a change in groups
  5806. for (id in groups) {
  5807. if (groups.hasOwnProperty(id)) {
  5808. group = groups[id];
  5809. label = group.label;
  5810. if (label) {
  5811. label.style.top = group.top + 'px';
  5812. label.style.height = group.height + 'px';
  5813. }
  5814. }
  5815. }
  5816. return (changed > 0);
  5817. };
  5818. /**
  5819. * Create a label for group with given id
  5820. * @param {Number} id
  5821. * @return {Element} label
  5822. * @private
  5823. */
  5824. GroupSet.prototype._createLabel = function(id) {
  5825. var group = this.groups[id];
  5826. var label = document.createElement('div');
  5827. label.className = 'label';
  5828. var inner = document.createElement('div');
  5829. inner.className = 'inner';
  5830. label.appendChild(inner);
  5831. var content = group.data && group.data.content;
  5832. if (content instanceof Element) {
  5833. inner.appendChild(content);
  5834. }
  5835. else if (content != undefined) {
  5836. inner.innerHTML = content;
  5837. }
  5838. var className = group.data && group.data.className;
  5839. if (className) {
  5840. util.addClassName(label, className);
  5841. }
  5842. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  5843. return label;
  5844. };
  5845. /**
  5846. * Get container element
  5847. * @return {HTMLElement} container
  5848. */
  5849. GroupSet.prototype.getContainer = function getContainer() {
  5850. return this.dom.frame;
  5851. };
  5852. /**
  5853. * Get the width of the group labels
  5854. * @return {Number} width
  5855. */
  5856. GroupSet.prototype.getLabelsWidth = function getContainer() {
  5857. return this.props.labels.width;
  5858. };
  5859. /**
  5860. * Reflow the component
  5861. * @return {Boolean} resized
  5862. */
  5863. GroupSet.prototype.reflow = function reflow() {
  5864. var changed = 0,
  5865. id, group,
  5866. options = this.options,
  5867. update = util.updateProperty,
  5868. asNumber = util.option.asNumber,
  5869. asSize = util.option.asSize,
  5870. frame = this.dom.frame;
  5871. if (frame) {
  5872. var maxHeight = asNumber(options.maxHeight);
  5873. var fixedHeight = (asSize(options.height) != null);
  5874. var height;
  5875. if (fixedHeight) {
  5876. height = frame.offsetHeight;
  5877. }
  5878. else {
  5879. // height is not specified, calculate the sum of the height of all groups
  5880. height = 0;
  5881. for (id in this.groups) {
  5882. if (this.groups.hasOwnProperty(id)) {
  5883. group = this.groups[id];
  5884. height += group.height;
  5885. }
  5886. }
  5887. }
  5888. if (maxHeight != null) {
  5889. height = Math.min(height, maxHeight);
  5890. }
  5891. changed += update(this, 'height', height);
  5892. changed += update(this, 'top', frame.offsetTop);
  5893. changed += update(this, 'left', frame.offsetLeft);
  5894. changed += update(this, 'width', frame.offsetWidth);
  5895. }
  5896. // calculate the maximum width of the labels
  5897. var width = 0;
  5898. for (id in this.groups) {
  5899. if (this.groups.hasOwnProperty(id)) {
  5900. group = this.groups[id];
  5901. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  5902. width = Math.max(width, labelWidth);
  5903. }
  5904. }
  5905. changed += update(this.props.labels, 'width', width);
  5906. return (changed > 0);
  5907. };
  5908. /**
  5909. * Hide the component from the DOM
  5910. * @return {Boolean} changed
  5911. */
  5912. GroupSet.prototype.hide = function hide() {
  5913. if (this.dom.frame && this.dom.frame.parentNode) {
  5914. this.dom.frame.parentNode.removeChild(this.dom.frame);
  5915. return true;
  5916. }
  5917. else {
  5918. return false;
  5919. }
  5920. };
  5921. /**
  5922. * Show the component in the DOM (when not already visible).
  5923. * A repaint will be executed when the component is not visible
  5924. * @return {Boolean} changed
  5925. */
  5926. GroupSet.prototype.show = function show() {
  5927. if (!this.dom.frame || !this.dom.frame.parentNode) {
  5928. return this.repaint();
  5929. }
  5930. else {
  5931. return false;
  5932. }
  5933. };
  5934. /**
  5935. * Handle updated groups
  5936. * @param {Number[]} ids
  5937. * @private
  5938. */
  5939. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  5940. this._toQueue(ids, 'update');
  5941. };
  5942. /**
  5943. * Handle changed groups
  5944. * @param {Number[]} ids
  5945. * @private
  5946. */
  5947. GroupSet.prototype._onAdd = function _onAdd(ids) {
  5948. this._toQueue(ids, 'add');
  5949. };
  5950. /**
  5951. * Handle removed groups
  5952. * @param {Number[]} ids
  5953. * @private
  5954. */
  5955. GroupSet.prototype._onRemove = function _onRemove(ids) {
  5956. this._toQueue(ids, 'remove');
  5957. };
  5958. /**
  5959. * Put groups in the queue to be added/updated/remove
  5960. * @param {Number[]} ids
  5961. * @param {String} action can be 'add', 'update', 'remove'
  5962. */
  5963. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  5964. var queue = this.queue;
  5965. ids.forEach(function (id) {
  5966. queue[id] = action;
  5967. });
  5968. if (this.controller) {
  5969. //this.requestReflow();
  5970. this.requestRepaint();
  5971. }
  5972. };
  5973. /**
  5974. * Create a timeline visualization
  5975. * @param {HTMLElement} container
  5976. * @param {vis.DataSet | Array | DataTable} [items]
  5977. * @param {Object} [options] See Timeline.setOptions for the available options.
  5978. * @constructor
  5979. */
  5980. function Timeline (container, items, options) {
  5981. var me = this;
  5982. this.options = util.extend({
  5983. orientation: 'bottom',
  5984. min: null,
  5985. max: null,
  5986. zoomMin: 10, // milliseconds
  5987. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  5988. // moveable: true, // TODO: option moveable
  5989. // zoomable: true, // TODO: option zoomable
  5990. showMinorLabels: true,
  5991. showMajorLabels: true,
  5992. autoResize: false
  5993. }, options);
  5994. // controller
  5995. this.controller = new Controller();
  5996. // root panel
  5997. if (!container) {
  5998. throw new Error('No container element provided');
  5999. }
  6000. var rootOptions = Object.create(this.options);
  6001. rootOptions.height = function () {
  6002. if (me.options.height) {
  6003. // fixed height
  6004. return me.options.height;
  6005. }
  6006. else {
  6007. // auto height
  6008. return me.timeaxis.height + me.content.height;
  6009. }
  6010. };
  6011. this.rootPanel = new RootPanel(container, rootOptions);
  6012. this.controller.add(this.rootPanel);
  6013. // item panel
  6014. var itemOptions = Object.create(this.options);
  6015. itemOptions.left = function () {
  6016. return me.labelPanel.width;
  6017. };
  6018. itemOptions.width = function () {
  6019. return me.rootPanel.width - me.labelPanel.width;
  6020. };
  6021. itemOptions.top = null;
  6022. itemOptions.height = null;
  6023. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6024. this.controller.add(this.itemPanel);
  6025. // label panel
  6026. var labelOptions = Object.create(this.options);
  6027. labelOptions.top = null;
  6028. labelOptions.left = null;
  6029. labelOptions.height = null;
  6030. labelOptions.width = function () {
  6031. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6032. return me.content.getLabelsWidth();
  6033. }
  6034. else {
  6035. return 0;
  6036. }
  6037. };
  6038. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6039. this.controller.add(this.labelPanel);
  6040. // range
  6041. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6042. this.range = new Range({
  6043. start: now.clone().add('days', -3).valueOf(),
  6044. end: now.clone().add('days', 4).valueOf()
  6045. });
  6046. /* TODO: fix range options
  6047. var rangeOptions = Object.create(this.options);
  6048. this.range = new Range(rangeOptions);
  6049. this.range.setRange(
  6050. now.clone().add('days', -3).valueOf(),
  6051. now.clone().add('days', 4).valueOf()
  6052. );
  6053. */
  6054. // TODO: reckon with options moveable and zoomable
  6055. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  6056. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  6057. this.range.on('rangechange', function () {
  6058. var force = true;
  6059. me.controller.requestReflow(force);
  6060. });
  6061. this.range.on('rangechanged', function () {
  6062. var force = true;
  6063. me.controller.requestReflow(force);
  6064. });
  6065. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  6066. // time axis
  6067. var timeaxisOptions = Object.create(rootOptions);
  6068. timeaxisOptions.range = this.range;
  6069. timeaxisOptions.left = null;
  6070. timeaxisOptions.top = null;
  6071. timeaxisOptions.width = '100%';
  6072. timeaxisOptions.height = null;
  6073. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6074. this.timeaxis.setRange(this.range);
  6075. this.controller.add(this.timeaxis);
  6076. // create itemset or groupset
  6077. this.setGroups(null);
  6078. this.itemsData = null; // DataSet
  6079. this.groupsData = null; // DataSet
  6080. // set data
  6081. if (items) {
  6082. this.setItems(items);
  6083. }
  6084. }
  6085. /**
  6086. * Set options
  6087. * @param {Object} options TODO: describe the available options
  6088. */
  6089. Timeline.prototype.setOptions = function (options) {
  6090. if (options) {
  6091. util.extend(this.options, options);
  6092. }
  6093. // TODO: apply range min,max
  6094. this.controller.reflow();
  6095. this.controller.repaint();
  6096. };
  6097. /**
  6098. * Set items
  6099. * @param {vis.DataSet | Array | DataTable | null} items
  6100. */
  6101. Timeline.prototype.setItems = function(items) {
  6102. var initialLoad = (this.itemsData == null);
  6103. // convert to type DataSet when needed
  6104. var newItemSet;
  6105. if (!items) {
  6106. newItemSet = null;
  6107. }
  6108. else if (items instanceof DataSet) {
  6109. newItemSet = items;
  6110. }
  6111. if (!(items instanceof DataSet)) {
  6112. newItemSet = new DataSet({
  6113. convert: {
  6114. start: 'Date',
  6115. end: 'Date'
  6116. }
  6117. });
  6118. newItemSet.add(items);
  6119. }
  6120. // set items
  6121. this.itemsData = newItemSet;
  6122. this.content.setItems(newItemSet);
  6123. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6124. // apply the data range as range
  6125. var dataRange = this.getItemRange();
  6126. // add 5% on both sides
  6127. var min = dataRange.min;
  6128. var max = dataRange.max;
  6129. if (min != null && max != null) {
  6130. var interval = (max.valueOf() - min.valueOf());
  6131. min = new Date(min.valueOf() - interval * 0.05);
  6132. max = new Date(max.valueOf() + interval * 0.05);
  6133. }
  6134. // override specified start and/or end date
  6135. if (this.options.start != undefined) {
  6136. min = new Date(this.options.start.valueOf());
  6137. }
  6138. if (this.options.end != undefined) {
  6139. max = new Date(this.options.end.valueOf());
  6140. }
  6141. // apply range if there is a min or max available
  6142. if (min != null || max != null) {
  6143. this.range.setRange(min, max);
  6144. }
  6145. }
  6146. };
  6147. /**
  6148. * Set groups
  6149. * @param {vis.DataSet | Array | DataTable} groups
  6150. */
  6151. Timeline.prototype.setGroups = function(groups) {
  6152. var me = this;
  6153. this.groupsData = groups;
  6154. // switch content type between ItemSet or GroupSet when needed
  6155. var type = this.groupsData ? GroupSet : ItemSet;
  6156. if (!(this.content instanceof type)) {
  6157. // remove old content set
  6158. if (this.content) {
  6159. this.content.hide();
  6160. if (this.content.setItems) {
  6161. this.content.setItems(); // disconnect from items
  6162. }
  6163. if (this.content.setGroups) {
  6164. this.content.setGroups(); // disconnect from groups
  6165. }
  6166. this.controller.remove(this.content);
  6167. }
  6168. // create new content set
  6169. var options = Object.create(this.options);
  6170. util.extend(options, {
  6171. top: function () {
  6172. if (me.options.orientation == 'top') {
  6173. return me.timeaxis.height;
  6174. }
  6175. else {
  6176. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  6177. }
  6178. },
  6179. left: null,
  6180. width: '100%',
  6181. height: function () {
  6182. if (me.options.height) {
  6183. return me.itemPanel.height - me.timeaxis.height;
  6184. }
  6185. else {
  6186. return null;
  6187. }
  6188. },
  6189. maxHeight: function () {
  6190. if (me.options.maxHeight) {
  6191. if (!util.isNumber(me.options.maxHeight)) {
  6192. throw new TypeError('Number expected for property maxHeight');
  6193. }
  6194. return me.options.maxHeight - me.timeaxis.height;
  6195. }
  6196. else {
  6197. return null;
  6198. }
  6199. },
  6200. labelContainer: function () {
  6201. return me.labelPanel.getContainer();
  6202. }
  6203. });
  6204. this.content = new type(this.itemPanel, [this.timeaxis], options);
  6205. if (this.content.setRange) {
  6206. this.content.setRange(this.range);
  6207. }
  6208. if (this.content.setItems) {
  6209. this.content.setItems(this.itemsData);
  6210. }
  6211. if (this.content.setGroups) {
  6212. this.content.setGroups(this.groupsData);
  6213. }
  6214. this.controller.add(this.content);
  6215. }
  6216. };
  6217. /**
  6218. * Get the data range of the item set.
  6219. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6220. * When no minimum is found, min==null
  6221. * When no maximum is found, max==null
  6222. */
  6223. Timeline.prototype.getItemRange = function getItemRange() {
  6224. // calculate min from start filed
  6225. var itemsData = this.itemsData,
  6226. min = null,
  6227. max = null;
  6228. if (itemsData) {
  6229. // calculate the minimum value of the field 'start'
  6230. var minItem = itemsData.min('start');
  6231. min = minItem ? minItem.start.valueOf() : null;
  6232. // calculate maximum value of fields 'start' and 'end'
  6233. var maxStartItem = itemsData.max('start');
  6234. if (maxStartItem) {
  6235. max = maxStartItem.start.valueOf();
  6236. }
  6237. var maxEndItem = itemsData.max('end');
  6238. if (maxEndItem) {
  6239. if (max == null) {
  6240. max = maxEndItem.end.valueOf();
  6241. }
  6242. else {
  6243. max = Math.max(max, maxEndItem.end.valueOf());
  6244. }
  6245. }
  6246. }
  6247. return {
  6248. min: (min != null) ? new Date(min) : null,
  6249. max: (max != null) ? new Date(max) : null
  6250. };
  6251. };
  6252. (function(exports) {
  6253. /**
  6254. * Parse a text source containing data in DOT language into a JSON object.
  6255. * The object contains two lists: one with nodes and one with edges.
  6256. *
  6257. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6258. *
  6259. * @param {String} data Text containing a graph in DOT-notation
  6260. * @return {Object} graph An object containing two parameters:
  6261. * {Object[]} nodes
  6262. * {Object[]} edges
  6263. */
  6264. function parseDOT (data) {
  6265. dot = data;
  6266. return parseGraph();
  6267. }
  6268. // token types enumeration
  6269. var TOKENTYPE = {
  6270. NULL : 0,
  6271. DELIMITER : 1,
  6272. IDENTIFIER: 2,
  6273. UNKNOWN : 3
  6274. };
  6275. // map with all delimiters
  6276. var DELIMITERS = {
  6277. '{': true,
  6278. '}': true,
  6279. '[': true,
  6280. ']': true,
  6281. ';': true,
  6282. '=': true,
  6283. ',': true,
  6284. '->': true,
  6285. '--': true
  6286. };
  6287. var dot = ''; // current dot file
  6288. var index = 0; // current index in dot file
  6289. var c = ''; // current token character in expr
  6290. var token = ''; // current token
  6291. var tokenType = TOKENTYPE.NULL; // type of the token
  6292. /**
  6293. * Get the first character from the dot file.
  6294. * The character is stored into the char c. If the end of the dot file is
  6295. * reached, the function puts an empty string in c.
  6296. */
  6297. function first() {
  6298. index = 0;
  6299. c = dot.charAt(0);
  6300. }
  6301. /**
  6302. * Get the next character from the dot file.
  6303. * The character is stored into the char c. If the end of the dot file is
  6304. * reached, the function puts an empty string in c.
  6305. */
  6306. function next() {
  6307. index++;
  6308. c = dot.charAt(index);
  6309. }
  6310. /**
  6311. * Preview the next character from the dot file.
  6312. * @return {String} cNext
  6313. */
  6314. function nextPreview() {
  6315. return dot.charAt(index + 1);
  6316. }
  6317. /**
  6318. * Test whether given character is alphabetic or numeric
  6319. * @param {String} c
  6320. * @return {Boolean} isAlphaNumeric
  6321. */
  6322. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6323. function isAlphaNumeric(c) {
  6324. return regexAlphaNumeric.test(c);
  6325. }
  6326. /**
  6327. * Merge all properties of object b into object b
  6328. * @param {Object} a
  6329. * @param {Object} b
  6330. * @return {Object} a
  6331. */
  6332. function merge (a, b) {
  6333. if (!a) {
  6334. a = {};
  6335. }
  6336. if (b) {
  6337. for (var name in b) {
  6338. if (b.hasOwnProperty(name)) {
  6339. a[name] = b[name];
  6340. }
  6341. }
  6342. }
  6343. return a;
  6344. }
  6345. /**
  6346. * Set a value in an object, where the provided parameter name can be a
  6347. * path with nested parameters. For example:
  6348. *
  6349. * var obj = {a: 2};
  6350. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6351. *
  6352. * @param {Object} obj
  6353. * @param {String} path A parameter name or dot-separated parameter path,
  6354. * like "color.highlight.border".
  6355. * @param {*} value
  6356. */
  6357. function setValue(obj, path, value) {
  6358. var keys = path.split('.');
  6359. var o = obj;
  6360. while (keys.length) {
  6361. var key = keys.shift();
  6362. if (keys.length) {
  6363. // this isn't the end point
  6364. if (!o[key]) {
  6365. o[key] = {};
  6366. }
  6367. o = o[key];
  6368. }
  6369. else {
  6370. // this is the end point
  6371. o[key] = value;
  6372. }
  6373. }
  6374. }
  6375. /**
  6376. * Add a node to a graph object. If there is already a node with
  6377. * the same id, their attributes will be merged.
  6378. * @param {Object} graph
  6379. * @param {Object} node
  6380. */
  6381. function addNode(graph, node) {
  6382. var i, len;
  6383. var current = null;
  6384. // find root graph (in case of subgraph)
  6385. var graphs = [graph]; // list with all graphs from current graph to root graph
  6386. var root = graph;
  6387. while (root.parent) {
  6388. graphs.push(root.parent);
  6389. root = root.parent;
  6390. }
  6391. // find existing node (at root level) by its id
  6392. if (root.nodes) {
  6393. for (i = 0, len = root.nodes.length; i < len; i++) {
  6394. if (node.id === root.nodes[i].id) {
  6395. current = root.nodes[i];
  6396. break;
  6397. }
  6398. }
  6399. }
  6400. if (!current) {
  6401. // this is a new node
  6402. current = {
  6403. id: node.id
  6404. };
  6405. if (graph.node) {
  6406. // clone default attributes
  6407. current.attr = merge(current.attr, graph.node);
  6408. }
  6409. }
  6410. // add node to this (sub)graph and all its parent graphs
  6411. for (i = graphs.length - 1; i >= 0; i--) {
  6412. var g = graphs[i];
  6413. if (!g.nodes) {
  6414. g.nodes = [];
  6415. }
  6416. if (g.nodes.indexOf(current) == -1) {
  6417. g.nodes.push(current);
  6418. }
  6419. }
  6420. // merge attributes
  6421. if (node.attr) {
  6422. current.attr = merge(current.attr, node.attr);
  6423. }
  6424. }
  6425. /**
  6426. * Add an edge to a graph object
  6427. * @param {Object} graph
  6428. * @param {Object} edge
  6429. */
  6430. function addEdge(graph, edge) {
  6431. if (!graph.edges) {
  6432. graph.edges = [];
  6433. }
  6434. graph.edges.push(edge);
  6435. if (graph.edge) {
  6436. var attr = merge({}, graph.edge); // clone default attributes
  6437. edge.attr = merge(attr, edge.attr); // merge attributes
  6438. }
  6439. }
  6440. /**
  6441. * Create an edge to a graph object
  6442. * @param {Object} graph
  6443. * @param {String | Number | Object} from
  6444. * @param {String | Number | Object} to
  6445. * @param {String} type
  6446. * @param {Object | null} attr
  6447. * @return {Object} edge
  6448. */
  6449. function createEdge(graph, from, to, type, attr) {
  6450. var edge = {
  6451. from: from,
  6452. to: to,
  6453. type: type
  6454. };
  6455. if (graph.edge) {
  6456. edge.attr = merge({}, graph.edge); // clone default attributes
  6457. }
  6458. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6459. return edge;
  6460. }
  6461. /**
  6462. * Get next token in the current dot file.
  6463. * The token and token type are available as token and tokenType
  6464. */
  6465. function getToken() {
  6466. tokenType = TOKENTYPE.NULL;
  6467. token = '';
  6468. // skip over whitespaces
  6469. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6470. next();
  6471. }
  6472. do {
  6473. var isComment = false;
  6474. // skip comment
  6475. if (c == '#') {
  6476. // find the previous non-space character
  6477. var i = index - 1;
  6478. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  6479. i--;
  6480. }
  6481. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  6482. // the # is at the start of a line, this is indeed a line comment
  6483. while (c != '' && c != '\n') {
  6484. next();
  6485. }
  6486. isComment = true;
  6487. }
  6488. }
  6489. if (c == '/' && nextPreview() == '/') {
  6490. // skip line comment
  6491. while (c != '' && c != '\n') {
  6492. next();
  6493. }
  6494. isComment = true;
  6495. }
  6496. if (c == '/' && nextPreview() == '*') {
  6497. // skip block comment
  6498. while (c != '') {
  6499. if (c == '*' && nextPreview() == '/') {
  6500. // end of block comment found. skip these last two characters
  6501. next();
  6502. next();
  6503. break;
  6504. }
  6505. else {
  6506. next();
  6507. }
  6508. }
  6509. isComment = true;
  6510. }
  6511. // skip over whitespaces
  6512. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6513. next();
  6514. }
  6515. }
  6516. while (isComment);
  6517. // check for end of dot file
  6518. if (c == '') {
  6519. // token is still empty
  6520. tokenType = TOKENTYPE.DELIMITER;
  6521. return;
  6522. }
  6523. // check for delimiters consisting of 2 characters
  6524. var c2 = c + nextPreview();
  6525. if (DELIMITERS[c2]) {
  6526. tokenType = TOKENTYPE.DELIMITER;
  6527. token = c2;
  6528. next();
  6529. next();
  6530. return;
  6531. }
  6532. // check for delimiters consisting of 1 character
  6533. if (DELIMITERS[c]) {
  6534. tokenType = TOKENTYPE.DELIMITER;
  6535. token = c;
  6536. next();
  6537. return;
  6538. }
  6539. // check for an identifier (number or string)
  6540. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  6541. if (isAlphaNumeric(c) || c == '-') {
  6542. token += c;
  6543. next();
  6544. while (isAlphaNumeric(c)) {
  6545. token += c;
  6546. next();
  6547. }
  6548. if (token == 'false') {
  6549. token = false; // convert to boolean
  6550. }
  6551. else if (token == 'true') {
  6552. token = true; // convert to boolean
  6553. }
  6554. else if (!isNaN(Number(token))) {
  6555. token = Number(token); // convert to number
  6556. }
  6557. tokenType = TOKENTYPE.IDENTIFIER;
  6558. return;
  6559. }
  6560. // check for a string enclosed by double quotes
  6561. if (c == '"') {
  6562. next();
  6563. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  6564. token += c;
  6565. if (c == '"') { // skip the escape character
  6566. next();
  6567. }
  6568. next();
  6569. }
  6570. if (c != '"') {
  6571. throw newSyntaxError('End of string " expected');
  6572. }
  6573. next();
  6574. tokenType = TOKENTYPE.IDENTIFIER;
  6575. return;
  6576. }
  6577. // something unknown is found, wrong characters, a syntax error
  6578. tokenType = TOKENTYPE.UNKNOWN;
  6579. while (c != '') {
  6580. token += c;
  6581. next();
  6582. }
  6583. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  6584. }
  6585. /**
  6586. * Parse a graph.
  6587. * @returns {Object} graph
  6588. */
  6589. function parseGraph() {
  6590. var graph = {};
  6591. first();
  6592. getToken();
  6593. // optional strict keyword
  6594. if (token == 'strict') {
  6595. graph.strict = true;
  6596. getToken();
  6597. }
  6598. // graph or digraph keyword
  6599. if (token == 'graph' || token == 'digraph') {
  6600. graph.type = token;
  6601. getToken();
  6602. }
  6603. // optional graph id
  6604. if (tokenType == TOKENTYPE.IDENTIFIER) {
  6605. graph.id = token;
  6606. getToken();
  6607. }
  6608. // open angle bracket
  6609. if (token != '{') {
  6610. throw newSyntaxError('Angle bracket { expected');
  6611. }
  6612. getToken();
  6613. // statements
  6614. parseStatements(graph);
  6615. // close angle bracket
  6616. if (token != '}') {
  6617. throw newSyntaxError('Angle bracket } expected');
  6618. }
  6619. getToken();
  6620. // end of file
  6621. if (token !== '') {
  6622. throw newSyntaxError('End of file expected');
  6623. }
  6624. getToken();
  6625. // remove temporary default properties
  6626. delete graph.node;
  6627. delete graph.edge;
  6628. delete graph.graph;
  6629. return graph;
  6630. }
  6631. /**
  6632. * Parse a list with statements.
  6633. * @param {Object} graph
  6634. */
  6635. function parseStatements (graph) {
  6636. while (token !== '' && token != '}') {
  6637. parseStatement(graph);
  6638. if (token == ';') {
  6639. getToken();
  6640. }
  6641. }
  6642. }
  6643. /**
  6644. * Parse a single statement. Can be a an attribute statement, node
  6645. * statement, a series of node statements and edge statements, or a
  6646. * parameter.
  6647. * @param {Object} graph
  6648. */
  6649. function parseStatement(graph) {
  6650. // parse subgraph
  6651. var subgraph = parseSubgraph(graph);
  6652. if (subgraph) {
  6653. // edge statements
  6654. parseEdge(graph, subgraph);
  6655. return;
  6656. }
  6657. // parse an attribute statement
  6658. var attr = parseAttributeStatement(graph);
  6659. if (attr) {
  6660. return;
  6661. }
  6662. // parse node
  6663. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6664. throw newSyntaxError('Identifier expected');
  6665. }
  6666. var id = token; // id can be a string or a number
  6667. getToken();
  6668. if (token == '=') {
  6669. // id statement
  6670. getToken();
  6671. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6672. throw newSyntaxError('Identifier expected');
  6673. }
  6674. graph[id] = token;
  6675. getToken();
  6676. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  6677. }
  6678. else {
  6679. parseNodeStatement(graph, id);
  6680. }
  6681. }
  6682. /**
  6683. * Parse a subgraph
  6684. * @param {Object} graph parent graph object
  6685. * @return {Object | null} subgraph
  6686. */
  6687. function parseSubgraph (graph) {
  6688. var subgraph = null;
  6689. // optional subgraph keyword
  6690. if (token == 'subgraph') {
  6691. subgraph = {};
  6692. subgraph.type = 'subgraph';
  6693. getToken();
  6694. // optional graph id
  6695. if (tokenType == TOKENTYPE.IDENTIFIER) {
  6696. subgraph.id = token;
  6697. getToken();
  6698. }
  6699. }
  6700. // open angle bracket
  6701. if (token == '{') {
  6702. getToken();
  6703. if (!subgraph) {
  6704. subgraph = {};
  6705. }
  6706. subgraph.parent = graph;
  6707. subgraph.node = graph.node;
  6708. subgraph.edge = graph.edge;
  6709. subgraph.graph = graph.graph;
  6710. // statements
  6711. parseStatements(subgraph);
  6712. // close angle bracket
  6713. if (token != '}') {
  6714. throw newSyntaxError('Angle bracket } expected');
  6715. }
  6716. getToken();
  6717. // remove temporary default properties
  6718. delete subgraph.node;
  6719. delete subgraph.edge;
  6720. delete subgraph.graph;
  6721. delete subgraph.parent;
  6722. // register at the parent graph
  6723. if (!graph.subgraphs) {
  6724. graph.subgraphs = [];
  6725. }
  6726. graph.subgraphs.push(subgraph);
  6727. }
  6728. return subgraph;
  6729. }
  6730. /**
  6731. * parse an attribute statement like "node [shape=circle fontSize=16]".
  6732. * Available keywords are 'node', 'edge', 'graph'.
  6733. * The previous list with default attributes will be replaced
  6734. * @param {Object} graph
  6735. * @returns {String | null} keyword Returns the name of the parsed attribute
  6736. * (node, edge, graph), or null if nothing
  6737. * is parsed.
  6738. */
  6739. function parseAttributeStatement (graph) {
  6740. // attribute statements
  6741. if (token == 'node') {
  6742. getToken();
  6743. // node attributes
  6744. graph.node = parseAttributeList();
  6745. return 'node';
  6746. }
  6747. else if (token == 'edge') {
  6748. getToken();
  6749. // edge attributes
  6750. graph.edge = parseAttributeList();
  6751. return 'edge';
  6752. }
  6753. else if (token == 'graph') {
  6754. getToken();
  6755. // graph attributes
  6756. graph.graph = parseAttributeList();
  6757. return 'graph';
  6758. }
  6759. return null;
  6760. }
  6761. /**
  6762. * parse a node statement
  6763. * @param {Object} graph
  6764. * @param {String | Number} id
  6765. */
  6766. function parseNodeStatement(graph, id) {
  6767. // node statement
  6768. var node = {
  6769. id: id
  6770. };
  6771. var attr = parseAttributeList();
  6772. if (attr) {
  6773. node.attr = attr;
  6774. }
  6775. addNode(graph, node);
  6776. // edge statements
  6777. parseEdge(graph, id);
  6778. }
  6779. /**
  6780. * Parse an edge or a series of edges
  6781. * @param {Object} graph
  6782. * @param {String | Number} from Id of the from node
  6783. */
  6784. function parseEdge(graph, from) {
  6785. while (token == '->' || token == '--') {
  6786. var to;
  6787. var type = token;
  6788. getToken();
  6789. var subgraph = parseSubgraph(graph);
  6790. if (subgraph) {
  6791. to = subgraph;
  6792. }
  6793. else {
  6794. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6795. throw newSyntaxError('Identifier or subgraph expected');
  6796. }
  6797. to = token;
  6798. addNode(graph, {
  6799. id: to
  6800. });
  6801. getToken();
  6802. }
  6803. // parse edge attributes
  6804. var attr = parseAttributeList();
  6805. // create edge
  6806. var edge = createEdge(graph, from, to, type, attr);
  6807. addEdge(graph, edge);
  6808. from = to;
  6809. }
  6810. }
  6811. /**
  6812. * Parse a set with attributes,
  6813. * for example [label="1.000", shape=solid]
  6814. * @return {Object | null} attr
  6815. */
  6816. function parseAttributeList() {
  6817. var attr = null;
  6818. while (token == '[') {
  6819. getToken();
  6820. attr = {};
  6821. while (token !== '' && token != ']') {
  6822. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6823. throw newSyntaxError('Attribute name expected');
  6824. }
  6825. var name = token;
  6826. getToken();
  6827. if (token != '=') {
  6828. throw newSyntaxError('Equal sign = expected');
  6829. }
  6830. getToken();
  6831. if (tokenType != TOKENTYPE.IDENTIFIER) {
  6832. throw newSyntaxError('Attribute value expected');
  6833. }
  6834. var value = token;
  6835. setValue(attr, name, value); // name can be a path
  6836. getToken();
  6837. if (token ==',') {
  6838. getToken();
  6839. }
  6840. }
  6841. if (token != ']') {
  6842. throw newSyntaxError('Bracket ] expected');
  6843. }
  6844. getToken();
  6845. }
  6846. return attr;
  6847. }
  6848. /**
  6849. * Create a syntax error with extra information on current token and index.
  6850. * @param {String} message
  6851. * @returns {SyntaxError} err
  6852. */
  6853. function newSyntaxError(message) {
  6854. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  6855. }
  6856. /**
  6857. * Chop off text after a maximum length
  6858. * @param {String} text
  6859. * @param {Number} maxLength
  6860. * @returns {String}
  6861. */
  6862. function chop (text, maxLength) {
  6863. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  6864. }
  6865. /**
  6866. * Execute a function fn for each pair of elements in two arrays
  6867. * @param {Array | *} array1
  6868. * @param {Array | *} array2
  6869. * @param {function} fn
  6870. */
  6871. function forEach2(array1, array2, fn) {
  6872. if (array1 instanceof Array) {
  6873. array1.forEach(function (elem1) {
  6874. if (array2 instanceof Array) {
  6875. array2.forEach(function (elem2) {
  6876. fn(elem1, elem2);
  6877. });
  6878. }
  6879. else {
  6880. fn(elem1, array2);
  6881. }
  6882. });
  6883. }
  6884. else {
  6885. if (array2 instanceof Array) {
  6886. array2.forEach(function (elem2) {
  6887. fn(array1, elem2);
  6888. });
  6889. }
  6890. else {
  6891. fn(array1, array2);
  6892. }
  6893. }
  6894. }
  6895. /**
  6896. * Convert a string containing a graph in DOT language into a map containing
  6897. * with nodes and edges in the format of graph.
  6898. * @param {String} data Text containing a graph in DOT-notation
  6899. * @return {Object} graphData
  6900. */
  6901. function DOTToGraph (data) {
  6902. // parse the DOT file
  6903. var dotData = parseDOT(data);
  6904. var graphData = {
  6905. nodes: [],
  6906. edges: [],
  6907. options: {}
  6908. };
  6909. // copy the nodes
  6910. if (dotData.nodes) {
  6911. dotData.nodes.forEach(function (dotNode) {
  6912. var graphNode = {
  6913. id: dotNode.id,
  6914. label: String(dotNode.label || dotNode.id)
  6915. };
  6916. merge(graphNode, dotNode.attr);
  6917. if (graphNode.image) {
  6918. graphNode.shape = 'image';
  6919. }
  6920. graphData.nodes.push(graphNode);
  6921. });
  6922. }
  6923. // copy the edges
  6924. if (dotData.edges) {
  6925. /**
  6926. * Convert an edge in DOT format to an edge with VisGraph format
  6927. * @param {Object} dotEdge
  6928. * @returns {Object} graphEdge
  6929. */
  6930. function convertEdge(dotEdge) {
  6931. var graphEdge = {
  6932. from: dotEdge.from,
  6933. to: dotEdge.to
  6934. };
  6935. merge(graphEdge, dotEdge.attr);
  6936. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  6937. return graphEdge;
  6938. }
  6939. dotData.edges.forEach(function (dotEdge) {
  6940. var from, to;
  6941. if (dotEdge.from instanceof Object) {
  6942. from = dotEdge.from.nodes;
  6943. }
  6944. else {
  6945. from = {
  6946. id: dotEdge.from
  6947. }
  6948. }
  6949. if (dotEdge.to instanceof Object) {
  6950. to = dotEdge.to.nodes;
  6951. }
  6952. else {
  6953. to = {
  6954. id: dotEdge.to
  6955. }
  6956. }
  6957. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  6958. dotEdge.from.edges.forEach(function (subEdge) {
  6959. var graphEdge = convertEdge(subEdge);
  6960. graphData.edges.push(graphEdge);
  6961. });
  6962. }
  6963. forEach2(from, to, function (from, to) {
  6964. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  6965. var graphEdge = convertEdge(subEdge);
  6966. graphData.edges.push(graphEdge);
  6967. });
  6968. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  6969. dotEdge.to.edges.forEach(function (subEdge) {
  6970. var graphEdge = convertEdge(subEdge);
  6971. graphData.edges.push(graphEdge);
  6972. });
  6973. }
  6974. });
  6975. }
  6976. // copy the options
  6977. if (dotData.attr) {
  6978. graphData.options = dotData.attr;
  6979. }
  6980. return graphData;
  6981. }
  6982. // exports
  6983. exports.parseDOT = parseDOT;
  6984. exports.DOTToGraph = DOTToGraph;
  6985. })(typeof util !== 'undefined' ? util : exports);
  6986. /**
  6987. * Canvas shapes used by the Graph
  6988. */
  6989. if (typeof CanvasRenderingContext2D !== 'undefined') {
  6990. /**
  6991. * Draw a circle shape
  6992. */
  6993. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  6994. this.beginPath();
  6995. this.arc(x, y, r, 0, 2*Math.PI, false);
  6996. };
  6997. /**
  6998. * Draw a square shape
  6999. * @param {Number} x horizontal center
  7000. * @param {Number} y vertical center
  7001. * @param {Number} r size, width and height of the square
  7002. */
  7003. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7004. this.beginPath();
  7005. this.rect(x - r, y - r, r * 2, r * 2);
  7006. };
  7007. /**
  7008. * Draw a triangle shape
  7009. * @param {Number} x horizontal center
  7010. * @param {Number} y vertical center
  7011. * @param {Number} r radius, half the length of the sides of the triangle
  7012. */
  7013. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7014. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7015. this.beginPath();
  7016. var s = r * 2;
  7017. var s2 = s / 2;
  7018. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7019. var h = Math.sqrt(s * s - s2 * s2); // height
  7020. this.moveTo(x, y - (h - ir));
  7021. this.lineTo(x + s2, y + ir);
  7022. this.lineTo(x - s2, y + ir);
  7023. this.lineTo(x, y - (h - ir));
  7024. this.closePath();
  7025. };
  7026. /**
  7027. * Draw a triangle shape in downward orientation
  7028. * @param {Number} x horizontal center
  7029. * @param {Number} y vertical center
  7030. * @param {Number} r radius
  7031. */
  7032. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7033. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7034. this.beginPath();
  7035. var s = r * 2;
  7036. var s2 = s / 2;
  7037. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7038. var h = Math.sqrt(s * s - s2 * s2); // height
  7039. this.moveTo(x, y + (h - ir));
  7040. this.lineTo(x + s2, y - ir);
  7041. this.lineTo(x - s2, y - ir);
  7042. this.lineTo(x, y + (h - ir));
  7043. this.closePath();
  7044. };
  7045. /**
  7046. * Draw a star shape, a star with 5 points
  7047. * @param {Number} x horizontal center
  7048. * @param {Number} y vertical center
  7049. * @param {Number} r radius, half the length of the sides of the triangle
  7050. */
  7051. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7052. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7053. this.beginPath();
  7054. for (var n = 0; n < 10; n++) {
  7055. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7056. this.lineTo(
  7057. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7058. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7059. );
  7060. }
  7061. this.closePath();
  7062. };
  7063. /**
  7064. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7065. */
  7066. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7067. var r2d = Math.PI/180;
  7068. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7069. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7070. this.beginPath();
  7071. this.moveTo(x+r,y);
  7072. this.lineTo(x+w-r,y);
  7073. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7074. this.lineTo(x+w,y+h-r);
  7075. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7076. this.lineTo(x+r,y+h);
  7077. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7078. this.lineTo(x,y+r);
  7079. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7080. };
  7081. /**
  7082. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7083. */
  7084. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7085. var kappa = .5522848,
  7086. ox = (w / 2) * kappa, // control point offset horizontal
  7087. oy = (h / 2) * kappa, // control point offset vertical
  7088. xe = x + w, // x-end
  7089. ye = y + h, // y-end
  7090. xm = x + w / 2, // x-middle
  7091. ym = y + h / 2; // y-middle
  7092. this.beginPath();
  7093. this.moveTo(x, ym);
  7094. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7095. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7096. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7097. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7098. };
  7099. /**
  7100. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7101. */
  7102. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7103. var f = 1/3;
  7104. var wEllipse = w;
  7105. var hEllipse = h * f;
  7106. var kappa = .5522848,
  7107. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7108. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7109. xe = x + wEllipse, // x-end
  7110. ye = y + hEllipse, // y-end
  7111. xm = x + wEllipse / 2, // x-middle
  7112. ym = y + hEllipse / 2, // y-middle
  7113. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7114. yeb = y + h; // y-end, bottom ellipse
  7115. this.beginPath();
  7116. this.moveTo(xe, ym);
  7117. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7118. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7119. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7120. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7121. this.lineTo(xe, ymb);
  7122. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7123. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7124. this.lineTo(x, ym);
  7125. };
  7126. /**
  7127. * Draw an arrow point (no line)
  7128. */
  7129. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7130. // tail
  7131. var xt = x - length * Math.cos(angle);
  7132. var yt = y - length * Math.sin(angle);
  7133. // inner tail
  7134. // TODO: allow to customize different shapes
  7135. var xi = x - length * 0.9 * Math.cos(angle);
  7136. var yi = y - length * 0.9 * Math.sin(angle);
  7137. // left
  7138. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7139. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7140. // right
  7141. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7142. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7143. this.beginPath();
  7144. this.moveTo(x, y);
  7145. this.lineTo(xl, yl);
  7146. this.lineTo(xi, yi);
  7147. this.lineTo(xr, yr);
  7148. this.closePath();
  7149. };
  7150. /**
  7151. * Sets up the dashedLine functionality for drawing
  7152. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7153. * @author David Jordan
  7154. * @date 2012-08-08
  7155. */
  7156. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7157. if (!dashArray) dashArray=[10,5];
  7158. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7159. var dashCount = dashArray.length;
  7160. this.moveTo(x, y);
  7161. var dx = (x2-x), dy = (y2-y);
  7162. var slope = dy/dx;
  7163. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7164. var dashIndex=0, draw=true;
  7165. while (distRemaining>=0.1){
  7166. var dashLength = dashArray[dashIndex++%dashCount];
  7167. if (dashLength > distRemaining) dashLength = distRemaining;
  7168. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7169. if (dx<0) xStep = -xStep;
  7170. x += xStep;
  7171. y += slope*xStep;
  7172. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7173. distRemaining -= dashLength;
  7174. draw = !draw;
  7175. }
  7176. };
  7177. // TODO: add diamond shape
  7178. }
  7179. /**
  7180. * @class Node
  7181. * A node. A node can be connected to other nodes via one or multiple edges.
  7182. * @param {object} properties An object containing properties for the node. All
  7183. * properties are optional, except for the id.
  7184. * {number} id Id of the node. Required
  7185. * {string} label Text label for the node
  7186. * {number} x Horizontal position of the node
  7187. * {number} y Vertical position of the node
  7188. * {string} shape Node shape, available:
  7189. * "database", "circle", "ellipse",
  7190. * "box", "image", "text", "dot",
  7191. * "star", "triangle", "triangleDown",
  7192. * "square"
  7193. * {string} image An image url
  7194. * {string} title An title text, can be HTML
  7195. * {anytype} group A group name or number
  7196. * @param {Graph.Images} imagelist A list with images. Only needed
  7197. * when the node has an image
  7198. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7199. * retrieving group properties
  7200. * @param {Object} constants An object with default values for
  7201. * example for the color
  7202. */
  7203. function Node(properties, imagelist, grouplist, constants) {
  7204. this.selected = false;
  7205. this.edges = []; // all edges connected to this node
  7206. this.group = constants.nodes.group;
  7207. this.fontSize = constants.nodes.fontSize;
  7208. this.fontFace = constants.nodes.fontFace;
  7209. this.fontColor = constants.nodes.fontColor;
  7210. this.color = constants.nodes.color;
  7211. // set defaults for the properties
  7212. this.id = undefined;
  7213. this.shape = constants.nodes.shape;
  7214. this.image = constants.nodes.image;
  7215. this.x = 0;
  7216. this.y = 0;
  7217. this.xFixed = false;
  7218. this.yFixed = false;
  7219. this.radius = constants.nodes.radius;
  7220. this.radiusFixed = false;
  7221. this.radiusMin = constants.nodes.radiusMin;
  7222. this.radiusMax = constants.nodes.radiusMax;
  7223. this.imagelist = imagelist;
  7224. this.grouplist = grouplist;
  7225. this.setProperties(properties, constants);
  7226. // mass, force, velocity
  7227. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  7228. this.fx = 0.0; // external force x
  7229. this.fy = 0.0; // external force y
  7230. this.vx = 0.0; // velocity x
  7231. this.vy = 0.0; // velocity y
  7232. this.minForce = constants.minForce;
  7233. this.damping = 0.9; // damping factor
  7234. };
  7235. /**
  7236. * Attach a edge to the node
  7237. * @param {Edge} edge
  7238. */
  7239. Node.prototype.attachEdge = function(edge) {
  7240. if (this.edges.indexOf(edge) == -1) {
  7241. this.edges.push(edge);
  7242. }
  7243. this._updateMass();
  7244. };
  7245. /**
  7246. * Detach a edge from the node
  7247. * @param {Edge} edge
  7248. */
  7249. Node.prototype.detachEdge = function(edge) {
  7250. var index = this.edges.indexOf(edge);
  7251. if (index != -1) {
  7252. this.edges.splice(index, 1);
  7253. }
  7254. this._updateMass();
  7255. };
  7256. /**
  7257. * Update the nodes mass, which is determined by the number of edges connecting
  7258. * to it (more edges -> heavier node).
  7259. * @private
  7260. */
  7261. Node.prototype._updateMass = function() {
  7262. this.mass = 50 + 20 * this.edges.length; // kg
  7263. };
  7264. /**
  7265. * Set or overwrite properties for the node
  7266. * @param {Object} properties an object with properties
  7267. * @param {Object} constants and object with default, global properties
  7268. */
  7269. Node.prototype.setProperties = function(properties, constants) {
  7270. if (!properties) {
  7271. return;
  7272. }
  7273. // basic properties
  7274. if (properties.id != undefined) {this.id = properties.id;}
  7275. if (properties.label != undefined) {this.label = properties.label;}
  7276. if (properties.title != undefined) {this.title = properties.title;}
  7277. if (properties.group != undefined) {this.group = properties.group;}
  7278. if (properties.x != undefined) {this.x = properties.x;}
  7279. if (properties.y != undefined) {this.y = properties.y;}
  7280. if (properties.value != undefined) {this.value = properties.value;}
  7281. if (this.id === undefined) {
  7282. throw "Node must have an id";
  7283. }
  7284. // copy group properties
  7285. if (this.group) {
  7286. var groupObj = this.grouplist.get(this.group);
  7287. for (var prop in groupObj) {
  7288. if (groupObj.hasOwnProperty(prop)) {
  7289. this[prop] = groupObj[prop];
  7290. }
  7291. }
  7292. }
  7293. // individual shape properties
  7294. if (properties.shape != undefined) {this.shape = properties.shape;}
  7295. if (properties.image != undefined) {this.image = properties.image;}
  7296. if (properties.radius != undefined) {this.radius = properties.radius;}
  7297. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  7298. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7299. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7300. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7301. if (this.image != undefined) {
  7302. if (this.imagelist) {
  7303. this.imageObj = this.imagelist.load(this.image);
  7304. }
  7305. else {
  7306. throw "No imagelist provided";
  7307. }
  7308. }
  7309. this.xFixed = this.xFixed || (properties.x != undefined);
  7310. this.yFixed = this.yFixed || (properties.y != undefined);
  7311. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  7312. if (this.shape == 'image') {
  7313. this.radiusMin = constants.nodes.widthMin;
  7314. this.radiusMax = constants.nodes.widthMax;
  7315. }
  7316. // choose draw method depending on the shape
  7317. switch (this.shape) {
  7318. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7319. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7320. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7321. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7322. // TODO: add diamond shape
  7323. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7324. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7325. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7326. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7327. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7328. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7329. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7330. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7331. }
  7332. // reset the size of the node, this can be changed
  7333. this._reset();
  7334. };
  7335. /**
  7336. * Parse a color property into an object with border, background, and
  7337. * hightlight colors
  7338. * @param {Object | String} color
  7339. * @return {Object} colorObject
  7340. */
  7341. Node.parseColor = function(color) {
  7342. var c;
  7343. if (util.isString(color)) {
  7344. c = {
  7345. border: color,
  7346. background: color,
  7347. highlight: {
  7348. border: color,
  7349. background: color
  7350. }
  7351. };
  7352. // TODO: automatically generate a nice highlight color
  7353. }
  7354. else {
  7355. c = {};
  7356. c.background = color.background || 'white';
  7357. c.border = color.border || c.background;
  7358. if (util.isString(color.highlight)) {
  7359. c.highlight = {
  7360. border: color.highlight,
  7361. background: color.highlight
  7362. }
  7363. }
  7364. else {
  7365. c.highlight = {};
  7366. c.highlight.background = color.highlight && color.highlight.background || c.background;
  7367. c.highlight.border = color.highlight && color.highlight.border || c.border;
  7368. }
  7369. }
  7370. return c;
  7371. };
  7372. /**
  7373. * select this node
  7374. */
  7375. Node.prototype.select = function() {
  7376. this.selected = true;
  7377. this._reset();
  7378. };
  7379. /**
  7380. * unselect this node
  7381. */
  7382. Node.prototype.unselect = function() {
  7383. this.selected = false;
  7384. this._reset();
  7385. };
  7386. /**
  7387. * Reset the calculated size of the node, forces it to recalculate its size
  7388. * @private
  7389. */
  7390. Node.prototype._reset = function() {
  7391. this.width = undefined;
  7392. this.height = undefined;
  7393. };
  7394. /**
  7395. * get the title of this node.
  7396. * @return {string} title The title of the node, or undefined when no title
  7397. * has been set.
  7398. */
  7399. Node.prototype.getTitle = function() {
  7400. return this.title;
  7401. };
  7402. /**
  7403. * Calculate the distance to the border of the Node
  7404. * @param {CanvasRenderingContext2D} ctx
  7405. * @param {Number} angle Angle in radians
  7406. * @returns {number} distance Distance to the border in pixels
  7407. */
  7408. Node.prototype.distanceToBorder = function (ctx, angle) {
  7409. var borderWidth = 1;
  7410. if (!this.width) {
  7411. this.resize(ctx);
  7412. }
  7413. //noinspection FallthroughInSwitchStatementJS
  7414. switch (this.shape) {
  7415. case 'circle':
  7416. case 'dot':
  7417. return this.radius + borderWidth;
  7418. case 'ellipse':
  7419. var a = this.width / 2;
  7420. var b = this.height / 2;
  7421. var w = (Math.sin(angle) * a);
  7422. var h = (Math.cos(angle) * b);
  7423. return a * b / Math.sqrt(w * w + h * h);
  7424. // TODO: implement distanceToBorder for database
  7425. // TODO: implement distanceToBorder for triangle
  7426. // TODO: implement distanceToBorder for triangleDown
  7427. case 'box':
  7428. case 'image':
  7429. case 'text':
  7430. default:
  7431. if (this.width) {
  7432. return Math.min(
  7433. Math.abs(this.width / 2 / Math.cos(angle)),
  7434. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7435. // TODO: reckon with border radius too in case of box
  7436. }
  7437. else {
  7438. return 0;
  7439. }
  7440. }
  7441. // TODO: implement calculation of distance to border for all shapes
  7442. };
  7443. /**
  7444. * Set forces acting on the node
  7445. * @param {number} fx Force in horizontal direction
  7446. * @param {number} fy Force in vertical direction
  7447. */
  7448. Node.prototype._setForce = function(fx, fy) {
  7449. this.fx = fx;
  7450. this.fy = fy;
  7451. };
  7452. /**
  7453. * Add forces acting on the node
  7454. * @param {number} fx Force in horizontal direction
  7455. * @param {number} fy Force in vertical direction
  7456. * @private
  7457. */
  7458. Node.prototype._addForce = function(fx, fy) {
  7459. this.fx += fx;
  7460. this.fy += fy;
  7461. };
  7462. /**
  7463. * Perform one discrete step for the node
  7464. * @param {number} interval Time interval in seconds
  7465. */
  7466. Node.prototype.discreteStep = function(interval) {
  7467. if (!this.xFixed) {
  7468. var dx = -this.damping * this.vx; // damping force
  7469. var ax = (this.fx + dx) / this.mass; // acceleration
  7470. this.vx += ax / interval; // velocity
  7471. this.x += this.vx / interval; // position
  7472. }
  7473. if (!this.yFixed) {
  7474. var dy = -this.damping * this.vy; // damping force
  7475. var ay = (this.fy + dy) / this.mass; // acceleration
  7476. this.vy += ay / interval; // velocity
  7477. this.y += this.vy / interval; // position
  7478. }
  7479. };
  7480. /**
  7481. * Check if this node has a fixed x and y position
  7482. * @return {boolean} true if fixed, false if not
  7483. */
  7484. Node.prototype.isFixed = function() {
  7485. return (this.xFixed && this.yFixed);
  7486. };
  7487. /**
  7488. * Check if this node is moving
  7489. * @param {number} vmin the minimum velocity considered as "moving"
  7490. * @return {boolean} true if moving, false if it has no velocity
  7491. */
  7492. // TODO: replace this method with calculating the kinetic energy
  7493. Node.prototype.isMoving = function(vmin) {
  7494. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  7495. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  7496. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  7497. };
  7498. /**
  7499. * check if this node is selecte
  7500. * @return {boolean} selected True if node is selected, else false
  7501. */
  7502. Node.prototype.isSelected = function() {
  7503. return this.selected;
  7504. };
  7505. /**
  7506. * Retrieve the value of the node. Can be undefined
  7507. * @return {Number} value
  7508. */
  7509. Node.prototype.getValue = function() {
  7510. return this.value;
  7511. };
  7512. /**
  7513. * Calculate the distance from the nodes location to the given location (x,y)
  7514. * @param {Number} x
  7515. * @param {Number} y
  7516. * @return {Number} value
  7517. */
  7518. Node.prototype.getDistance = function(x, y) {
  7519. var dx = this.x - x,
  7520. dy = this.y - y;
  7521. return Math.sqrt(dx * dx + dy * dy);
  7522. };
  7523. /**
  7524. * Adjust the value range of the node. The node will adjust it's radius
  7525. * based on its value.
  7526. * @param {Number} min
  7527. * @param {Number} max
  7528. */
  7529. Node.prototype.setValueRange = function(min, max) {
  7530. if (!this.radiusFixed && this.value !== undefined) {
  7531. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  7532. this.radius = (this.value - min) * scale + this.radiusMin;
  7533. }
  7534. };
  7535. /**
  7536. * Draw this node in the given canvas
  7537. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7538. * @param {CanvasRenderingContext2D} ctx
  7539. */
  7540. Node.prototype.draw = function(ctx) {
  7541. throw "Draw method not initialized for node";
  7542. };
  7543. /**
  7544. * Recalculate the size of this node in the given canvas
  7545. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7546. * @param {CanvasRenderingContext2D} ctx
  7547. */
  7548. Node.prototype.resize = function(ctx) {
  7549. throw "Resize method not initialized for node";
  7550. };
  7551. /**
  7552. * Check if this object is overlapping with the provided object
  7553. * @param {Object} obj an object with parameters left, top, right, bottom
  7554. * @return {boolean} True if location is located on node
  7555. */
  7556. Node.prototype.isOverlappingWith = function(obj) {
  7557. return (this.left < obj.right &&
  7558. this.left + this.width > obj.left &&
  7559. this.top < obj.bottom &&
  7560. this.top + this.height > obj.top);
  7561. };
  7562. Node.prototype._resizeImage = function (ctx) {
  7563. // TODO: pre calculate the image size
  7564. if (!this.width) { // undefined or 0
  7565. var width, height;
  7566. if (this.value) {
  7567. var scale = this.imageObj.height / this.imageObj.width;
  7568. width = this.radius || this.imageObj.width;
  7569. height = this.radius * scale || this.imageObj.height;
  7570. }
  7571. else {
  7572. width = this.imageObj.width;
  7573. height = this.imageObj.height;
  7574. }
  7575. this.width = width;
  7576. this.height = height;
  7577. }
  7578. };
  7579. Node.prototype._drawImage = function (ctx) {
  7580. this._resizeImage(ctx);
  7581. this.left = this.x - this.width / 2;
  7582. this.top = this.y - this.height / 2;
  7583. var yLabel;
  7584. if (this.imageObj) {
  7585. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  7586. yLabel = this.y + this.height / 2;
  7587. }
  7588. else {
  7589. // image still loading... just draw the label for now
  7590. yLabel = this.y;
  7591. }
  7592. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  7593. };
  7594. Node.prototype._resizeBox = function (ctx) {
  7595. if (!this.width) {
  7596. var margin = 5;
  7597. var textSize = this.getTextSize(ctx);
  7598. this.width = textSize.width + 2 * margin;
  7599. this.height = textSize.height + 2 * margin;
  7600. }
  7601. };
  7602. Node.prototype._drawBox = function (ctx) {
  7603. this._resizeBox(ctx);
  7604. this.left = this.x - this.width / 2;
  7605. this.top = this.y - this.height / 2;
  7606. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7607. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7608. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7609. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  7610. ctx.fill();
  7611. ctx.stroke();
  7612. this._label(ctx, this.label, this.x, this.y);
  7613. };
  7614. Node.prototype._resizeDatabase = function (ctx) {
  7615. if (!this.width) {
  7616. var margin = 5;
  7617. var textSize = this.getTextSize(ctx);
  7618. var size = textSize.width + 2 * margin;
  7619. this.width = size;
  7620. this.height = size;
  7621. }
  7622. };
  7623. Node.prototype._drawDatabase = function (ctx) {
  7624. this._resizeDatabase(ctx);
  7625. this.left = this.x - this.width / 2;
  7626. this.top = this.y - this.height / 2;
  7627. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7628. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7629. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7630. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  7631. ctx.fill();
  7632. ctx.stroke();
  7633. this._label(ctx, this.label, this.x, this.y);
  7634. };
  7635. Node.prototype._resizeCircle = function (ctx) {
  7636. if (!this.width) {
  7637. var margin = 5;
  7638. var textSize = this.getTextSize(ctx);
  7639. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  7640. this.radius = diameter / 2;
  7641. this.width = diameter;
  7642. this.height = diameter;
  7643. }
  7644. };
  7645. Node.prototype._drawCircle = function (ctx) {
  7646. this._resizeCircle(ctx);
  7647. this.left = this.x - this.width / 2;
  7648. this.top = this.y - this.height / 2;
  7649. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7650. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7651. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7652. ctx.circle(this.x, this.y, this.radius);
  7653. ctx.fill();
  7654. ctx.stroke();
  7655. this._label(ctx, this.label, this.x, this.y);
  7656. };
  7657. Node.prototype._resizeEllipse = function (ctx) {
  7658. if (!this.width) {
  7659. var textSize = this.getTextSize(ctx);
  7660. this.width = textSize.width * 1.5;
  7661. this.height = textSize.height * 2;
  7662. if (this.width < this.height) {
  7663. this.width = this.height;
  7664. }
  7665. }
  7666. };
  7667. Node.prototype._drawEllipse = function (ctx) {
  7668. this._resizeEllipse(ctx);
  7669. this.left = this.x - this.width / 2;
  7670. this.top = this.y - this.height / 2;
  7671. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7672. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7673. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7674. ctx.ellipse(this.left, this.top, this.width, this.height);
  7675. ctx.fill();
  7676. ctx.stroke();
  7677. this._label(ctx, this.label, this.x, this.y);
  7678. };
  7679. Node.prototype._drawDot = function (ctx) {
  7680. this._drawShape(ctx, 'circle');
  7681. };
  7682. Node.prototype._drawTriangle = function (ctx) {
  7683. this._drawShape(ctx, 'triangle');
  7684. };
  7685. Node.prototype._drawTriangleDown = function (ctx) {
  7686. this._drawShape(ctx, 'triangleDown');
  7687. };
  7688. Node.prototype._drawSquare = function (ctx) {
  7689. this._drawShape(ctx, 'square');
  7690. };
  7691. Node.prototype._drawStar = function (ctx) {
  7692. this._drawShape(ctx, 'star');
  7693. };
  7694. Node.prototype._resizeShape = function (ctx) {
  7695. if (!this.width) {
  7696. var size = 2 * this.radius;
  7697. this.width = size;
  7698. this.height = size;
  7699. }
  7700. };
  7701. Node.prototype._drawShape = function (ctx, shape) {
  7702. this._resizeShape(ctx);
  7703. this.left = this.x - this.width / 2;
  7704. this.top = this.y - this.height / 2;
  7705. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  7706. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  7707. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  7708. ctx[shape](this.x, this.y, this.radius);
  7709. ctx.fill();
  7710. ctx.stroke();
  7711. if (this.label) {
  7712. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  7713. }
  7714. };
  7715. Node.prototype._resizeText = function (ctx) {
  7716. if (!this.width) {
  7717. var margin = 5;
  7718. var textSize = this.getTextSize(ctx);
  7719. this.width = textSize.width + 2 * margin;
  7720. this.height = textSize.height + 2 * margin;
  7721. }
  7722. };
  7723. Node.prototype._drawText = function (ctx) {
  7724. this._resizeText(ctx);
  7725. this.left = this.x - this.width / 2;
  7726. this.top = this.y - this.height / 2;
  7727. this._label(ctx, this.label, this.x, this.y);
  7728. };
  7729. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  7730. if (text) {
  7731. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7732. ctx.fillStyle = this.fontColor || "black";
  7733. ctx.textAlign = align || "center";
  7734. ctx.textBaseline = baseline || "middle";
  7735. var lines = text.split('\n'),
  7736. lineCount = lines.length,
  7737. fontSize = (this.fontSize + 4),
  7738. yLine = y + (1 - lineCount) / 2 * fontSize;
  7739. for (var i = 0; i < lineCount; i++) {
  7740. ctx.fillText(lines[i], x, yLine);
  7741. yLine += fontSize;
  7742. }
  7743. }
  7744. };
  7745. Node.prototype.getTextSize = function(ctx) {
  7746. if (this.label != undefined) {
  7747. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  7748. var lines = this.label.split('\n'),
  7749. height = (this.fontSize + 4) * lines.length,
  7750. width = 0;
  7751. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  7752. width = Math.max(width, ctx.measureText(lines[i]).width);
  7753. }
  7754. return {"width": width, "height": height};
  7755. }
  7756. else {
  7757. return {"width": 0, "height": 0};
  7758. }
  7759. };
  7760. /**
  7761. * @class Edge
  7762. *
  7763. * A edge connects two nodes
  7764. * @param {Object} properties Object with properties. Must contain
  7765. * At least properties from and to.
  7766. * Available properties: from (number),
  7767. * to (number), label (string, color (string),
  7768. * width (number), style (string),
  7769. * length (number), title (string)
  7770. * @param {Graph} graph A graph object, used to find and edge to
  7771. * nodes.
  7772. * @param {Object} constants An object with default values for
  7773. * example for the color
  7774. */
  7775. function Edge (properties, graph, constants) {
  7776. if (!graph) {
  7777. throw "No graph provided";
  7778. }
  7779. this.graph = graph;
  7780. // initialize constants
  7781. this.widthMin = constants.edges.widthMin;
  7782. this.widthMax = constants.edges.widthMax;
  7783. // initialize variables
  7784. this.id = undefined;
  7785. this.fromId = undefined;
  7786. this.toId = undefined;
  7787. this.style = constants.edges.style;
  7788. this.title = undefined;
  7789. this.width = constants.edges.width;
  7790. this.value = undefined;
  7791. this.length = constants.edges.length;
  7792. this.from = null; // a node
  7793. this.to = null; // a node
  7794. this.connected = false;
  7795. // Added to support dashed lines
  7796. // David Jordan
  7797. // 2012-08-08
  7798. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  7799. this.stiffness = undefined; // depends on the length of the edge
  7800. this.color = constants.edges.color;
  7801. this.widthFixed = false;
  7802. this.lengthFixed = false;
  7803. this.setProperties(properties, constants);
  7804. }
  7805. /**
  7806. * Set or overwrite properties for the edge
  7807. * @param {Object} properties an object with properties
  7808. * @param {Object} constants and object with default, global properties
  7809. */
  7810. Edge.prototype.setProperties = function(properties, constants) {
  7811. if (!properties) {
  7812. return;
  7813. }
  7814. if (properties.from != undefined) {this.fromId = properties.from;}
  7815. if (properties.to != undefined) {this.toId = properties.to;}
  7816. if (properties.id != undefined) {this.id = properties.id;}
  7817. if (properties.style != undefined) {this.style = properties.style;}
  7818. if (properties.label != undefined) {this.label = properties.label;}
  7819. if (this.label) {
  7820. this.fontSize = constants.edges.fontSize;
  7821. this.fontFace = constants.edges.fontFace;
  7822. this.fontColor = constants.edges.fontColor;
  7823. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  7824. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  7825. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  7826. }
  7827. if (properties.title != undefined) {this.title = properties.title;}
  7828. if (properties.width != undefined) {this.width = properties.width;}
  7829. if (properties.value != undefined) {this.value = properties.value;}
  7830. if (properties.length != undefined) {this.length = properties.length;}
  7831. // Added to support dashed lines
  7832. // David Jordan
  7833. // 2012-08-08
  7834. if (properties.dash) {
  7835. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  7836. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  7837. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  7838. }
  7839. if (properties.color != undefined) {this.color = properties.color;}
  7840. // A node is connected when it has a from and to node.
  7841. this.connect();
  7842. this.widthFixed = this.widthFixed || (properties.width != undefined);
  7843. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  7844. this.stiffness = 1 / this.length;
  7845. // set draw method based on style
  7846. switch (this.style) {
  7847. case 'line': this.draw = this._drawLine; break;
  7848. case 'arrow': this.draw = this._drawArrow; break;
  7849. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  7850. case 'dash-line': this.draw = this._drawDashLine; break;
  7851. default: this.draw = this._drawLine; break;
  7852. }
  7853. };
  7854. /**
  7855. * Connect an edge to its nodes
  7856. */
  7857. Edge.prototype.connect = function () {
  7858. this.disconnect();
  7859. this.from = this.graph.nodes[this.fromId] || null;
  7860. this.to = this.graph.nodes[this.toId] || null;
  7861. this.connected = (this.from && this.to);
  7862. if (this.connected) {
  7863. this.from.attachEdge(this);
  7864. this.to.attachEdge(this);
  7865. }
  7866. else {
  7867. if (this.from) {
  7868. this.from.detachEdge(this);
  7869. }
  7870. if (this.to) {
  7871. this.to.detachEdge(this);
  7872. }
  7873. }
  7874. };
  7875. /**
  7876. * Disconnect an edge from its nodes
  7877. */
  7878. Edge.prototype.disconnect = function () {
  7879. if (this.from) {
  7880. this.from.detachEdge(this);
  7881. this.from = null;
  7882. }
  7883. if (this.to) {
  7884. this.to.detachEdge(this);
  7885. this.to = null;
  7886. }
  7887. this.connected = false;
  7888. };
  7889. /**
  7890. * get the title of this edge.
  7891. * @return {string} title The title of the edge, or undefined when no title
  7892. * has been set.
  7893. */
  7894. Edge.prototype.getTitle = function() {
  7895. return this.title;
  7896. };
  7897. /**
  7898. * Retrieve the value of the edge. Can be undefined
  7899. * @return {Number} value
  7900. */
  7901. Edge.prototype.getValue = function() {
  7902. return this.value;
  7903. };
  7904. /**
  7905. * Adjust the value range of the edge. The edge will adjust it's width
  7906. * based on its value.
  7907. * @param {Number} min
  7908. * @param {Number} max
  7909. */
  7910. Edge.prototype.setValueRange = function(min, max) {
  7911. if (!this.widthFixed && this.value !== undefined) {
  7912. var factor = (this.widthMax - this.widthMin) / (max - min);
  7913. this.width = (this.value - min) * factor + this.widthMin;
  7914. }
  7915. };
  7916. /**
  7917. * Redraw a edge
  7918. * Draw this edge in the given canvas
  7919. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7920. * @param {CanvasRenderingContext2D} ctx
  7921. */
  7922. Edge.prototype.draw = function(ctx) {
  7923. throw "Method draw not initialized in edge";
  7924. };
  7925. /**
  7926. * Check if this object is overlapping with the provided object
  7927. * @param {Object} obj an object with parameters left, top
  7928. * @return {boolean} True if location is located on the edge
  7929. */
  7930. Edge.prototype.isOverlappingWith = function(obj) {
  7931. var distMax = 10;
  7932. var xFrom = this.from.x;
  7933. var yFrom = this.from.y;
  7934. var xTo = this.to.x;
  7935. var yTo = this.to.y;
  7936. var xObj = obj.left;
  7937. var yObj = obj.top;
  7938. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  7939. return (dist < distMax);
  7940. };
  7941. /**
  7942. * Redraw a edge as a line
  7943. * Draw this edge in the given canvas
  7944. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  7945. * @param {CanvasRenderingContext2D} ctx
  7946. * @private
  7947. */
  7948. Edge.prototype._drawLine = function(ctx) {
  7949. // set style
  7950. ctx.strokeStyle = this.color;
  7951. ctx.lineWidth = this._getLineWidth();
  7952. var point;
  7953. if (this.from != this.to) {
  7954. // draw line
  7955. this._line(ctx);
  7956. // draw label
  7957. if (this.label) {
  7958. point = this._pointOnLine(0.5);
  7959. this._label(ctx, this.label, point.x, point.y);
  7960. }
  7961. }
  7962. else {
  7963. var x, y;
  7964. var radius = this.length / 4;
  7965. var node = this.from;
  7966. if (!node.width) {
  7967. node.resize(ctx);
  7968. }
  7969. if (node.width > node.height) {
  7970. x = node.x + node.width / 2;
  7971. y = node.y - radius;
  7972. }
  7973. else {
  7974. x = node.x + radius;
  7975. y = node.y - node.height / 2;
  7976. }
  7977. this._circle(ctx, x, y, radius);
  7978. point = this._pointOnCircle(x, y, radius, 0.5);
  7979. this._label(ctx, this.label, point.x, point.y);
  7980. }
  7981. };
  7982. /**
  7983. * Get the line width of the edge. Depends on width and whether one of the
  7984. * connected nodes is selected.
  7985. * @return {Number} width
  7986. * @private
  7987. */
  7988. Edge.prototype._getLineWidth = function() {
  7989. if (this.from.selected || this.to.selected) {
  7990. return Math.min(this.width * 2, this.widthMax);
  7991. }
  7992. else {
  7993. return this.width;
  7994. }
  7995. };
  7996. /**
  7997. * Draw a line between two nodes
  7998. * @param {CanvasRenderingContext2D} ctx
  7999. * @private
  8000. */
  8001. Edge.prototype._line = function (ctx) {
  8002. // draw a straight line
  8003. ctx.beginPath();
  8004. ctx.moveTo(this.from.x, this.from.y);
  8005. ctx.lineTo(this.to.x, this.to.y);
  8006. ctx.stroke();
  8007. };
  8008. /**
  8009. * Draw a line from a node to itself, a circle
  8010. * @param {CanvasRenderingContext2D} ctx
  8011. * @param {Number} x
  8012. * @param {Number} y
  8013. * @param {Number} radius
  8014. * @private
  8015. */
  8016. Edge.prototype._circle = function (ctx, x, y, radius) {
  8017. // draw a circle
  8018. ctx.beginPath();
  8019. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8020. ctx.stroke();
  8021. };
  8022. /**
  8023. * Draw label with white background and with the middle at (x, y)
  8024. * @param {CanvasRenderingContext2D} ctx
  8025. * @param {String} text
  8026. * @param {Number} x
  8027. * @param {Number} y
  8028. * @private
  8029. */
  8030. Edge.prototype._label = function (ctx, text, x, y) {
  8031. if (text) {
  8032. // TODO: cache the calculated size
  8033. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8034. this.fontSize + "px " + this.fontFace;
  8035. ctx.fillStyle = 'white';
  8036. var width = ctx.measureText(text).width;
  8037. var height = this.fontSize;
  8038. var left = x - width / 2;
  8039. var top = y - height / 2;
  8040. ctx.fillRect(left, top, width, height);
  8041. // draw text
  8042. ctx.fillStyle = this.fontColor || "black";
  8043. ctx.textAlign = "left";
  8044. ctx.textBaseline = "top";
  8045. ctx.fillText(text, left, top);
  8046. }
  8047. };
  8048. /**
  8049. * Redraw a edge as a dashed line
  8050. * Draw this edge in the given canvas
  8051. * @author David Jordan
  8052. * @date 2012-08-08
  8053. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8054. * @param {CanvasRenderingContext2D} ctx
  8055. * @private
  8056. */
  8057. Edge.prototype._drawDashLine = function(ctx) {
  8058. // set style
  8059. ctx.strokeStyle = this.color;
  8060. ctx.lineWidth = this._getLineWidth();
  8061. // draw dashed line
  8062. ctx.beginPath();
  8063. ctx.lineCap = 'round';
  8064. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  8065. {
  8066. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8067. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8068. }
  8069. 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
  8070. {
  8071. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8072. [this.dash.length,this.dash.gap]);
  8073. }
  8074. else //If all else fails draw a line
  8075. {
  8076. ctx.moveTo(this.from.x, this.from.y);
  8077. ctx.lineTo(this.to.x, this.to.y);
  8078. }
  8079. ctx.stroke();
  8080. // draw label
  8081. if (this.label) {
  8082. var point = this._pointOnLine(0.5);
  8083. this._label(ctx, this.label, point.x, point.y);
  8084. }
  8085. };
  8086. /**
  8087. * Get a point on a line
  8088. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8089. * @return {Object} point
  8090. * @private
  8091. */
  8092. Edge.prototype._pointOnLine = function (percentage) {
  8093. return {
  8094. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  8095. y: (1 - percentage) * this.from.y + percentage * this.to.y
  8096. }
  8097. };
  8098. /**
  8099. * Get a point on a circle
  8100. * @param {Number} x
  8101. * @param {Number} y
  8102. * @param {Number} radius
  8103. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8104. * @return {Object} point
  8105. * @private
  8106. */
  8107. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  8108. var angle = (percentage - 3/8) * 2 * Math.PI;
  8109. return {
  8110. x: x + radius * Math.cos(angle),
  8111. y: y - radius * Math.sin(angle)
  8112. }
  8113. };
  8114. /**
  8115. * Redraw a edge as a line with an arrow halfway the line
  8116. * Draw this edge in the given canvas
  8117. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8118. * @param {CanvasRenderingContext2D} ctx
  8119. * @private
  8120. */
  8121. Edge.prototype._drawArrowCenter = function(ctx) {
  8122. var point;
  8123. // set style
  8124. ctx.strokeStyle = this.color;
  8125. ctx.fillStyle = this.color;
  8126. ctx.lineWidth = this._getLineWidth();
  8127. if (this.from != this.to) {
  8128. // draw line
  8129. this._line(ctx);
  8130. // draw an arrow halfway the line
  8131. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8132. var length = 10 + 5 * this.width; // TODO: make customizable?
  8133. point = this._pointOnLine(0.5);
  8134. ctx.arrow(point.x, point.y, angle, length);
  8135. ctx.fill();
  8136. ctx.stroke();
  8137. // draw label
  8138. if (this.label) {
  8139. point = this._pointOnLine(0.5);
  8140. this._label(ctx, this.label, point.x, point.y);
  8141. }
  8142. }
  8143. else {
  8144. // draw circle
  8145. var x, y;
  8146. var radius = this.length / 4;
  8147. var node = this.from;
  8148. if (!node.width) {
  8149. node.resize(ctx);
  8150. }
  8151. if (node.width > node.height) {
  8152. x = node.x + node.width / 2;
  8153. y = node.y - radius;
  8154. }
  8155. else {
  8156. x = node.x + radius;
  8157. y = node.y - node.height / 2;
  8158. }
  8159. this._circle(ctx, x, y, radius);
  8160. // draw all arrows
  8161. var angle = 0.2 * Math.PI;
  8162. var length = 10 + 5 * this.width; // TODO: make customizable?
  8163. point = this._pointOnCircle(x, y, radius, 0.5);
  8164. ctx.arrow(point.x, point.y, angle, length);
  8165. ctx.fill();
  8166. ctx.stroke();
  8167. // draw label
  8168. if (this.label) {
  8169. point = this._pointOnCircle(x, y, radius, 0.5);
  8170. this._label(ctx, this.label, point.x, point.y);
  8171. }
  8172. }
  8173. };
  8174. /**
  8175. * Redraw a edge as a line with an arrow
  8176. * Draw this edge in the given canvas
  8177. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8178. * @param {CanvasRenderingContext2D} ctx
  8179. * @private
  8180. */
  8181. Edge.prototype._drawArrow = function(ctx) {
  8182. // set style
  8183. ctx.strokeStyle = this.color;
  8184. ctx.fillStyle = this.color;
  8185. ctx.lineWidth = this._getLineWidth();
  8186. // draw line
  8187. var angle, length;
  8188. if (this.from != this.to) {
  8189. // calculate length and angle of the line
  8190. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8191. var dx = (this.to.x - this.from.x);
  8192. var dy = (this.to.y - this.from.y);
  8193. var lEdge = Math.sqrt(dx * dx + dy * dy);
  8194. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  8195. var pFrom = (lEdge - lFrom) / lEdge;
  8196. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  8197. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  8198. var lTo = this.to.distanceToBorder(ctx, angle);
  8199. var pTo = (lEdge - lTo) / lEdge;
  8200. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  8201. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  8202. ctx.beginPath();
  8203. ctx.moveTo(xFrom, yFrom);
  8204. ctx.lineTo(xTo, yTo);
  8205. ctx.stroke();
  8206. // draw arrow at the end of the line
  8207. length = 10 + 5 * this.width; // TODO: make customizable?
  8208. ctx.arrow(xTo, yTo, angle, length);
  8209. ctx.fill();
  8210. ctx.stroke();
  8211. // draw label
  8212. if (this.label) {
  8213. var point = this._pointOnLine(0.5);
  8214. this._label(ctx, this.label, point.x, point.y);
  8215. }
  8216. }
  8217. else {
  8218. // draw circle
  8219. var node = this.from;
  8220. var x, y, arrow;
  8221. var radius = this.length / 4;
  8222. if (!node.width) {
  8223. node.resize(ctx);
  8224. }
  8225. if (node.width > node.height) {
  8226. x = node.x + node.width / 2;
  8227. y = node.y - radius;
  8228. arrow = {
  8229. x: x,
  8230. y: node.y,
  8231. angle: 0.9 * Math.PI
  8232. };
  8233. }
  8234. else {
  8235. x = node.x + radius;
  8236. y = node.y - node.height / 2;
  8237. arrow = {
  8238. x: node.x,
  8239. y: y,
  8240. angle: 0.6 * Math.PI
  8241. };
  8242. }
  8243. ctx.beginPath();
  8244. // TODO: do not draw a circle, but an arc
  8245. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  8246. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8247. ctx.stroke();
  8248. // draw all arrows
  8249. length = 10 + 5 * this.width; // TODO: make customizable?
  8250. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  8251. ctx.fill();
  8252. ctx.stroke();
  8253. // draw label
  8254. if (this.label) {
  8255. point = this._pointOnCircle(x, y, radius, 0.5);
  8256. this._label(ctx, this.label, point.x, point.y);
  8257. }
  8258. }
  8259. };
  8260. /**
  8261. * Calculate the distance between a point (x3,y3) and a line segment from
  8262. * (x1,y1) to (x2,y2).
  8263. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  8264. * @param {number} x1
  8265. * @param {number} y1
  8266. * @param {number} x2
  8267. * @param {number} y2
  8268. * @param {number} x3
  8269. * @param {number} y3
  8270. * @private
  8271. */
  8272. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  8273. var px = x2-x1,
  8274. py = y2-y1,
  8275. something = px*px + py*py,
  8276. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  8277. if (u > 1) {
  8278. u = 1;
  8279. }
  8280. else if (u < 0) {
  8281. u = 0;
  8282. }
  8283. var x = x1 + u * px,
  8284. y = y1 + u * py,
  8285. dx = x - x3,
  8286. dy = y - y3;
  8287. //# Note: If the actual distance does not matter,
  8288. //# if you only want to compare what this function
  8289. //# returns to other results of this function, you
  8290. //# can just return the squared distance instead
  8291. //# (i.e. remove the sqrt) to gain a little performance
  8292. return Math.sqrt(dx*dx + dy*dy);
  8293. };
  8294. /**
  8295. * Popup is a class to create a popup window with some text
  8296. * @param {Element} container The container object.
  8297. * @param {Number} [x]
  8298. * @param {Number} [y]
  8299. * @param {String} [text]
  8300. */
  8301. function Popup(container, x, y, text) {
  8302. if (container) {
  8303. this.container = container;
  8304. }
  8305. else {
  8306. this.container = document.body;
  8307. }
  8308. this.x = 0;
  8309. this.y = 0;
  8310. this.padding = 5;
  8311. if (x !== undefined && y !== undefined ) {
  8312. this.setPosition(x, y);
  8313. }
  8314. if (text !== undefined) {
  8315. this.setText(text);
  8316. }
  8317. // create the frame
  8318. this.frame = document.createElement("div");
  8319. var style = this.frame.style;
  8320. style.position = "absolute";
  8321. style.visibility = "hidden";
  8322. style.border = "1px solid #666";
  8323. style.color = "black";
  8324. style.padding = this.padding + "px";
  8325. style.backgroundColor = "#FFFFC6";
  8326. style.borderRadius = "3px";
  8327. style.MozBorderRadius = "3px";
  8328. style.WebkitBorderRadius = "3px";
  8329. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  8330. style.whiteSpace = "nowrap";
  8331. this.container.appendChild(this.frame);
  8332. };
  8333. /**
  8334. * @param {number} x Horizontal position of the popup window
  8335. * @param {number} y Vertical position of the popup window
  8336. */
  8337. Popup.prototype.setPosition = function(x, y) {
  8338. this.x = parseInt(x);
  8339. this.y = parseInt(y);
  8340. };
  8341. /**
  8342. * Set the text for the popup window. This can be HTML code
  8343. * @param {string} text
  8344. */
  8345. Popup.prototype.setText = function(text) {
  8346. this.frame.innerHTML = text;
  8347. };
  8348. /**
  8349. * Show the popup window
  8350. * @param {boolean} show Optional. Show or hide the window
  8351. */
  8352. Popup.prototype.show = function (show) {
  8353. if (show === undefined) {
  8354. show = true;
  8355. }
  8356. if (show) {
  8357. var height = this.frame.clientHeight;
  8358. var width = this.frame.clientWidth;
  8359. var maxHeight = this.frame.parentNode.clientHeight;
  8360. var maxWidth = this.frame.parentNode.clientWidth;
  8361. var top = (this.y - height);
  8362. if (top + height + this.padding > maxHeight) {
  8363. top = maxHeight - height - this.padding;
  8364. }
  8365. if (top < this.padding) {
  8366. top = this.padding;
  8367. }
  8368. var left = this.x;
  8369. if (left + width + this.padding > maxWidth) {
  8370. left = maxWidth - width - this.padding;
  8371. }
  8372. if (left < this.padding) {
  8373. left = this.padding;
  8374. }
  8375. this.frame.style.left = left + "px";
  8376. this.frame.style.top = top + "px";
  8377. this.frame.style.visibility = "visible";
  8378. }
  8379. else {
  8380. this.hide();
  8381. }
  8382. };
  8383. /**
  8384. * Hide the popup window
  8385. */
  8386. Popup.prototype.hide = function () {
  8387. this.frame.style.visibility = "hidden";
  8388. };
  8389. /**
  8390. * @class Groups
  8391. * This class can store groups and properties specific for groups.
  8392. */
  8393. Groups = function () {
  8394. this.clear();
  8395. this.defaultIndex = 0;
  8396. };
  8397. /**
  8398. * default constants for group colors
  8399. */
  8400. Groups.DEFAULT = [
  8401. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  8402. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  8403. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  8404. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  8405. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  8406. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  8407. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  8408. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  8409. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  8410. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  8411. ];
  8412. /**
  8413. * Clear all groups
  8414. */
  8415. Groups.prototype.clear = function () {
  8416. this.groups = {};
  8417. this.groups.length = function()
  8418. {
  8419. var i = 0;
  8420. for ( var p in this ) {
  8421. if (this.hasOwnProperty(p)) {
  8422. i++;
  8423. }
  8424. }
  8425. return i;
  8426. }
  8427. };
  8428. /**
  8429. * get group properties of a groupname. If groupname is not found, a new group
  8430. * is added.
  8431. * @param {*} groupname Can be a number, string, Date, etc.
  8432. * @return {Object} group The created group, containing all group properties
  8433. */
  8434. Groups.prototype.get = function (groupname) {
  8435. var group = this.groups[groupname];
  8436. if (group == undefined) {
  8437. // create new group
  8438. var index = this.defaultIndex % Groups.DEFAULT.length;
  8439. this.defaultIndex++;
  8440. group = {};
  8441. group.color = Groups.DEFAULT[index];
  8442. this.groups[groupname] = group;
  8443. }
  8444. return group;
  8445. };
  8446. /**
  8447. * Add a custom group style
  8448. * @param {String} groupname
  8449. * @param {Object} style An object containing borderColor,
  8450. * backgroundColor, etc.
  8451. * @return {Object} group The created group object
  8452. */
  8453. Groups.prototype.add = function (groupname, style) {
  8454. this.groups[groupname] = style;
  8455. if (style.color) {
  8456. style.color = Node.parseColor(style.color);
  8457. }
  8458. return style;
  8459. };
  8460. /**
  8461. * @class Images
  8462. * This class loads images and keeps them stored.
  8463. */
  8464. Images = function () {
  8465. this.images = {};
  8466. this.callback = undefined;
  8467. };
  8468. /**
  8469. * Set an onload callback function. This will be called each time an image
  8470. * is loaded
  8471. * @param {function} callback
  8472. */
  8473. Images.prototype.setOnloadCallback = function(callback) {
  8474. this.callback = callback;
  8475. };
  8476. /**
  8477. *
  8478. * @param {string} url Url of the image
  8479. * @return {Image} img The image object
  8480. */
  8481. Images.prototype.load = function(url) {
  8482. var img = this.images[url];
  8483. if (img == undefined) {
  8484. // create the image
  8485. var images = this;
  8486. img = new Image();
  8487. this.images[url] = img;
  8488. img.onload = function() {
  8489. if (images.callback) {
  8490. images.callback(this);
  8491. }
  8492. };
  8493. img.src = url;
  8494. }
  8495. return img;
  8496. };
  8497. /**
  8498. * @constructor Graph
  8499. * Create a graph visualization, displaying nodes and edges.
  8500. *
  8501. * @param {Element} container The DOM element in which the Graph will
  8502. * be created. Normally a div element.
  8503. * @param {Object} data An object containing parameters
  8504. * {Array} nodes
  8505. * {Array} edges
  8506. * @param {Object} options Options
  8507. */
  8508. function Graph (container, data, options) {
  8509. // create variables and set default values
  8510. this.containerElement = container;
  8511. this.width = '100%';
  8512. this.height = '100%';
  8513. this.refreshRate = 50; // milliseconds
  8514. this.stabilize = true; // stabilize before displaying the graph
  8515. this.selectable = true;
  8516. // set constant values
  8517. this.constants = {
  8518. nodes: {
  8519. radiusMin: 5,
  8520. radiusMax: 20,
  8521. radius: 5,
  8522. distance: 100, // px
  8523. shape: 'ellipse',
  8524. image: undefined,
  8525. widthMin: 16, // px
  8526. widthMax: 64, // px
  8527. fontColor: 'black',
  8528. fontSize: 14, // px
  8529. //fontFace: verdana,
  8530. fontFace: 'arial',
  8531. color: {
  8532. border: '#2B7CE9',
  8533. background: '#97C2FC',
  8534. highlight: {
  8535. border: '#2B7CE9',
  8536. background: '#D2E5FF'
  8537. }
  8538. },
  8539. borderColor: '#2B7CE9',
  8540. backgroundColor: '#97C2FC',
  8541. highlightColor: '#D2E5FF',
  8542. group: undefined
  8543. },
  8544. edges: {
  8545. widthMin: 1,
  8546. widthMax: 15,
  8547. width: 1,
  8548. style: 'line',
  8549. color: '#343434',
  8550. fontColor: '#343434',
  8551. fontSize: 14, // px
  8552. fontFace: 'arial',
  8553. //distance: 100, //px
  8554. length: 100, // px
  8555. dash: {
  8556. length: 10,
  8557. gap: 5,
  8558. altLength: undefined
  8559. }
  8560. },
  8561. minForce: 0.05,
  8562. minVelocity: 0.02, // px/s
  8563. maxIterations: 1000 // maximum number of iteration to stabilize
  8564. };
  8565. var graph = this;
  8566. this.nodes = {}; // object with Node objects
  8567. this.edges = {}; // object with Edge objects
  8568. // TODO: create a counter to keep track on the number of nodes having values
  8569. // TODO: create a counter to keep track on the number of nodes currently moving
  8570. // TODO: create a counter to keep track on the number of edges having values
  8571. this.nodesData = null; // A DataSet or DataView
  8572. this.edgesData = null; // A DataSet or DataView
  8573. // create event listeners used to subscribe on the DataSets of the nodes and edges
  8574. var me = this;
  8575. this.nodesListeners = {
  8576. 'add': function (event, params) {
  8577. me._addNodes(params.items);
  8578. me.start();
  8579. },
  8580. 'update': function (event, params) {
  8581. me._updateNodes(params.items);
  8582. me.start();
  8583. },
  8584. 'remove': function (event, params) {
  8585. me._removeNodes(params.items);
  8586. me.start();
  8587. }
  8588. };
  8589. this.edgesListeners = {
  8590. 'add': function (event, params) {
  8591. me._addEdges(params.items);
  8592. me.start();
  8593. },
  8594. 'update': function (event, params) {
  8595. me._updateEdges(params.items);
  8596. me.start();
  8597. },
  8598. 'remove': function (event, params) {
  8599. me._removeEdges(params.items);
  8600. me.start();
  8601. }
  8602. };
  8603. this.groups = new Groups(); // object with groups
  8604. this.images = new Images(); // object with images
  8605. this.images.setOnloadCallback(function () {
  8606. graph._redraw();
  8607. });
  8608. // properties of the data
  8609. this.moving = false; // True if any of the nodes have an undefined position
  8610. this.selection = [];
  8611. this.timer = undefined;
  8612. // create a frame and canvas
  8613. this._create();
  8614. // apply options
  8615. this.setOptions(options);
  8616. // draw data
  8617. this.setData(data);
  8618. }
  8619. /**
  8620. * Set nodes and edges, and optionally options as well.
  8621. *
  8622. * @param {Object} data Object containing parameters:
  8623. * {Array | DataSet | DataView} [nodes] Array with nodes
  8624. * {Array | DataSet | DataView} [edges] Array with edges
  8625. * {String} [dot] String containing data in DOT format
  8626. * {Options} [options] Object with options
  8627. */
  8628. Graph.prototype.setData = function(data) {
  8629. if (data && data.dot && (data.nodes || data.edges)) {
  8630. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  8631. ' parameter pair "nodes" and "edges", but not both.');
  8632. }
  8633. // set options
  8634. this.setOptions(data && data.options);
  8635. // set all data
  8636. if (data && data.dot) {
  8637. // parse DOT file
  8638. if(data && data.dot) {
  8639. var dotData = vis.util.DOTToGraph(data.dot);
  8640. this.setData(dotData);
  8641. return;
  8642. }
  8643. }
  8644. else {
  8645. this._setNodes(data && data.nodes);
  8646. this._setEdges(data && data.edges);
  8647. }
  8648. // find a stable position or start animating to a stable position
  8649. if (this.stabilize) {
  8650. this._doStabilize();
  8651. }
  8652. this.start();
  8653. };
  8654. /**
  8655. * Set options
  8656. * @param {Object} options
  8657. */
  8658. Graph.prototype.setOptions = function (options) {
  8659. if (options) {
  8660. // retrieve parameter values
  8661. if (options.width != undefined) {this.width = options.width;}
  8662. if (options.height != undefined) {this.height = options.height;}
  8663. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  8664. if (options.selectable != undefined) {this.selectable = options.selectable;}
  8665. // TODO: work out these options and document them
  8666. if (options.edges) {
  8667. for (var prop in options.edges) {
  8668. if (options.edges.hasOwnProperty(prop)) {
  8669. this.constants.edges[prop] = options.edges[prop];
  8670. }
  8671. }
  8672. if (options.edges.length != undefined &&
  8673. options.nodes && options.nodes.distance == undefined) {
  8674. this.constants.edges.length = options.edges.length;
  8675. this.constants.nodes.distance = options.edges.length * 1.25;
  8676. }
  8677. if (!options.edges.fontColor) {
  8678. this.constants.edges.fontColor = options.edges.color;
  8679. }
  8680. // Added to support dashed lines
  8681. // David Jordan
  8682. // 2012-08-08
  8683. if (options.edges.dash) {
  8684. if (options.edges.dash.length != undefined) {
  8685. this.constants.edges.dash.length = options.edges.dash.length;
  8686. }
  8687. if (options.edges.dash.gap != undefined) {
  8688. this.constants.edges.dash.gap = options.edges.dash.gap;
  8689. }
  8690. if (options.edges.dash.altLength != undefined) {
  8691. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  8692. }
  8693. }
  8694. }
  8695. if (options.nodes) {
  8696. for (prop in options.nodes) {
  8697. if (options.nodes.hasOwnProperty(prop)) {
  8698. this.constants.nodes[prop] = options.nodes[prop];
  8699. }
  8700. }
  8701. if (options.nodes.color) {
  8702. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  8703. }
  8704. /*
  8705. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  8706. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  8707. */
  8708. }
  8709. if (options.groups) {
  8710. for (var groupname in options.groups) {
  8711. if (options.groups.hasOwnProperty(groupname)) {
  8712. var group = options.groups[groupname];
  8713. this.groups.add(groupname, group);
  8714. }
  8715. }
  8716. }
  8717. }
  8718. this.setSize(this.width, this.height);
  8719. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  8720. this._setScale(1);
  8721. };
  8722. /**
  8723. * fire an event
  8724. * @param {String} event The name of an event, for example "select"
  8725. * @param {Object} params Optional object with event parameters
  8726. * @private
  8727. */
  8728. Graph.prototype._trigger = function (event, params) {
  8729. events.trigger(this, event, params);
  8730. };
  8731. /**
  8732. * Create the main frame for the Graph.
  8733. * This function is executed once when a Graph object is created. The frame
  8734. * contains a canvas, and this canvas contains all objects like the axis and
  8735. * nodes.
  8736. * @private
  8737. */
  8738. Graph.prototype._create = function () {
  8739. // remove all elements from the container element.
  8740. while (this.containerElement.hasChildNodes()) {
  8741. this.containerElement.removeChild(this.containerElement.firstChild);
  8742. }
  8743. this.frame = document.createElement("div");
  8744. this.frame.className = "graph-frame";
  8745. this.frame.style.position = "relative";
  8746. this.frame.style.overflow = "hidden";
  8747. // create the graph canvas (HTML canvas element)
  8748. this.frame.canvas = document.createElement( "canvas" );
  8749. this.frame.canvas.style.position = "relative";
  8750. this.frame.appendChild(this.frame.canvas);
  8751. if (!this.frame.canvas.getContext) {
  8752. var noCanvas = document.createElement( "DIV" );
  8753. noCanvas.style.color = "red";
  8754. noCanvas.style.fontWeight = "bold" ;
  8755. noCanvas.style.padding = "10px";
  8756. noCanvas.innerHTML = "Error: your browser does not support HTML canvas";
  8757. this.frame.canvas.appendChild(noCanvas);
  8758. }
  8759. // create event listeners
  8760. var me = this;
  8761. var onmousedown = function (event) {me._onMouseDown(event);};
  8762. var onmousemove = function (event) {me._onMouseMoveTitle(event);};
  8763. var onmousewheel = function (event) {me._onMouseWheel(event);};
  8764. var ontouchstart = function (event) {me._onTouchStart(event);};
  8765. vis.util.addEventListener(this.frame.canvas, "mousedown", onmousedown);
  8766. vis.util.addEventListener(this.frame.canvas, "mousemove", onmousemove);
  8767. vis.util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
  8768. vis.util.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
  8769. // add the frame to the container element
  8770. this.containerElement.appendChild(this.frame);
  8771. };
  8772. /**
  8773. * handle on mouse down event
  8774. * @private
  8775. */
  8776. Graph.prototype._onMouseDown = function (event) {
  8777. event = event || window.event;
  8778. if (!this.selectable) {
  8779. return;
  8780. }
  8781. // check if mouse is still down (may be up when focus is lost for example
  8782. // in an iframe)
  8783. if (this.leftButtonDown) {
  8784. this._onMouseUp(event);
  8785. }
  8786. // only react on left mouse button down
  8787. this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  8788. if (!this.leftButtonDown && !this.touchDown) {
  8789. return;
  8790. }
  8791. // add event listeners to handle moving the contents
  8792. // we store the function onmousemove and onmouseup in the timeline, so we can
  8793. // remove the eventlisteners lateron in the function mouseUp()
  8794. var me = this;
  8795. if (!this.onmousemove) {
  8796. this.onmousemove = function (event) {me._onMouseMove(event);};
  8797. vis.util.addEventListener(document, "mousemove", me.onmousemove);
  8798. }
  8799. if (!this.onmouseup) {
  8800. this.onmouseup = function (event) {me._onMouseUp(event);};
  8801. vis.util.addEventListener(document, "mouseup", me.onmouseup);
  8802. }
  8803. vis.util.preventDefault(event);
  8804. // store the start x and y position of the mouse
  8805. this.startMouseX = util.getPageX(event);
  8806. this.startMouseY = util.getPageY(event);
  8807. this.startFrameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  8808. this.startFrameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  8809. this.startTranslation = this._getTranslation();
  8810. this.ctrlKeyDown = event.ctrlKey;
  8811. this.shiftKeyDown = event.shiftKey;
  8812. var obj = {
  8813. left: this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8814. top: this._yToCanvas(this.startMouseY - this.startFrameTop),
  8815. right: this._xToCanvas(this.startMouseX - this.startFrameLeft),
  8816. bottom: this._yToCanvas(this.startMouseY - this.startFrameTop)
  8817. };
  8818. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8819. // if there are overlapping nodes, select the last one, this is the
  8820. // one which is drawn on top of the others
  8821. this.startClickedObj = (overlappingNodes.length > 0) ?
  8822. overlappingNodes[overlappingNodes.length - 1] : undefined;
  8823. if (this.startClickedObj) {
  8824. // move clicked node with the mouse
  8825. // make the clicked node temporarily fixed, and store their original state
  8826. var node = this.nodes[this.startClickedObj];
  8827. this.startClickedObj.xFixed = node.xFixed;
  8828. this.startClickedObj.yFixed = node.yFixed;
  8829. node.xFixed = true;
  8830. node.yFixed = true;
  8831. if (!this.ctrlKeyDown || !node.isSelected()) {
  8832. // select this node
  8833. this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
  8834. }
  8835. else {
  8836. // unselect this node
  8837. this._unselectNodes([this.startClickedObj]);
  8838. }
  8839. if (!this.moving) {
  8840. this._redraw();
  8841. }
  8842. }
  8843. else if (this.shiftKeyDown) {
  8844. // start selection of multiple nodes
  8845. }
  8846. else {
  8847. // start moving the graph
  8848. this.moved = false;
  8849. }
  8850. };
  8851. /**
  8852. * handle on mouse move event
  8853. * @param {Event} event
  8854. * @private
  8855. */
  8856. Graph.prototype._onMouseMove = function (event) {
  8857. event = event || window.event;
  8858. if (!this.selectable) {
  8859. return;
  8860. }
  8861. var mouseX = util.getPageX(event);
  8862. var mouseY = util.getPageY(event);
  8863. this.mouseX = mouseX;
  8864. this.mouseY = mouseY;
  8865. if (this.startClickedObj) {
  8866. var node = this.nodes[this.startClickedObj];
  8867. if (!this.startClickedObj.xFixed)
  8868. node.x = this._xToCanvas(mouseX - this.startFrameLeft);
  8869. if (!this.startClickedObj.yFixed)
  8870. node.y = this._yToCanvas(mouseY - this.startFrameTop);
  8871. // start animation if not yet running
  8872. if (!this.moving) {
  8873. this.moving = true;
  8874. this.start();
  8875. }
  8876. }
  8877. else if (this.shiftKeyDown) {
  8878. // draw a rect from start mouse location to current mouse location
  8879. if (this.frame.selRect == undefined) {
  8880. this.frame.selRect = document.createElement("DIV");
  8881. this.frame.appendChild(this.frame.selRect);
  8882. this.frame.selRect.style.position = "absolute";
  8883. this.frame.selRect.style.border = "1px dashed red";
  8884. }
  8885. var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
  8886. var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop;
  8887. var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
  8888. var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
  8889. this.frame.selRect.style.left = left + "px";
  8890. this.frame.selRect.style.top = top + "px";
  8891. this.frame.selRect.style.width = (right - left) + "px";
  8892. this.frame.selRect.style.height = (bottom - top) + "px";
  8893. }
  8894. else {
  8895. // move the graph
  8896. var diffX = mouseX - this.startMouseX;
  8897. var diffY = mouseY - this.startMouseY;
  8898. this._setTranslation(
  8899. this.startTranslation.x + diffX,
  8900. this.startTranslation.y + diffY);
  8901. this._redraw();
  8902. this.moved = true;
  8903. }
  8904. vis.util.preventDefault(event);
  8905. };
  8906. /**
  8907. * handle on mouse up event
  8908. * @param {Event} event
  8909. * @private
  8910. */
  8911. Graph.prototype._onMouseUp = function (event) {
  8912. event = event || window.event;
  8913. if (!this.selectable) {
  8914. return;
  8915. }
  8916. // remove event listeners here, important for Safari
  8917. if (this.onmousemove) {
  8918. vis.util.removeEventListener(document, "mousemove", this.onmousemove);
  8919. this.onmousemove = undefined;
  8920. }
  8921. if (this.onmouseup) {
  8922. vis.util.removeEventListener(document, "mouseup", this.onmouseup);
  8923. this.onmouseup = undefined;
  8924. }
  8925. vis.util.preventDefault(event);
  8926. // check selected nodes
  8927. var endMouseX = util.getPageX(event) || this.mouseX || 0;
  8928. var endMouseY = util.getPageY(event) || this.mouseY || 0;
  8929. var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
  8930. if (this.startClickedObj) {
  8931. // restore the original fixed state
  8932. var node = this.nodes[this.startClickedObj];
  8933. node.xFixed = this.startClickedObj.xFixed;
  8934. node.yFixed = this.startClickedObj.yFixed;
  8935. }
  8936. else if (this.shiftKeyDown) {
  8937. // select nodes inside selection area
  8938. var obj = {
  8939. "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
  8940. "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
  8941. "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
  8942. "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
  8943. };
  8944. var overlappingNodes = this._getNodesOverlappingWith(obj);
  8945. this._selectNodes(overlappingNodes, ctrlKey);
  8946. this.redraw();
  8947. // remove the selection rectangle
  8948. if (this.frame.selRect) {
  8949. this.frame.removeChild(this.frame.selRect);
  8950. this.frame.selRect = undefined;
  8951. }
  8952. }
  8953. else {
  8954. if (!this.ctrlKeyDown && !this.moved) {
  8955. // remove selection
  8956. this._unselectNodes();
  8957. this._redraw();
  8958. }
  8959. }
  8960. this.leftButtonDown = false;
  8961. this.ctrlKeyDown = false;
  8962. };
  8963. /**
  8964. * Event handler for mouse wheel event, used to zoom the timeline
  8965. * Code from http://adomas.org/javascript-mouse-wheel/
  8966. * @param {Event} event
  8967. * @private
  8968. */
  8969. Graph.prototype._onMouseWheel = function(event) {
  8970. event = event || window.event;
  8971. var mouseX = util.getPageX(event);
  8972. var mouseY = util.getPageY(event);
  8973. // retrieve delta
  8974. var delta = 0;
  8975. if (event.wheelDelta) { /* IE/Opera. */
  8976. delta = event.wheelDelta/120;
  8977. } else if (event.detail) { /* Mozilla case. */
  8978. // In Mozilla, sign of delta is different than in IE.
  8979. // Also, delta is multiple of 3.
  8980. delta = -event.detail/3;
  8981. }
  8982. // If delta is nonzero, handle it.
  8983. // Basically, delta is now positive if wheel was scrolled up,
  8984. // and negative, if wheel was scrolled down.
  8985. if (delta) {
  8986. // determine zoom factor, and adjust the zoom factor such that zooming in
  8987. // and zooming out correspond wich each other
  8988. var zoom = delta / 10;
  8989. if (delta < 0) {
  8990. zoom = zoom / (1 - zoom);
  8991. }
  8992. var scaleOld = this._getScale();
  8993. var scaleNew = scaleOld * (1 + zoom);
  8994. if (scaleNew < 0.01) {
  8995. scaleNew = 0.01;
  8996. }
  8997. if (scaleNew > 10) {
  8998. scaleNew = 10;
  8999. }
  9000. var frameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
  9001. var frameTop = vis.util.getAbsoluteTop(this.frame.canvas);
  9002. var x = mouseX - frameLeft;
  9003. var y = mouseY - frameTop;
  9004. var translation = this._getTranslation();
  9005. var scaleFrac = scaleNew / scaleOld;
  9006. var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
  9007. var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
  9008. this._setScale(scaleNew);
  9009. this._setTranslation(tx, ty);
  9010. this._redraw();
  9011. }
  9012. // Prevent default actions caused by mouse wheel.
  9013. // That might be ugly, but we handle scrolls somehow
  9014. // anyway, so don't bother here...
  9015. vis.util.preventDefault(event);
  9016. };
  9017. /**
  9018. * Mouse move handler for checking whether the title moves over a node with a title.
  9019. * @param {Event} event
  9020. * @private
  9021. */
  9022. Graph.prototype._onMouseMoveTitle = function (event) {
  9023. event = event || window.event;
  9024. var startMouseX = util.getPageX(event);
  9025. var startMouseY = util.getPageY(event);
  9026. this.startFrameLeft = this.startFrameLeft || vis.util.getAbsoluteLeft(this.frame.canvas);
  9027. this.startFrameTop = this.startFrameTop || vis.util.getAbsoluteTop(this.frame.canvas);
  9028. var x = startMouseX - this.startFrameLeft;
  9029. var y = startMouseY - this.startFrameTop;
  9030. // check if the previously selected node is still selected
  9031. if (this.popupNode) {
  9032. this._checkHidePopup(x, y);
  9033. }
  9034. // start a timeout that will check if the mouse is positioned above
  9035. // an element
  9036. var me = this;
  9037. var checkShow = function() {
  9038. me._checkShowPopup(x, y);
  9039. };
  9040. if (this.popupTimer) {
  9041. clearInterval(this.popupTimer); // stop any running timer
  9042. }
  9043. if (!this.leftButtonDown) {
  9044. this.popupTimer = setTimeout(checkShow, 300);
  9045. }
  9046. };
  9047. /**
  9048. * Check if there is an element on the given position in the graph
  9049. * (a node or edge). If so, and if this element has a title,
  9050. * show a popup window with its title.
  9051. *
  9052. * @param {number} x
  9053. * @param {number} y
  9054. * @private
  9055. */
  9056. Graph.prototype._checkShowPopup = function (x, y) {
  9057. var obj = {
  9058. "left" : this._xToCanvas(x),
  9059. "top" : this._yToCanvas(y),
  9060. "right" : this._xToCanvas(x),
  9061. "bottom" : this._yToCanvas(y)
  9062. };
  9063. var id;
  9064. var lastPopupNode = this.popupNode;
  9065. if (this.popupNode == undefined) {
  9066. // search the nodes for overlap, select the top one in case of multiple nodes
  9067. var nodes = this.nodes;
  9068. for (id in nodes) {
  9069. if (nodes.hasOwnProperty(id)) {
  9070. var node = nodes[id];
  9071. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  9072. this.popupNode = node;
  9073. break;
  9074. }
  9075. }
  9076. }
  9077. }
  9078. if (this.popupNode == undefined) {
  9079. // search the edges for overlap
  9080. var edges = this.edges;
  9081. for (id in edges) {
  9082. if (edges.hasOwnProperty(id)) {
  9083. var edge = edges[id];
  9084. if (edge.connected && (edge.getTitle() != undefined) &&
  9085. edge.isOverlappingWith(obj)) {
  9086. this.popupNode = edge;
  9087. break;
  9088. }
  9089. }
  9090. }
  9091. }
  9092. if (this.popupNode) {
  9093. // show popup message window
  9094. if (this.popupNode != lastPopupNode) {
  9095. var me = this;
  9096. if (!me.popup) {
  9097. me.popup = new Popup(me.frame);
  9098. }
  9099. // adjust a small offset such that the mouse cursor is located in the
  9100. // bottom left location of the popup, and you can easily move over the
  9101. // popup area
  9102. me.popup.setPosition(x - 3, y - 3);
  9103. me.popup.setText(me.popupNode.getTitle());
  9104. me.popup.show();
  9105. }
  9106. }
  9107. else {
  9108. if (this.popup) {
  9109. this.popup.hide();
  9110. }
  9111. }
  9112. };
  9113. /**
  9114. * Check if the popup must be hided, which is the case when the mouse is no
  9115. * longer hovering on the object
  9116. * @param {number} x
  9117. * @param {number} y
  9118. * @private
  9119. */
  9120. Graph.prototype._checkHidePopup = function (x, y) {
  9121. var obj = {
  9122. "left" : x,
  9123. "top" : y,
  9124. "right" : x,
  9125. "bottom" : y
  9126. };
  9127. if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
  9128. this.popupNode = undefined;
  9129. if (this.popup) {
  9130. this.popup.hide();
  9131. }
  9132. }
  9133. };
  9134. /**
  9135. * Event handler for touchstart event on mobile devices
  9136. * @param {Event} event
  9137. * @private
  9138. */
  9139. Graph.prototype._onTouchStart = function(event) {
  9140. vis.util.preventDefault(event);
  9141. if (this.touchDown) {
  9142. // if already moving, return
  9143. return;
  9144. }
  9145. this.touchDown = true;
  9146. var me = this;
  9147. if (!this.ontouchmove) {
  9148. this.ontouchmove = function (event) {me._onTouchMove(event);};
  9149. vis.util.addEventListener(document, "touchmove", this.ontouchmove);
  9150. }
  9151. if (!this.ontouchend) {
  9152. this.ontouchend = function (event) {me._onTouchEnd(event);};
  9153. vis.util.addEventListener(document, "touchend", this.ontouchend);
  9154. }
  9155. this._onMouseDown(event);
  9156. };
  9157. /**
  9158. * Event handler for touchmove event on mobile devices
  9159. * @param {Event} event
  9160. * @private
  9161. */
  9162. Graph.prototype._onTouchMove = function(event) {
  9163. vis.util.preventDefault(event);
  9164. this._onMouseMove(event);
  9165. };
  9166. /**
  9167. * Event handler for touchend event on mobile devices
  9168. * @param {Event} event
  9169. * @private
  9170. */
  9171. Graph.prototype._onTouchEnd = function(event) {
  9172. vis.util.preventDefault(event);
  9173. this.touchDown = false;
  9174. if (this.ontouchmove) {
  9175. vis.util.removeEventListener(document, "touchmove", this.ontouchmove);
  9176. this.ontouchmove = undefined;
  9177. }
  9178. if (this.ontouchend) {
  9179. vis.util.removeEventListener(document, "touchend", this.ontouchend);
  9180. this.ontouchend = undefined;
  9181. }
  9182. this._onMouseUp(event);
  9183. };
  9184. /**
  9185. * Unselect selected nodes. If no selection array is provided, all nodes
  9186. * are unselected
  9187. * @param {Object[]} selection Array with selection objects, each selection
  9188. * object has a parameter row. Optional
  9189. * @param {Boolean} triggerSelect If true (default), the select event
  9190. * is triggered when nodes are unselected
  9191. * @return {Boolean} changed True if the selection is changed
  9192. * @private
  9193. */
  9194. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  9195. var changed = false;
  9196. var i, iMax, id;
  9197. if (selection) {
  9198. // remove provided selections
  9199. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9200. id = selection[i];
  9201. this.nodes[id].unselect();
  9202. var j = 0;
  9203. while (j < this.selection.length) {
  9204. if (this.selection[j] == id) {
  9205. this.selection.splice(j, 1);
  9206. changed = true;
  9207. }
  9208. else {
  9209. j++;
  9210. }
  9211. }
  9212. }
  9213. }
  9214. else if (this.selection && this.selection.length) {
  9215. // remove all selections
  9216. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9217. id = this.selection[i];
  9218. this.nodes[id].unselect();
  9219. changed = true;
  9220. }
  9221. this.selection = [];
  9222. }
  9223. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  9224. // fire the select event
  9225. this._trigger('select');
  9226. }
  9227. return changed;
  9228. };
  9229. /**
  9230. * select all nodes on given location x, y
  9231. * @param {Array} selection an array with node ids
  9232. * @param {boolean} append If true, the new selection will be appended to the
  9233. * current selection (except for duplicate entries)
  9234. * @return {Boolean} changed True if the selection is changed
  9235. * @private
  9236. */
  9237. Graph.prototype._selectNodes = function(selection, append) {
  9238. var changed = false;
  9239. var i, iMax;
  9240. // TODO: the selectNodes method is a little messy, rework this
  9241. // check if the current selection equals the desired selection
  9242. var selectionAlreadyThere = true;
  9243. if (selection.length != this.selection.length) {
  9244. selectionAlreadyThere = false;
  9245. }
  9246. else {
  9247. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  9248. if (selection[i] != this.selection[i]) {
  9249. selectionAlreadyThere = false;
  9250. break;
  9251. }
  9252. }
  9253. }
  9254. if (selectionAlreadyThere) {
  9255. return changed;
  9256. }
  9257. if (append == undefined || append == false) {
  9258. // first deselect any selected node
  9259. var triggerSelect = false;
  9260. changed = this._unselectNodes(undefined, triggerSelect);
  9261. }
  9262. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9263. // add each of the new selections, but only when they are not duplicate
  9264. var id = selection[i];
  9265. var isDuplicate = (this.selection.indexOf(id) != -1);
  9266. if (!isDuplicate) {
  9267. this.nodes[id].select();
  9268. this.selection.push(id);
  9269. changed = true;
  9270. }
  9271. }
  9272. if (changed) {
  9273. // fire the select event
  9274. this._trigger('select');
  9275. }
  9276. return changed;
  9277. };
  9278. /**
  9279. * retrieve all nodes overlapping with given object
  9280. * @param {Object} obj An object with parameters left, top, right, bottom
  9281. * @return {Object[]} An array with selection objects containing
  9282. * the parameter row.
  9283. * @private
  9284. */
  9285. Graph.prototype._getNodesOverlappingWith = function (obj) {
  9286. var nodes = this.nodes,
  9287. overlappingNodes = [];
  9288. for (var id in nodes) {
  9289. if (nodes.hasOwnProperty(id)) {
  9290. if (nodes[id].isOverlappingWith(obj)) {
  9291. overlappingNodes.push(id);
  9292. }
  9293. }
  9294. }
  9295. return overlappingNodes;
  9296. };
  9297. /**
  9298. * retrieve the currently selected nodes
  9299. * @return {Number[] | String[]} selection An array with the ids of the
  9300. * selected nodes.
  9301. */
  9302. Graph.prototype.getSelection = function() {
  9303. return this.selection.concat([]);
  9304. };
  9305. /**
  9306. * select zero or more nodes
  9307. * @param {Number[] | String[]} selection An array with the ids of the
  9308. * selected nodes.
  9309. */
  9310. Graph.prototype.setSelection = function(selection) {
  9311. var i, iMax, id;
  9312. if (selection.length == undefined)
  9313. throw "Selection must be an array with ids";
  9314. // first unselect any selected node
  9315. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  9316. id = this.selection[i];
  9317. this.nodes[id].unselect();
  9318. }
  9319. this.selection = [];
  9320. for (i = 0, iMax = selection.length; i < iMax; i++) {
  9321. id = selection[i];
  9322. var node = this.nodes[id];
  9323. if (!node) {
  9324. throw new RangeError('Node with id "' + id + '" not found');
  9325. }
  9326. node.select();
  9327. this.selection.push(id);
  9328. }
  9329. this.redraw();
  9330. };
  9331. /**
  9332. * Validate the selection: remove ids of nodes which no longer exist
  9333. * @private
  9334. */
  9335. Graph.prototype._updateSelection = function () {
  9336. var i = 0;
  9337. while (i < this.selection.length) {
  9338. var id = this.selection[i];
  9339. if (!this.nodes[id]) {
  9340. this.selection.splice(i, 1);
  9341. }
  9342. else {
  9343. i++;
  9344. }
  9345. }
  9346. };
  9347. /**
  9348. * Temporary method to test calculating a hub value for the nodes
  9349. * @param {number} level Maximum number edges between two nodes in order
  9350. * to call them connected. Optional, 1 by default
  9351. * @return {Number[]} connectioncount array with the connection count
  9352. * for each node
  9353. * @private
  9354. */
  9355. Graph.prototype._getConnectionCount = function(level) {
  9356. if (level == undefined) {
  9357. level = 1;
  9358. }
  9359. // get the nodes connected to given nodes
  9360. function getConnectedNodes(nodes) {
  9361. var connectedNodes = [];
  9362. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  9363. var node = nodes[j];
  9364. // find all nodes connected to this node
  9365. var edges = node.edges;
  9366. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  9367. var edge = edges[i];
  9368. var other = null;
  9369. // check if connected
  9370. if (edge.from == node)
  9371. other = edge.to;
  9372. else if (edge.to == node)
  9373. other = edge.from;
  9374. // check if the other node is not already in the list with nodes
  9375. var k, kMax;
  9376. if (other) {
  9377. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  9378. if (nodes[k] == other) {
  9379. other = null;
  9380. break;
  9381. }
  9382. }
  9383. }
  9384. if (other) {
  9385. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  9386. if (connectedNodes[k] == other) {
  9387. other = null;
  9388. break;
  9389. }
  9390. }
  9391. }
  9392. if (other)
  9393. connectedNodes.push(other);
  9394. }
  9395. }
  9396. return connectedNodes;
  9397. }
  9398. var connections = [];
  9399. var nodes = this.nodes;
  9400. for (var id in nodes) {
  9401. if (nodes.hasOwnProperty(id)) {
  9402. var c = [nodes[id]];
  9403. for (var l = 0; l < level; l++) {
  9404. c = c.concat(getConnectedNodes(c));
  9405. }
  9406. connections.push(c);
  9407. }
  9408. }
  9409. var hubs = [];
  9410. for (var i = 0, len = connections.length; i < len; i++) {
  9411. hubs.push(connections[i].length);
  9412. }
  9413. return hubs;
  9414. };
  9415. /**
  9416. * Set a new size for the graph
  9417. * @param {string} width Width in pixels or percentage (for example "800px"
  9418. * or "50%")
  9419. * @param {string} height Height in pixels or percentage (for example "400px"
  9420. * or "30%")
  9421. */
  9422. Graph.prototype.setSize = function(width, height) {
  9423. this.frame.style.width = width;
  9424. this.frame.style.height = height;
  9425. this.frame.canvas.style.width = "100%";
  9426. this.frame.canvas.style.height = "100%";
  9427. this.frame.canvas.width = this.frame.canvas.clientWidth;
  9428. this.frame.canvas.height = this.frame.canvas.clientHeight;
  9429. };
  9430. /**
  9431. * Set a data set with nodes for the graph
  9432. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  9433. * @private
  9434. */
  9435. Graph.prototype._setNodes = function(nodes) {
  9436. var oldNodesData = this.nodesData;
  9437. if (nodes instanceof DataSet || nodes instanceof DataView) {
  9438. this.nodesData = nodes;
  9439. }
  9440. else if (nodes instanceof Array) {
  9441. this.nodesData = new DataSet();
  9442. this.nodesData.add(nodes);
  9443. }
  9444. else if (!nodes) {
  9445. this.nodesData = new DataSet();
  9446. }
  9447. else {
  9448. throw new TypeError('Array or DataSet expected');
  9449. }
  9450. if (oldNodesData) {
  9451. // unsubscribe from old dataset
  9452. util.forEach(this.nodesListeners, function (callback, event) {
  9453. oldNodesData.unsubscribe(event, callback);
  9454. });
  9455. }
  9456. // remove drawn nodes
  9457. this.nodes = {};
  9458. if (this.nodesData) {
  9459. // subscribe to new dataset
  9460. var me = this;
  9461. util.forEach(this.nodesListeners, function (callback, event) {
  9462. me.nodesData.subscribe(event, callback);
  9463. });
  9464. // draw all new nodes
  9465. var ids = this.nodesData.getIds();
  9466. this._addNodes(ids);
  9467. }
  9468. this._updateSelection();
  9469. };
  9470. /**
  9471. * Add nodes
  9472. * @param {Number[] | String[]} ids
  9473. * @private
  9474. */
  9475. Graph.prototype._addNodes = function(ids) {
  9476. var id;
  9477. for (var i = 0, len = ids.length; i < len; i++) {
  9478. id = ids[i];
  9479. var data = this.nodesData.get(id);
  9480. var node = new Node(data, this.images, this.groups, this.constants);
  9481. this.nodes[id] = node; // note: this may replace an existing node
  9482. if (!node.isFixed()) {
  9483. // TODO: position new nodes in a smarter way!
  9484. var radius = this.constants.edges.length * 2;
  9485. var count = ids.length;
  9486. var angle = 2 * Math.PI * (i / count);
  9487. node.x = radius * Math.cos(angle);
  9488. node.y = radius * Math.sin(angle);
  9489. // note: no not use node.isMoving() here, as that gives the current
  9490. // velocity of the node, which is zero after creation of the node.
  9491. this.moving = true;
  9492. }
  9493. }
  9494. this._reconnectEdges();
  9495. this._updateValueRange(this.nodes);
  9496. };
  9497. /**
  9498. * Update existing nodes, or create them when not yet existing
  9499. * @param {Number[] | String[]} ids
  9500. * @private
  9501. */
  9502. Graph.prototype._updateNodes = function(ids) {
  9503. var nodes = this.nodes,
  9504. nodesData = this.nodesData;
  9505. for (var i = 0, len = ids.length; i < len; i++) {
  9506. var id = ids[i];
  9507. var node = nodes[id];
  9508. var data = nodesData.get(id);
  9509. if (node) {
  9510. // update node
  9511. node.setProperties(data, this.constants);
  9512. }
  9513. else {
  9514. // create node
  9515. node = new Node(properties, this.images, this.groups, this.constants);
  9516. nodes[id] = node;
  9517. if (!node.isFixed()) {
  9518. this.moving = true;
  9519. }
  9520. }
  9521. }
  9522. this._reconnectEdges();
  9523. this._updateValueRange(nodes);
  9524. };
  9525. /**
  9526. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  9527. * @param {Number[] | String[]} ids
  9528. * @private
  9529. */
  9530. Graph.prototype._removeNodes = function(ids) {
  9531. var nodes = this.nodes;
  9532. for (var i = 0, len = ids.length; i < len; i++) {
  9533. var id = ids[i];
  9534. delete nodes[id];
  9535. }
  9536. this._reconnectEdges();
  9537. this._updateSelection();
  9538. this._updateValueRange(nodes);
  9539. };
  9540. /**
  9541. * Load edges by reading the data table
  9542. * @param {Array | DataSet | DataView} edges The data containing the edges.
  9543. * @private
  9544. * @private
  9545. */
  9546. Graph.prototype._setEdges = function(edges) {
  9547. var oldEdgesData = this.edgesData;
  9548. if (edges instanceof DataSet || edges instanceof DataView) {
  9549. this.edgesData = edges;
  9550. }
  9551. else if (edges instanceof Array) {
  9552. this.edgesData = new DataSet();
  9553. this.edgesData.add(edges);
  9554. }
  9555. else if (!edges) {
  9556. this.edgesData = new DataSet();
  9557. }
  9558. else {
  9559. throw new TypeError('Array or DataSet expected');
  9560. }
  9561. if (oldEdgesData) {
  9562. // unsubscribe from old dataset
  9563. util.forEach(this.edgesListeners, function (callback, event) {
  9564. oldEdgesData.unsubscribe(event, callback);
  9565. });
  9566. }
  9567. // remove drawn edges
  9568. this.edges = {};
  9569. if (this.edgesData) {
  9570. // subscribe to new dataset
  9571. var me = this;
  9572. util.forEach(this.edgesListeners, function (callback, event) {
  9573. me.edgesData.subscribe(event, callback);
  9574. });
  9575. // draw all new nodes
  9576. var ids = this.edgesData.getIds();
  9577. this._addEdges(ids);
  9578. }
  9579. this._reconnectEdges();
  9580. };
  9581. /**
  9582. * Add edges
  9583. * @param {Number[] | String[]} ids
  9584. * @private
  9585. */
  9586. Graph.prototype._addEdges = function (ids) {
  9587. var edges = this.edges,
  9588. edgesData = this.edgesData;
  9589. for (var i = 0, len = ids.length; i < len; i++) {
  9590. var id = ids[i];
  9591. var oldEdge = edges[id];
  9592. if (oldEdge) {
  9593. oldEdge.disconnect();
  9594. }
  9595. var data = edgesData.get(id);
  9596. edges[id] = new Edge(data, this, this.constants);
  9597. }
  9598. this.moving = true;
  9599. this._updateValueRange(edges);
  9600. };
  9601. /**
  9602. * Update existing edges, or create them when not yet existing
  9603. * @param {Number[] | String[]} ids
  9604. * @private
  9605. */
  9606. Graph.prototype._updateEdges = function (ids) {
  9607. var edges = this.edges,
  9608. edgesData = this.edgesData;
  9609. for (var i = 0, len = ids.length; i < len; i++) {
  9610. var id = ids[i];
  9611. var data = edgesData.get(id);
  9612. var edge = edges[id];
  9613. if (edge) {
  9614. // update edge
  9615. edge.disconnect();
  9616. edge.setProperties(data, this.constants);
  9617. edge.connect();
  9618. }
  9619. else {
  9620. // create edge
  9621. edge = new Edge(data, this, this.constants);
  9622. this.edges[id] = edge;
  9623. }
  9624. }
  9625. this.moving = true;
  9626. this._updateValueRange(edges);
  9627. };
  9628. /**
  9629. * Remove existing edges. Non existing ids will be ignored
  9630. * @param {Number[] | String[]} ids
  9631. * @private
  9632. */
  9633. Graph.prototype._removeEdges = function (ids) {
  9634. var edges = this.edges;
  9635. for (var i = 0, len = ids.length; i < len; i++) {
  9636. var id = ids[i];
  9637. var edge = edges[id];
  9638. if (edge) {
  9639. edge.disconnect();
  9640. delete edges[id];
  9641. }
  9642. }
  9643. this.moving = true;
  9644. this._updateValueRange(edges);
  9645. };
  9646. /**
  9647. * Reconnect all edges
  9648. * @private
  9649. */
  9650. Graph.prototype._reconnectEdges = function() {
  9651. var id,
  9652. nodes = this.nodes,
  9653. edges = this.edges;
  9654. for (id in nodes) {
  9655. if (nodes.hasOwnProperty(id)) {
  9656. nodes[id].edges = [];
  9657. }
  9658. }
  9659. for (id in edges) {
  9660. if (edges.hasOwnProperty(id)) {
  9661. var edge = edges[id];
  9662. edge.from = null;
  9663. edge.to = null;
  9664. edge.connect();
  9665. }
  9666. }
  9667. };
  9668. /**
  9669. * Update the values of all object in the given array according to the current
  9670. * value range of the objects in the array.
  9671. * @param {Object} obj An object containing a set of Edges or Nodes
  9672. * The objects must have a method getValue() and
  9673. * setValueRange(min, max).
  9674. * @private
  9675. */
  9676. Graph.prototype._updateValueRange = function(obj) {
  9677. var id;
  9678. // determine the range of the objects
  9679. var valueMin = undefined;
  9680. var valueMax = undefined;
  9681. for (id in obj) {
  9682. if (obj.hasOwnProperty(id)) {
  9683. var value = obj[id].getValue();
  9684. if (value !== undefined) {
  9685. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  9686. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  9687. }
  9688. }
  9689. }
  9690. // adjust the range of all objects
  9691. if (valueMin !== undefined && valueMax !== undefined) {
  9692. for (id in obj) {
  9693. if (obj.hasOwnProperty(id)) {
  9694. obj[id].setValueRange(valueMin, valueMax);
  9695. }
  9696. }
  9697. }
  9698. };
  9699. /**
  9700. * Redraw the graph with the current data
  9701. * chart will be resized too.
  9702. */
  9703. Graph.prototype.redraw = function() {
  9704. this.setSize(this.width, this.height);
  9705. this._redraw();
  9706. };
  9707. /**
  9708. * Redraw the graph with the current data
  9709. * @private
  9710. */
  9711. Graph.prototype._redraw = function() {
  9712. var ctx = this.frame.canvas.getContext("2d");
  9713. // clear the canvas
  9714. var w = this.frame.canvas.width;
  9715. var h = this.frame.canvas.height;
  9716. ctx.clearRect(0, 0, w, h);
  9717. // set scaling and translation
  9718. ctx.save();
  9719. ctx.translate(this.translation.x, this.translation.y);
  9720. ctx.scale(this.scale, this.scale);
  9721. this._drawEdges(ctx);
  9722. this._drawNodes(ctx);
  9723. // restore original scaling and translation
  9724. ctx.restore();
  9725. };
  9726. /**
  9727. * Set the translation of the graph
  9728. * @param {Number} offsetX Horizontal offset
  9729. * @param {Number} offsetY Vertical offset
  9730. * @private
  9731. */
  9732. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  9733. if (this.translation === undefined) {
  9734. this.translation = {
  9735. "x": 0,
  9736. "y": 0
  9737. };
  9738. }
  9739. if (offsetX !== undefined) {
  9740. this.translation.x = offsetX;
  9741. }
  9742. if (offsetY !== undefined) {
  9743. this.translation.y = offsetY;
  9744. }
  9745. };
  9746. /**
  9747. * Get the translation of the graph
  9748. * @return {Object} translation An object with parameters x and y, both a number
  9749. * @private
  9750. */
  9751. Graph.prototype._getTranslation = function() {
  9752. return {
  9753. "x": this.translation.x,
  9754. "y": this.translation.y
  9755. };
  9756. };
  9757. /**
  9758. * Scale the graph
  9759. * @param {Number} scale Scaling factor 1.0 is unscaled
  9760. * @private
  9761. */
  9762. Graph.prototype._setScale = function(scale) {
  9763. this.scale = scale;
  9764. };
  9765. /**
  9766. * Get the current scale of the graph
  9767. * @return {Number} scale Scaling factor 1.0 is unscaled
  9768. * @private
  9769. */
  9770. Graph.prototype._getScale = function() {
  9771. return this.scale;
  9772. };
  9773. Graph.prototype._xToCanvas = function(x) {
  9774. return (x - this.translation.x) / this.scale;
  9775. };
  9776. Graph.prototype._canvasToX = function(x) {
  9777. return x * this.scale + this.translation.x;
  9778. };
  9779. Graph.prototype._yToCanvas = function(y) {
  9780. return (y - this.translation.y) / this.scale;
  9781. };
  9782. Graph.prototype._canvasToY = function(y) {
  9783. return y * this.scale + this.translation.y ;
  9784. };
  9785. /**
  9786. * Redraw all nodes
  9787. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9788. * @param {CanvasRenderingContext2D} ctx
  9789. * @private
  9790. */
  9791. Graph.prototype._drawNodes = function(ctx) {
  9792. // first draw the unselected nodes
  9793. var nodes = this.nodes;
  9794. var selected = [];
  9795. for (var id in nodes) {
  9796. if (nodes.hasOwnProperty(id)) {
  9797. if (nodes[id].isSelected()) {
  9798. selected.push(id);
  9799. }
  9800. else {
  9801. nodes[id].draw(ctx);
  9802. }
  9803. }
  9804. }
  9805. // draw the selected nodes on top
  9806. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  9807. nodes[selected[s]].draw(ctx);
  9808. }
  9809. };
  9810. /**
  9811. * Redraw all edges
  9812. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9813. * @param {CanvasRenderingContext2D} ctx
  9814. * @private
  9815. */
  9816. Graph.prototype._drawEdges = function(ctx) {
  9817. var edges = this.edges;
  9818. for (var id in edges) {
  9819. if (edges.hasOwnProperty(id)) {
  9820. var edge = edges[id];
  9821. if (edge.connected) {
  9822. edges[id].draw(ctx);
  9823. }
  9824. }
  9825. }
  9826. };
  9827. /**
  9828. * Find a stable position for all nodes
  9829. * @private
  9830. */
  9831. Graph.prototype._doStabilize = function() {
  9832. var start = new Date();
  9833. // find stable position
  9834. var count = 0;
  9835. var vmin = this.constants.minVelocity;
  9836. var stable = false;
  9837. while (!stable && count < this.constants.maxIterations) {
  9838. this._calculateForces();
  9839. this._discreteStepNodes();
  9840. stable = !this._isMoving(vmin);
  9841. count++;
  9842. }
  9843. var end = new Date();
  9844. // console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
  9845. };
  9846. /**
  9847. * Calculate the external forces acting on the nodes
  9848. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9849. * @private
  9850. */
  9851. Graph.prototype._calculateForces = function() {
  9852. // create a local edge to the nodes and edges, that is faster
  9853. var id, dx, dy, angle, distance, fx, fy,
  9854. repulsingForce, springForce, length, edgeLength,
  9855. nodes = this.nodes,
  9856. edges = this.edges;
  9857. // gravity, add a small constant force to pull the nodes towards the center of
  9858. // the graph
  9859. // Also, the forces are reset to zero in this loop by using _setForce instead
  9860. // of _addForce
  9861. var gravity = 0.01,
  9862. gx = this.frame.canvas.clientWidth / 2,
  9863. gy = this.frame.canvas.clientHeight / 2;
  9864. for (id in nodes) {
  9865. if (nodes.hasOwnProperty(id)) {
  9866. var node = nodes[id];
  9867. dx = gx - node.x;
  9868. dy = gy - node.y;
  9869. angle = Math.atan2(dy, dx);
  9870. fx = Math.cos(angle) * gravity;
  9871. fy = Math.sin(angle) * gravity;
  9872. node._setForce(fx, fy);
  9873. }
  9874. }
  9875. // repulsing forces between nodes
  9876. var minimumDistance = this.constants.nodes.distance,
  9877. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9878. for (var id1 in nodes) {
  9879. if (nodes.hasOwnProperty(id1)) {
  9880. var node1 = nodes[id1];
  9881. for (var id2 in nodes) {
  9882. if (nodes.hasOwnProperty(id2)) {
  9883. var node2 = nodes[id2];
  9884. // calculate normally distributed force
  9885. dx = node2.x - node1.x;
  9886. dy = node2.y - node1.y;
  9887. distance = Math.sqrt(dx * dx + dy * dy);
  9888. angle = Math.atan2(dy, dx);
  9889. // TODO: correct factor for repulsing force
  9890. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9891. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9892. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  9893. fx = Math.cos(angle) * repulsingForce;
  9894. fy = Math.sin(angle) * repulsingForce;
  9895. node1._addForce(-fx, -fy);
  9896. node2._addForce(fx, fy);
  9897. }
  9898. }
  9899. }
  9900. }
  9901. /* TODO: re-implement repulsion of edges
  9902. for (var n = 0; n < nodes.length; n++) {
  9903. for (var l = 0; l < edges.length; l++) {
  9904. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9905. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9906. // calculate normally distributed force
  9907. dx = nodes[n].x - lx,
  9908. dy = nodes[n].y - ly,
  9909. distance = Math.sqrt(dx * dx + dy * dy),
  9910. angle = Math.atan2(dy, dx),
  9911. // TODO: correct factor for repulsing force
  9912. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9913. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9914. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  9915. fx = Math.cos(angle) * repulsingforce,
  9916. fy = Math.sin(angle) * repulsingforce;
  9917. nodes[n]._addForce(fx, fy);
  9918. edges[l].from._addForce(-fx/2,-fy/2);
  9919. edges[l].to._addForce(-fx/2,-fy/2);
  9920. }
  9921. }
  9922. */
  9923. // forces caused by the edges, modelled as springs
  9924. for (id in edges) {
  9925. if (edges.hasOwnProperty(id)) {
  9926. var edge = edges[id];
  9927. if (edge.connected) {
  9928. dx = (edge.to.x - edge.from.x);
  9929. dy = (edge.to.y - edge.from.y);
  9930. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  9931. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  9932. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  9933. edgeLength = edge.length;
  9934. length = Math.sqrt(dx * dx + dy * dy);
  9935. angle = Math.atan2(dy, dx);
  9936. springForce = edge.stiffness * (edgeLength - length);
  9937. fx = Math.cos(angle) * springForce;
  9938. fy = Math.sin(angle) * springForce;
  9939. edge.from._addForce(-fx, -fy);
  9940. edge.to._addForce(fx, fy);
  9941. }
  9942. }
  9943. }
  9944. /* TODO: re-implement repulsion of edges
  9945. // repulsing forces between edges
  9946. var minimumDistance = this.constants.edges.distance,
  9947. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  9948. for (var l = 0; l < edges.length; l++) {
  9949. //Keep distance from other edge centers
  9950. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  9951. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  9952. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  9953. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  9954. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  9955. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  9956. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  9957. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  9958. // calculate normally distributed force
  9959. dx = l2x - lx,
  9960. dy = l2y - ly,
  9961. distance = Math.sqrt(dx * dx + dy * dy),
  9962. angle = Math.atan2(dy, dx),
  9963. // TODO: correct factor for repulsing force
  9964. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  9965. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  9966. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  9967. fx = Math.cos(angle) * repulsingforce,
  9968. fy = Math.sin(angle) * repulsingforce;
  9969. edges[l].from._addForce(-fx, -fy);
  9970. edges[l].to._addForce(-fx, -fy);
  9971. edges[l2].from._addForce(fx, fy);
  9972. edges[l2].to._addForce(fx, fy);
  9973. }
  9974. }
  9975. */
  9976. };
  9977. /**
  9978. * Check if any of the nodes is still moving
  9979. * @param {number} vmin the minimum velocity considered as "moving"
  9980. * @return {boolean} true if moving, false if non of the nodes is moving
  9981. * @private
  9982. */
  9983. Graph.prototype._isMoving = function(vmin) {
  9984. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  9985. var nodes = this.nodes;
  9986. for (var id in nodes) {
  9987. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  9988. return true;
  9989. }
  9990. }
  9991. return false;
  9992. };
  9993. /**
  9994. * Perform one discrete step for all nodes
  9995. * @private
  9996. */
  9997. Graph.prototype._discreteStepNodes = function() {
  9998. var interval = this.refreshRate / 1000.0; // in seconds
  9999. var nodes = this.nodes;
  10000. for (var id in nodes) {
  10001. if (nodes.hasOwnProperty(id)) {
  10002. nodes[id].discreteStep(interval);
  10003. }
  10004. }
  10005. };
  10006. /**
  10007. * Start animating nodes and edges
  10008. */
  10009. Graph.prototype.start = function() {
  10010. if (this.moving) {
  10011. this._calculateForces();
  10012. this._discreteStepNodes();
  10013. var vmin = this.constants.minVelocity;
  10014. this.moving = this._isMoving(vmin);
  10015. }
  10016. if (this.moving) {
  10017. // start animation. only start timer if it is not already running
  10018. if (!this.timer) {
  10019. var graph = this;
  10020. this.timer = window.setTimeout(function () {
  10021. graph.timer = undefined;
  10022. graph.start();
  10023. graph._redraw();
  10024. }, this.refreshRate);
  10025. }
  10026. }
  10027. else {
  10028. this._redraw();
  10029. }
  10030. };
  10031. /**
  10032. * Stop animating nodes and edges.
  10033. */
  10034. Graph.prototype.stop = function () {
  10035. if (this.timer) {
  10036. window.clearInterval(this.timer);
  10037. this.timer = undefined;
  10038. }
  10039. };
  10040. /**
  10041. * vis.js module exports
  10042. */
  10043. var vis = {
  10044. util: util,
  10045. events: events,
  10046. Controller: Controller,
  10047. DataSet: DataSet,
  10048. DataView: DataView,
  10049. Range: Range,
  10050. Stack: Stack,
  10051. TimeStep: TimeStep,
  10052. EventBus: EventBus,
  10053. components: {
  10054. items: {
  10055. Item: Item,
  10056. ItemBox: ItemBox,
  10057. ItemPoint: ItemPoint,
  10058. ItemRange: ItemRange
  10059. },
  10060. Component: Component,
  10061. Panel: Panel,
  10062. RootPanel: RootPanel,
  10063. ItemSet: ItemSet,
  10064. TimeAxis: TimeAxis
  10065. },
  10066. graph: {
  10067. Node: Node,
  10068. Edge: Edge,
  10069. Popup: Popup,
  10070. Groups: Groups,
  10071. Images: Images
  10072. },
  10073. Timeline: Timeline,
  10074. Graph: Graph
  10075. };
  10076. /**
  10077. * CommonJS module exports
  10078. */
  10079. if (typeof exports !== 'undefined') {
  10080. exports = vis;
  10081. }
  10082. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  10083. module.exports = vis;
  10084. }
  10085. /**
  10086. * AMD module exports
  10087. */
  10088. if (typeof(define) === 'function') {
  10089. define(function () {
  10090. return vis;
  10091. });
  10092. }
  10093. /**
  10094. * Window exports
  10095. */
  10096. if (typeof window !== 'undefined') {
  10097. // attach the module to the window, load as a regular javascript file
  10098. window['vis'] = vis;
  10099. }
  10100. // inject css
  10101. util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .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.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .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.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .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.vis.timeline .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.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .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.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  10102. })()
  10103. },{"moment":2}],2:[function(require,module,exports){
  10104. (function(){// moment.js
  10105. // version : 2.0.0
  10106. // author : Tim Wood
  10107. // license : MIT
  10108. // momentjs.com
  10109. (function (undefined) {
  10110. /************************************
  10111. Constants
  10112. ************************************/
  10113. var moment,
  10114. VERSION = "2.0.0",
  10115. round = Math.round, i,
  10116. // internal storage for language config files
  10117. languages = {},
  10118. // check for nodeJS
  10119. hasModule = (typeof module !== 'undefined' && module.exports),
  10120. // ASP.NET json date format regex
  10121. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  10122. // format tokens
  10123. 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,
  10124. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  10125. // parsing tokens
  10126. parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
  10127. // parsing token regexes
  10128. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  10129. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  10130. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  10131. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  10132. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  10133. 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.
  10134. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  10135. parseTokenT = /T/i, // T (ISO seperator)
  10136. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  10137. // preliminary iso regex
  10138. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  10139. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  10140. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  10141. // iso time formats and regexes
  10142. isoTimes = [
  10143. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  10144. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  10145. ['HH:mm', /(T| )\d\d:\d\d/],
  10146. ['HH', /(T| )\d\d/]
  10147. ],
  10148. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  10149. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  10150. // getter and setter names
  10151. proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  10152. unitMillisecondFactors = {
  10153. 'Milliseconds' : 1,
  10154. 'Seconds' : 1e3,
  10155. 'Minutes' : 6e4,
  10156. 'Hours' : 36e5,
  10157. 'Days' : 864e5,
  10158. 'Months' : 2592e6,
  10159. 'Years' : 31536e6
  10160. },
  10161. // format function strings
  10162. formatFunctions = {},
  10163. // tokens to ordinalize and pad
  10164. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  10165. paddedTokens = 'M D H h m s w W'.split(' '),
  10166. formatTokenFunctions = {
  10167. M : function () {
  10168. return this.month() + 1;
  10169. },
  10170. MMM : function (format) {
  10171. return this.lang().monthsShort(this, format);
  10172. },
  10173. MMMM : function (format) {
  10174. return this.lang().months(this, format);
  10175. },
  10176. D : function () {
  10177. return this.date();
  10178. },
  10179. DDD : function () {
  10180. return this.dayOfYear();
  10181. },
  10182. d : function () {
  10183. return this.day();
  10184. },
  10185. dd : function (format) {
  10186. return this.lang().weekdaysMin(this, format);
  10187. },
  10188. ddd : function (format) {
  10189. return this.lang().weekdaysShort(this, format);
  10190. },
  10191. dddd : function (format) {
  10192. return this.lang().weekdays(this, format);
  10193. },
  10194. w : function () {
  10195. return this.week();
  10196. },
  10197. W : function () {
  10198. return this.isoWeek();
  10199. },
  10200. YY : function () {
  10201. return leftZeroFill(this.year() % 100, 2);
  10202. },
  10203. YYYY : function () {
  10204. return leftZeroFill(this.year(), 4);
  10205. },
  10206. YYYYY : function () {
  10207. return leftZeroFill(this.year(), 5);
  10208. },
  10209. a : function () {
  10210. return this.lang().meridiem(this.hours(), this.minutes(), true);
  10211. },
  10212. A : function () {
  10213. return this.lang().meridiem(this.hours(), this.minutes(), false);
  10214. },
  10215. H : function () {
  10216. return this.hours();
  10217. },
  10218. h : function () {
  10219. return this.hours() % 12 || 12;
  10220. },
  10221. m : function () {
  10222. return this.minutes();
  10223. },
  10224. s : function () {
  10225. return this.seconds();
  10226. },
  10227. S : function () {
  10228. return ~~(this.milliseconds() / 100);
  10229. },
  10230. SS : function () {
  10231. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  10232. },
  10233. SSS : function () {
  10234. return leftZeroFill(this.milliseconds(), 3);
  10235. },
  10236. Z : function () {
  10237. var a = -this.zone(),
  10238. b = "+";
  10239. if (a < 0) {
  10240. a = -a;
  10241. b = "-";
  10242. }
  10243. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  10244. },
  10245. ZZ : function () {
  10246. var a = -this.zone(),
  10247. b = "+";
  10248. if (a < 0) {
  10249. a = -a;
  10250. b = "-";
  10251. }
  10252. return b + leftZeroFill(~~(10 * a / 6), 4);
  10253. },
  10254. X : function () {
  10255. return this.unix();
  10256. }
  10257. };
  10258. function padToken(func, count) {
  10259. return function (a) {
  10260. return leftZeroFill(func.call(this, a), count);
  10261. };
  10262. }
  10263. function ordinalizeToken(func) {
  10264. return function (a) {
  10265. return this.lang().ordinal(func.call(this, a));
  10266. };
  10267. }
  10268. while (ordinalizeTokens.length) {
  10269. i = ordinalizeTokens.pop();
  10270. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
  10271. }
  10272. while (paddedTokens.length) {
  10273. i = paddedTokens.pop();
  10274. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  10275. }
  10276. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  10277. /************************************
  10278. Constructors
  10279. ************************************/
  10280. function Language() {
  10281. }
  10282. // Moment prototype object
  10283. function Moment(config) {
  10284. extend(this, config);
  10285. }
  10286. // Duration Constructor
  10287. function Duration(duration) {
  10288. var data = this._data = {},
  10289. years = duration.years || duration.year || duration.y || 0,
  10290. months = duration.months || duration.month || duration.M || 0,
  10291. weeks = duration.weeks || duration.week || duration.w || 0,
  10292. days = duration.days || duration.day || duration.d || 0,
  10293. hours = duration.hours || duration.hour || duration.h || 0,
  10294. minutes = duration.minutes || duration.minute || duration.m || 0,
  10295. seconds = duration.seconds || duration.second || duration.s || 0,
  10296. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  10297. // representation for dateAddRemove
  10298. this._milliseconds = milliseconds +
  10299. seconds * 1e3 + // 1000
  10300. minutes * 6e4 + // 1000 * 60
  10301. hours * 36e5; // 1000 * 60 * 60
  10302. // Because of dateAddRemove treats 24 hours as different from a
  10303. // day when working around DST, we need to store them separately
  10304. this._days = days +
  10305. weeks * 7;
  10306. // It is impossible translate months into days without knowing
  10307. // which months you are are talking about, so we have to store
  10308. // it separately.
  10309. this._months = months +
  10310. years * 12;
  10311. // The following code bubbles up values, see the tests for
  10312. // examples of what that means.
  10313. data.milliseconds = milliseconds % 1000;
  10314. seconds += absRound(milliseconds / 1000);
  10315. data.seconds = seconds % 60;
  10316. minutes += absRound(seconds / 60);
  10317. data.minutes = minutes % 60;
  10318. hours += absRound(minutes / 60);
  10319. data.hours = hours % 24;
  10320. days += absRound(hours / 24);
  10321. days += weeks * 7;
  10322. data.days = days % 30;
  10323. months += absRound(days / 30);
  10324. data.months = months % 12;
  10325. years += absRound(months / 12);
  10326. data.years = years;
  10327. }
  10328. /************************************
  10329. Helpers
  10330. ************************************/
  10331. function extend(a, b) {
  10332. for (var i in b) {
  10333. if (b.hasOwnProperty(i)) {
  10334. a[i] = b[i];
  10335. }
  10336. }
  10337. return a;
  10338. }
  10339. function absRound(number) {
  10340. if (number < 0) {
  10341. return Math.ceil(number);
  10342. } else {
  10343. return Math.floor(number);
  10344. }
  10345. }
  10346. // left zero fill a number
  10347. // see http://jsperf.com/left-zero-filling for performance comparison
  10348. function leftZeroFill(number, targetLength) {
  10349. var output = number + '';
  10350. while (output.length < targetLength) {
  10351. output = '0' + output;
  10352. }
  10353. return output;
  10354. }
  10355. // helper function for _.addTime and _.subtractTime
  10356. function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
  10357. var ms = duration._milliseconds,
  10358. d = duration._days,
  10359. M = duration._months,
  10360. currentDate;
  10361. if (ms) {
  10362. mom._d.setTime(+mom + ms * isAdding);
  10363. }
  10364. if (d) {
  10365. mom.date(mom.date() + d * isAdding);
  10366. }
  10367. if (M) {
  10368. currentDate = mom.date();
  10369. mom.date(1)
  10370. .month(mom.month() + M * isAdding)
  10371. .date(Math.min(currentDate, mom.daysInMonth()));
  10372. }
  10373. }
  10374. // check if is an array
  10375. function isArray(input) {
  10376. return Object.prototype.toString.call(input) === '[object Array]';
  10377. }
  10378. // compare two arrays, return the number of differences
  10379. function compareArrays(array1, array2) {
  10380. var len = Math.min(array1.length, array2.length),
  10381. lengthDiff = Math.abs(array1.length - array2.length),
  10382. diffs = 0,
  10383. i;
  10384. for (i = 0; i < len; i++) {
  10385. if (~~array1[i] !== ~~array2[i]) {
  10386. diffs++;
  10387. }
  10388. }
  10389. return diffs + lengthDiff;
  10390. }
  10391. /************************************
  10392. Languages
  10393. ************************************/
  10394. Language.prototype = {
  10395. set : function (config) {
  10396. var prop, i;
  10397. for (i in config) {
  10398. prop = config[i];
  10399. if (typeof prop === 'function') {
  10400. this[i] = prop;
  10401. } else {
  10402. this['_' + i] = prop;
  10403. }
  10404. }
  10405. },
  10406. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  10407. months : function (m) {
  10408. return this._months[m.month()];
  10409. },
  10410. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  10411. monthsShort : function (m) {
  10412. return this._monthsShort[m.month()];
  10413. },
  10414. monthsParse : function (monthName) {
  10415. var i, mom, regex, output;
  10416. if (!this._monthsParse) {
  10417. this._monthsParse = [];
  10418. }
  10419. for (i = 0; i < 12; i++) {
  10420. // make the regex if we don't have it already
  10421. if (!this._monthsParse[i]) {
  10422. mom = moment([2000, i]);
  10423. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  10424. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  10425. }
  10426. // test the regex
  10427. if (this._monthsParse[i].test(monthName)) {
  10428. return i;
  10429. }
  10430. }
  10431. },
  10432. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  10433. weekdays : function (m) {
  10434. return this._weekdays[m.day()];
  10435. },
  10436. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  10437. weekdaysShort : function (m) {
  10438. return this._weekdaysShort[m.day()];
  10439. },
  10440. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  10441. weekdaysMin : function (m) {
  10442. return this._weekdaysMin[m.day()];
  10443. },
  10444. _longDateFormat : {
  10445. LT : "h:mm A",
  10446. L : "MM/DD/YYYY",
  10447. LL : "MMMM D YYYY",
  10448. LLL : "MMMM D YYYY LT",
  10449. LLLL : "dddd, MMMM D YYYY LT"
  10450. },
  10451. longDateFormat : function (key) {
  10452. var output = this._longDateFormat[key];
  10453. if (!output && this._longDateFormat[key.toUpperCase()]) {
  10454. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  10455. return val.slice(1);
  10456. });
  10457. this._longDateFormat[key] = output;
  10458. }
  10459. return output;
  10460. },
  10461. meridiem : function (hours, minutes, isLower) {
  10462. if (hours > 11) {
  10463. return isLower ? 'pm' : 'PM';
  10464. } else {
  10465. return isLower ? 'am' : 'AM';
  10466. }
  10467. },
  10468. _calendar : {
  10469. sameDay : '[Today at] LT',
  10470. nextDay : '[Tomorrow at] LT',
  10471. nextWeek : 'dddd [at] LT',
  10472. lastDay : '[Yesterday at] LT',
  10473. lastWeek : '[last] dddd [at] LT',
  10474. sameElse : 'L'
  10475. },
  10476. calendar : function (key, mom) {
  10477. var output = this._calendar[key];
  10478. return typeof output === 'function' ? output.apply(mom) : output;
  10479. },
  10480. _relativeTime : {
  10481. future : "in %s",
  10482. past : "%s ago",
  10483. s : "a few seconds",
  10484. m : "a minute",
  10485. mm : "%d minutes",
  10486. h : "an hour",
  10487. hh : "%d hours",
  10488. d : "a day",
  10489. dd : "%d days",
  10490. M : "a month",
  10491. MM : "%d months",
  10492. y : "a year",
  10493. yy : "%d years"
  10494. },
  10495. relativeTime : function (number, withoutSuffix, string, isFuture) {
  10496. var output = this._relativeTime[string];
  10497. return (typeof output === 'function') ?
  10498. output(number, withoutSuffix, string, isFuture) :
  10499. output.replace(/%d/i, number);
  10500. },
  10501. pastFuture : function (diff, output) {
  10502. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  10503. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  10504. },
  10505. ordinal : function (number) {
  10506. return this._ordinal.replace("%d", number);
  10507. },
  10508. _ordinal : "%d",
  10509. preparse : function (string) {
  10510. return string;
  10511. },
  10512. postformat : function (string) {
  10513. return string;
  10514. },
  10515. week : function (mom) {
  10516. return weekOfYear(mom, this._week.dow, this._week.doy);
  10517. },
  10518. _week : {
  10519. dow : 0, // Sunday is the first day of the week.
  10520. doy : 6 // The week that contains Jan 1st is the first week of the year.
  10521. }
  10522. };
  10523. // Loads a language definition into the `languages` cache. The function
  10524. // takes a key and optionally values. If not in the browser and no values
  10525. // are provided, it will load the language file module. As a convenience,
  10526. // this function also returns the language values.
  10527. function loadLang(key, values) {
  10528. values.abbr = key;
  10529. if (!languages[key]) {
  10530. languages[key] = new Language();
  10531. }
  10532. languages[key].set(values);
  10533. return languages[key];
  10534. }
  10535. // Determines which language definition to use and returns it.
  10536. //
  10537. // With no parameters, it will return the global language. If you
  10538. // pass in a language key, such as 'en', it will return the
  10539. // definition for 'en', so long as 'en' has already been loaded using
  10540. // moment.lang.
  10541. function getLangDefinition(key) {
  10542. if (!key) {
  10543. return moment.fn._lang;
  10544. }
  10545. if (!languages[key] && hasModule) {
  10546. require('./lang/' + key);
  10547. }
  10548. return languages[key];
  10549. }
  10550. /************************************
  10551. Formatting
  10552. ************************************/
  10553. function removeFormattingTokens(input) {
  10554. if (input.match(/\[.*\]/)) {
  10555. return input.replace(/^\[|\]$/g, "");
  10556. }
  10557. return input.replace(/\\/g, "");
  10558. }
  10559. function makeFormatFunction(format) {
  10560. var array = format.match(formattingTokens), i, length;
  10561. for (i = 0, length = array.length; i < length; i++) {
  10562. if (formatTokenFunctions[array[i]]) {
  10563. array[i] = formatTokenFunctions[array[i]];
  10564. } else {
  10565. array[i] = removeFormattingTokens(array[i]);
  10566. }
  10567. }
  10568. return function (mom) {
  10569. var output = "";
  10570. for (i = 0; i < length; i++) {
  10571. output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
  10572. }
  10573. return output;
  10574. };
  10575. }
  10576. // format date using native date object
  10577. function formatMoment(m, format) {
  10578. var i = 5;
  10579. function replaceLongDateFormatTokens(input) {
  10580. return m.lang().longDateFormat(input) || input;
  10581. }
  10582. while (i-- && localFormattingTokens.test(format)) {
  10583. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  10584. }
  10585. if (!formatFunctions[format]) {
  10586. formatFunctions[format] = makeFormatFunction(format);
  10587. }
  10588. return formatFunctions[format](m);
  10589. }
  10590. /************************************
  10591. Parsing
  10592. ************************************/
  10593. // get the regex to find the next token
  10594. function getParseRegexForToken(token) {
  10595. switch (token) {
  10596. case 'DDDD':
  10597. return parseTokenThreeDigits;
  10598. case 'YYYY':
  10599. return parseTokenFourDigits;
  10600. case 'YYYYY':
  10601. return parseTokenSixDigits;
  10602. case 'S':
  10603. case 'SS':
  10604. case 'SSS':
  10605. case 'DDD':
  10606. return parseTokenOneToThreeDigits;
  10607. case 'MMM':
  10608. case 'MMMM':
  10609. case 'dd':
  10610. case 'ddd':
  10611. case 'dddd':
  10612. case 'a':
  10613. case 'A':
  10614. return parseTokenWord;
  10615. case 'X':
  10616. return parseTokenTimestampMs;
  10617. case 'Z':
  10618. case 'ZZ':
  10619. return parseTokenTimezone;
  10620. case 'T':
  10621. return parseTokenT;
  10622. case 'MM':
  10623. case 'DD':
  10624. case 'YY':
  10625. case 'HH':
  10626. case 'hh':
  10627. case 'mm':
  10628. case 'ss':
  10629. case 'M':
  10630. case 'D':
  10631. case 'd':
  10632. case 'H':
  10633. case 'h':
  10634. case 'm':
  10635. case 's':
  10636. return parseTokenOneOrTwoDigits;
  10637. default :
  10638. return new RegExp(token.replace('\\', ''));
  10639. }
  10640. }
  10641. // function to convert string input to date
  10642. function addTimeToArrayFromToken(token, input, config) {
  10643. var a, b,
  10644. datePartArray = config._a;
  10645. switch (token) {
  10646. // MONTH
  10647. case 'M' : // fall through to MM
  10648. case 'MM' :
  10649. datePartArray[1] = (input == null) ? 0 : ~~input - 1;
  10650. break;
  10651. case 'MMM' : // fall through to MMMM
  10652. case 'MMMM' :
  10653. a = getLangDefinition(config._l).monthsParse(input);
  10654. // if we didn't find a month name, mark the date as invalid.
  10655. if (a != null) {
  10656. datePartArray[1] = a;
  10657. } else {
  10658. config._isValid = false;
  10659. }
  10660. break;
  10661. // DAY OF MONTH
  10662. case 'D' : // fall through to DDDD
  10663. case 'DD' : // fall through to DDDD
  10664. case 'DDD' : // fall through to DDDD
  10665. case 'DDDD' :
  10666. if (input != null) {
  10667. datePartArray[2] = ~~input;
  10668. }
  10669. break;
  10670. // YEAR
  10671. case 'YY' :
  10672. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  10673. break;
  10674. case 'YYYY' :
  10675. case 'YYYYY' :
  10676. datePartArray[0] = ~~input;
  10677. break;
  10678. // AM / PM
  10679. case 'a' : // fall through to A
  10680. case 'A' :
  10681. config._isPm = ((input + '').toLowerCase() === 'pm');
  10682. break;
  10683. // 24 HOUR
  10684. case 'H' : // fall through to hh
  10685. case 'HH' : // fall through to hh
  10686. case 'h' : // fall through to hh
  10687. case 'hh' :
  10688. datePartArray[3] = ~~input;
  10689. break;
  10690. // MINUTE
  10691. case 'm' : // fall through to mm
  10692. case 'mm' :
  10693. datePartArray[4] = ~~input;
  10694. break;
  10695. // SECOND
  10696. case 's' : // fall through to ss
  10697. case 'ss' :
  10698. datePartArray[5] = ~~input;
  10699. break;
  10700. // MILLISECOND
  10701. case 'S' :
  10702. case 'SS' :
  10703. case 'SSS' :
  10704. datePartArray[6] = ~~ (('0.' + input) * 1000);
  10705. break;
  10706. // UNIX TIMESTAMP WITH MS
  10707. case 'X':
  10708. config._d = new Date(parseFloat(input) * 1000);
  10709. break;
  10710. // TIMEZONE
  10711. case 'Z' : // fall through to ZZ
  10712. case 'ZZ' :
  10713. config._useUTC = true;
  10714. a = (input + '').match(parseTimezoneChunker);
  10715. if (a && a[1]) {
  10716. config._tzh = ~~a[1];
  10717. }
  10718. if (a && a[2]) {
  10719. config._tzm = ~~a[2];
  10720. }
  10721. // reverse offsets
  10722. if (a && a[0] === '+') {
  10723. config._tzh = -config._tzh;
  10724. config._tzm = -config._tzm;
  10725. }
  10726. break;
  10727. }
  10728. // if the input is null, the date is not valid
  10729. if (input == null) {
  10730. config._isValid = false;
  10731. }
  10732. }
  10733. // convert an array to a date.
  10734. // the array should mirror the parameters below
  10735. // note: all values past the year are optional and will default to the lowest possible value.
  10736. // [year, month, day , hour, minute, second, millisecond]
  10737. function dateFromArray(config) {
  10738. var i, date, input = [];
  10739. if (config._d) {
  10740. return;
  10741. }
  10742. for (i = 0; i < 7; i++) {
  10743. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  10744. }
  10745. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  10746. input[3] += config._tzh || 0;
  10747. input[4] += config._tzm || 0;
  10748. date = new Date(0);
  10749. if (config._useUTC) {
  10750. date.setUTCFullYear(input[0], input[1], input[2]);
  10751. date.setUTCHours(input[3], input[4], input[5], input[6]);
  10752. } else {
  10753. date.setFullYear(input[0], input[1], input[2]);
  10754. date.setHours(input[3], input[4], input[5], input[6]);
  10755. }
  10756. config._d = date;
  10757. }
  10758. // date from string and format string
  10759. function makeDateFromStringAndFormat(config) {
  10760. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  10761. var tokens = config._f.match(formattingTokens),
  10762. string = config._i,
  10763. i, parsedInput;
  10764. config._a = [];
  10765. for (i = 0; i < tokens.length; i++) {
  10766. parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
  10767. if (parsedInput) {
  10768. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  10769. }
  10770. // don't parse if its not a known token
  10771. if (formatTokenFunctions[tokens[i]]) {
  10772. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  10773. }
  10774. }
  10775. // handle am pm
  10776. if (config._isPm && config._a[3] < 12) {
  10777. config._a[3] += 12;
  10778. }
  10779. // if is 12 am, change hours to 0
  10780. if (config._isPm === false && config._a[3] === 12) {
  10781. config._a[3] = 0;
  10782. }
  10783. // return
  10784. dateFromArray(config);
  10785. }
  10786. // date from string and array of format strings
  10787. function makeDateFromStringAndArray(config) {
  10788. var tempConfig,
  10789. tempMoment,
  10790. bestMoment,
  10791. scoreToBeat = 99,
  10792. i,
  10793. currentDate,
  10794. currentScore;
  10795. while (config._f.length) {
  10796. tempConfig = extend({}, config);
  10797. tempConfig._f = config._f.pop();
  10798. makeDateFromStringAndFormat(tempConfig);
  10799. tempMoment = new Moment(tempConfig);
  10800. if (tempMoment.isValid()) {
  10801. bestMoment = tempMoment;
  10802. break;
  10803. }
  10804. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  10805. if (currentScore < scoreToBeat) {
  10806. scoreToBeat = currentScore;
  10807. bestMoment = tempMoment;
  10808. }
  10809. }
  10810. extend(config, bestMoment);
  10811. }
  10812. // date from iso format
  10813. function makeDateFromString(config) {
  10814. var i,
  10815. string = config._i;
  10816. if (isoRegex.exec(string)) {
  10817. config._f = 'YYYY-MM-DDT';
  10818. for (i = 0; i < 4; i++) {
  10819. if (isoTimes[i][1].exec(string)) {
  10820. config._f += isoTimes[i][0];
  10821. break;
  10822. }
  10823. }
  10824. if (parseTokenTimezone.exec(string)) {
  10825. config._f += " Z";
  10826. }
  10827. makeDateFromStringAndFormat(config);
  10828. } else {
  10829. config._d = new Date(string);
  10830. }
  10831. }
  10832. function makeDateFromInput(config) {
  10833. var input = config._i,
  10834. matched = aspNetJsonRegex.exec(input);
  10835. if (input === undefined) {
  10836. config._d = new Date();
  10837. } else if (matched) {
  10838. config._d = new Date(+matched[1]);
  10839. } else if (typeof input === 'string') {
  10840. makeDateFromString(config);
  10841. } else if (isArray(input)) {
  10842. config._a = input.slice(0);
  10843. dateFromArray(config);
  10844. } else {
  10845. config._d = input instanceof Date ? new Date(+input) : new Date(input);
  10846. }
  10847. }
  10848. /************************************
  10849. Relative Time
  10850. ************************************/
  10851. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  10852. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  10853. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  10854. }
  10855. function relativeTime(milliseconds, withoutSuffix, lang) {
  10856. var seconds = round(Math.abs(milliseconds) / 1000),
  10857. minutes = round(seconds / 60),
  10858. hours = round(minutes / 60),
  10859. days = round(hours / 24),
  10860. years = round(days / 365),
  10861. args = seconds < 45 && ['s', seconds] ||
  10862. minutes === 1 && ['m'] ||
  10863. minutes < 45 && ['mm', minutes] ||
  10864. hours === 1 && ['h'] ||
  10865. hours < 22 && ['hh', hours] ||
  10866. days === 1 && ['d'] ||
  10867. days <= 25 && ['dd', days] ||
  10868. days <= 45 && ['M'] ||
  10869. days < 345 && ['MM', round(days / 30)] ||
  10870. years === 1 && ['y'] || ['yy', years];
  10871. args[2] = withoutSuffix;
  10872. args[3] = milliseconds > 0;
  10873. args[4] = lang;
  10874. return substituteTimeAgo.apply({}, args);
  10875. }
  10876. /************************************
  10877. Week of Year
  10878. ************************************/
  10879. // firstDayOfWeek 0 = sun, 6 = sat
  10880. // the day of the week that starts the week
  10881. // (usually sunday or monday)
  10882. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  10883. // the first week is the week that contains the first
  10884. // of this day of the week
  10885. // (eg. ISO weeks use thursday (4))
  10886. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  10887. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  10888. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
  10889. if (daysToDayOfWeek > end) {
  10890. daysToDayOfWeek -= 7;
  10891. }
  10892. if (daysToDayOfWeek < end - 7) {
  10893. daysToDayOfWeek += 7;
  10894. }
  10895. return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
  10896. }
  10897. /************************************
  10898. Top Level Functions
  10899. ************************************/
  10900. function makeMoment(config) {
  10901. var input = config._i,
  10902. format = config._f;
  10903. if (input === null || input === '') {
  10904. return null;
  10905. }
  10906. if (typeof input === 'string') {
  10907. config._i = input = getLangDefinition().preparse(input);
  10908. }
  10909. if (moment.isMoment(input)) {
  10910. config = extend({}, input);
  10911. config._d = new Date(+input._d);
  10912. } else if (format) {
  10913. if (isArray(format)) {
  10914. makeDateFromStringAndArray(config);
  10915. } else {
  10916. makeDateFromStringAndFormat(config);
  10917. }
  10918. } else {
  10919. makeDateFromInput(config);
  10920. }
  10921. return new Moment(config);
  10922. }
  10923. moment = function (input, format, lang) {
  10924. return makeMoment({
  10925. _i : input,
  10926. _f : format,
  10927. _l : lang,
  10928. _isUTC : false
  10929. });
  10930. };
  10931. // creating with utc
  10932. moment.utc = function (input, format, lang) {
  10933. return makeMoment({
  10934. _useUTC : true,
  10935. _isUTC : true,
  10936. _l : lang,
  10937. _i : input,
  10938. _f : format
  10939. });
  10940. };
  10941. // creating with unix timestamp (in seconds)
  10942. moment.unix = function (input) {
  10943. return moment(input * 1000);
  10944. };
  10945. // duration
  10946. moment.duration = function (input, key) {
  10947. var isDuration = moment.isDuration(input),
  10948. isNumber = (typeof input === 'number'),
  10949. duration = (isDuration ? input._data : (isNumber ? {} : input)),
  10950. ret;
  10951. if (isNumber) {
  10952. if (key) {
  10953. duration[key] = input;
  10954. } else {
  10955. duration.milliseconds = input;
  10956. }
  10957. }
  10958. ret = new Duration(duration);
  10959. if (isDuration && input.hasOwnProperty('_lang')) {
  10960. ret._lang = input._lang;
  10961. }
  10962. return ret;
  10963. };
  10964. // version number
  10965. moment.version = VERSION;
  10966. // default format
  10967. moment.defaultFormat = isoFormat;
  10968. // This function will load languages and then set the global language. If
  10969. // no arguments are passed in, it will simply return the current global
  10970. // language key.
  10971. moment.lang = function (key, values) {
  10972. var i;
  10973. if (!key) {
  10974. return moment.fn._lang._abbr;
  10975. }
  10976. if (values) {
  10977. loadLang(key, values);
  10978. } else if (!languages[key]) {
  10979. getLangDefinition(key);
  10980. }
  10981. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  10982. };
  10983. // returns language data
  10984. moment.langData = function (key) {
  10985. if (key && key._lang && key._lang._abbr) {
  10986. key = key._lang._abbr;
  10987. }
  10988. return getLangDefinition(key);
  10989. };
  10990. // compare moment object
  10991. moment.isMoment = function (obj) {
  10992. return obj instanceof Moment;
  10993. };
  10994. // for typechecking Duration objects
  10995. moment.isDuration = function (obj) {
  10996. return obj instanceof Duration;
  10997. };
  10998. /************************************
  10999. Moment Prototype
  11000. ************************************/
  11001. moment.fn = Moment.prototype = {
  11002. clone : function () {
  11003. return moment(this);
  11004. },
  11005. valueOf : function () {
  11006. return +this._d;
  11007. },
  11008. unix : function () {
  11009. return Math.floor(+this._d / 1000);
  11010. },
  11011. toString : function () {
  11012. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  11013. },
  11014. toDate : function () {
  11015. return this._d;
  11016. },
  11017. toJSON : function () {
  11018. return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  11019. },
  11020. toArray : function () {
  11021. var m = this;
  11022. return [
  11023. m.year(),
  11024. m.month(),
  11025. m.date(),
  11026. m.hours(),
  11027. m.minutes(),
  11028. m.seconds(),
  11029. m.milliseconds()
  11030. ];
  11031. },
  11032. isValid : function () {
  11033. if (this._isValid == null) {
  11034. if (this._a) {
  11035. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  11036. } else {
  11037. this._isValid = !isNaN(this._d.getTime());
  11038. }
  11039. }
  11040. return !!this._isValid;
  11041. },
  11042. utc : function () {
  11043. this._isUTC = true;
  11044. return this;
  11045. },
  11046. local : function () {
  11047. this._isUTC = false;
  11048. return this;
  11049. },
  11050. format : function (inputString) {
  11051. var output = formatMoment(this, inputString || moment.defaultFormat);
  11052. return this.lang().postformat(output);
  11053. },
  11054. add : function (input, val) {
  11055. var dur;
  11056. // switch args to support add('s', 1) and add(1, 's')
  11057. if (typeof input === 'string') {
  11058. dur = moment.duration(+val, input);
  11059. } else {
  11060. dur = moment.duration(input, val);
  11061. }
  11062. addOrSubtractDurationFromMoment(this, dur, 1);
  11063. return this;
  11064. },
  11065. subtract : function (input, val) {
  11066. var dur;
  11067. // switch args to support subtract('s', 1) and subtract(1, 's')
  11068. if (typeof input === 'string') {
  11069. dur = moment.duration(+val, input);
  11070. } else {
  11071. dur = moment.duration(input, val);
  11072. }
  11073. addOrSubtractDurationFromMoment(this, dur, -1);
  11074. return this;
  11075. },
  11076. diff : function (input, units, asFloat) {
  11077. var that = this._isUTC ? moment(input).utc() : moment(input).local(),
  11078. zoneDiff = (this.zone() - that.zone()) * 6e4,
  11079. diff, output;
  11080. if (units) {
  11081. // standardize on singular form
  11082. units = units.replace(/s$/, '');
  11083. }
  11084. if (units === 'year' || units === 'month') {
  11085. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  11086. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  11087. output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
  11088. if (units === 'year') {
  11089. output = output / 12;
  11090. }
  11091. } else {
  11092. diff = (this - that) - zoneDiff;
  11093. output = units === 'second' ? diff / 1e3 : // 1000
  11094. units === 'minute' ? diff / 6e4 : // 1000 * 60
  11095. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  11096. units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
  11097. units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
  11098. diff;
  11099. }
  11100. return asFloat ? output : absRound(output);
  11101. },
  11102. from : function (time, withoutSuffix) {
  11103. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  11104. },
  11105. fromNow : function (withoutSuffix) {
  11106. return this.from(moment(), withoutSuffix);
  11107. },
  11108. calendar : function () {
  11109. var diff = this.diff(moment().startOf('day'), 'days', true),
  11110. format = diff < -6 ? 'sameElse' :
  11111. diff < -1 ? 'lastWeek' :
  11112. diff < 0 ? 'lastDay' :
  11113. diff < 1 ? 'sameDay' :
  11114. diff < 2 ? 'nextDay' :
  11115. diff < 7 ? 'nextWeek' : 'sameElse';
  11116. return this.format(this.lang().calendar(format, this));
  11117. },
  11118. isLeapYear : function () {
  11119. var year = this.year();
  11120. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  11121. },
  11122. isDST : function () {
  11123. return (this.zone() < moment([this.year()]).zone() ||
  11124. this.zone() < moment([this.year(), 5]).zone());
  11125. },
  11126. day : function (input) {
  11127. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  11128. return input == null ? day :
  11129. this.add({ d : input - day });
  11130. },
  11131. startOf: function (units) {
  11132. units = units.replace(/s$/, '');
  11133. // the following switch intentionally omits break keywords
  11134. // to utilize falling through the cases.
  11135. switch (units) {
  11136. case 'year':
  11137. this.month(0);
  11138. /* falls through */
  11139. case 'month':
  11140. this.date(1);
  11141. /* falls through */
  11142. case 'week':
  11143. case 'day':
  11144. this.hours(0);
  11145. /* falls through */
  11146. case 'hour':
  11147. this.minutes(0);
  11148. /* falls through */
  11149. case 'minute':
  11150. this.seconds(0);
  11151. /* falls through */
  11152. case 'second':
  11153. this.milliseconds(0);
  11154. /* falls through */
  11155. }
  11156. // weeks are a special case
  11157. if (units === 'week') {
  11158. this.day(0);
  11159. }
  11160. return this;
  11161. },
  11162. endOf: function (units) {
  11163. return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
  11164. },
  11165. isAfter: function (input, units) {
  11166. units = typeof units !== 'undefined' ? units : 'millisecond';
  11167. return +this.clone().startOf(units) > +moment(input).startOf(units);
  11168. },
  11169. isBefore: function (input, units) {
  11170. units = typeof units !== 'undefined' ? units : 'millisecond';
  11171. return +this.clone().startOf(units) < +moment(input).startOf(units);
  11172. },
  11173. isSame: function (input, units) {
  11174. units = typeof units !== 'undefined' ? units : 'millisecond';
  11175. return +this.clone().startOf(units) === +moment(input).startOf(units);
  11176. },
  11177. zone : function () {
  11178. return this._isUTC ? 0 : this._d.getTimezoneOffset();
  11179. },
  11180. daysInMonth : function () {
  11181. return moment.utc([this.year(), this.month() + 1, 0]).date();
  11182. },
  11183. dayOfYear : function (input) {
  11184. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  11185. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  11186. },
  11187. isoWeek : function (input) {
  11188. var week = weekOfYear(this, 1, 4);
  11189. return input == null ? week : this.add("d", (input - week) * 7);
  11190. },
  11191. week : function (input) {
  11192. var week = this.lang().week(this);
  11193. return input == null ? week : this.add("d", (input - week) * 7);
  11194. },
  11195. // If passed a language key, it will set the language for this
  11196. // instance. Otherwise, it will return the language configuration
  11197. // variables for this instance.
  11198. lang : function (key) {
  11199. if (key === undefined) {
  11200. return this._lang;
  11201. } else {
  11202. this._lang = getLangDefinition(key);
  11203. return this;
  11204. }
  11205. }
  11206. };
  11207. // helper for adding shortcuts
  11208. function makeGetterAndSetter(name, key) {
  11209. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  11210. var utc = this._isUTC ? 'UTC' : '';
  11211. if (input != null) {
  11212. this._d['set' + utc + key](input);
  11213. return this;
  11214. } else {
  11215. return this._d['get' + utc + key]();
  11216. }
  11217. };
  11218. }
  11219. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  11220. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  11221. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  11222. }
  11223. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  11224. makeGetterAndSetter('year', 'FullYear');
  11225. // add plural methods
  11226. moment.fn.days = moment.fn.day;
  11227. moment.fn.weeks = moment.fn.week;
  11228. moment.fn.isoWeeks = moment.fn.isoWeek;
  11229. /************************************
  11230. Duration Prototype
  11231. ************************************/
  11232. moment.duration.fn = Duration.prototype = {
  11233. weeks : function () {
  11234. return absRound(this.days() / 7);
  11235. },
  11236. valueOf : function () {
  11237. return this._milliseconds +
  11238. this._days * 864e5 +
  11239. this._months * 2592e6;
  11240. },
  11241. humanize : function (withSuffix) {
  11242. var difference = +this,
  11243. output = relativeTime(difference, !withSuffix, this.lang());
  11244. if (withSuffix) {
  11245. output = this.lang().pastFuture(difference, output);
  11246. }
  11247. return this.lang().postformat(output);
  11248. },
  11249. lang : moment.fn.lang
  11250. };
  11251. function makeDurationGetter(name) {
  11252. moment.duration.fn[name] = function () {
  11253. return this._data[name];
  11254. };
  11255. }
  11256. function makeDurationAsGetter(name, factor) {
  11257. moment.duration.fn['as' + name] = function () {
  11258. return +this / factor;
  11259. };
  11260. }
  11261. for (i in unitMillisecondFactors) {
  11262. if (unitMillisecondFactors.hasOwnProperty(i)) {
  11263. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  11264. makeDurationGetter(i.toLowerCase());
  11265. }
  11266. }
  11267. makeDurationAsGetter('Weeks', 6048e5);
  11268. /************************************
  11269. Default Lang
  11270. ************************************/
  11271. // Set default language, other languages will inherit from English.
  11272. moment.lang('en', {
  11273. ordinal : function (number) {
  11274. var b = number % 10,
  11275. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  11276. (b === 1) ? 'st' :
  11277. (b === 2) ? 'nd' :
  11278. (b === 3) ? 'rd' : 'th';
  11279. return number + output;
  11280. }
  11281. });
  11282. /************************************
  11283. Exposing Moment
  11284. ************************************/
  11285. // CommonJS module is defined
  11286. if (hasModule) {
  11287. module.exports = moment;
  11288. }
  11289. /*global ender:false */
  11290. if (typeof ender === 'undefined') {
  11291. // here, `this` means `window` in the browser, or `global` on the server
  11292. // add `moment` as a global object via a string identifier,
  11293. // for Closure Compiler "advanced" mode
  11294. this['moment'] = moment;
  11295. }
  11296. /*global define:false */
  11297. if (typeof define === "function" && define.amd) {
  11298. define("moment", [], function () {
  11299. return moment;
  11300. });
  11301. }
  11302. }).call(this);
  11303. })()
  11304. },{}]},{},[1])(1)
  11305. });
  11306. ;