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.

23898 lines
707 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version @@version
  8. * @date @@date
  9. *
  10. * @license
  11. * Copyright (C) 2011-2014 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("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.vis=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  26. /**
  27. * vis.js module imports
  28. */
  29. // Try to load dependencies from the global window object.
  30. // If not available there, load via require.
  31. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  32. var Emitter = require('emitter-component');
  33. var Hammer;
  34. if (typeof window !== 'undefined') {
  35. // load hammer.js only when running in a browser (where window is available)
  36. Hammer = window['Hammer'] || require('hammerjs');
  37. }
  38. else {
  39. Hammer = function () {
  40. throw Error('hammer.js is only available in a browser, not in node.js.');
  41. }
  42. }
  43. var mousetrap;
  44. if (typeof window !== 'undefined') {
  45. // load mousetrap.js only when running in a browser (where window is available)
  46. mousetrap = window['mousetrap'] || require('mousetrap');
  47. }
  48. else {
  49. mousetrap = function () {
  50. throw Error('mouseTrap is only available in a browser, not in node.js.');
  51. }
  52. }
  53. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  54. // it here in that case.
  55. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  56. if(!Array.prototype.indexOf) {
  57. Array.prototype.indexOf = function(obj){
  58. for(var i = 0; i < this.length; i++){
  59. if(this[i] == obj){
  60. return i;
  61. }
  62. }
  63. return -1;
  64. };
  65. try {
  66. console.log("Warning: Ancient browser detected. Please update your browser");
  67. }
  68. catch (err) {
  69. }
  70. }
  71. // Internet Explorer 8 and older does not support Array.forEach, so we define
  72. // it here in that case.
  73. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  74. if (!Array.prototype.forEach) {
  75. Array.prototype.forEach = function(fn, scope) {
  76. for(var i = 0, len = this.length; i < len; ++i) {
  77. fn.call(scope || this, this[i], i, this);
  78. }
  79. }
  80. }
  81. // Internet Explorer 8 and older does not support Array.map, so we define it
  82. // here in that case.
  83. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  84. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  85. // Reference: http://es5.github.com/#x15.4.4.19
  86. if (!Array.prototype.map) {
  87. Array.prototype.map = function(callback, thisArg) {
  88. var T, A, k;
  89. if (this == null) {
  90. throw new TypeError(" this is null or not defined");
  91. }
  92. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  93. var O = Object(this);
  94. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  95. // 3. Let len be ToUint32(lenValue).
  96. var len = O.length >>> 0;
  97. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  98. // See: http://es5.github.com/#x9.11
  99. if (typeof callback !== "function") {
  100. throw new TypeError(callback + " is not a function");
  101. }
  102. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  103. if (thisArg) {
  104. T = thisArg;
  105. }
  106. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  107. // the standard built-in constructor with that name and len is the value of len.
  108. A = new Array(len);
  109. // 7. Let k be 0
  110. k = 0;
  111. // 8. Repeat, while k < len
  112. while(k < len) {
  113. var kValue, mappedValue;
  114. // a. Let Pk be ToString(k).
  115. // This is implicit for LHS operands of the in operator
  116. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  117. // This step can be combined with c
  118. // c. If kPresent is true, then
  119. if (k in O) {
  120. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  121. kValue = O[ k ];
  122. // ii. Let mappedValue be the result of calling the Call internal method of callback
  123. // with T as the this value and argument list containing kValue, k, and O.
  124. mappedValue = callback.call(T, kValue, k, O);
  125. // iii. Call the DefineOwnProperty internal method of A with arguments
  126. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  127. // and false.
  128. // In browsers that support Object.defineProperty, use the following:
  129. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  130. // For best browser support, use the following:
  131. A[ k ] = mappedValue;
  132. }
  133. // d. Increase k by 1.
  134. k++;
  135. }
  136. // 9. return A
  137. return A;
  138. };
  139. }
  140. // Internet Explorer 8 and older does not support Array.filter, so we define it
  141. // here in that case.
  142. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  143. if (!Array.prototype.filter) {
  144. Array.prototype.filter = function(fun /*, thisp */) {
  145. "use strict";
  146. if (this == null) {
  147. throw new TypeError();
  148. }
  149. var t = Object(this);
  150. var len = t.length >>> 0;
  151. if (typeof fun != "function") {
  152. throw new TypeError();
  153. }
  154. var res = [];
  155. var thisp = arguments[1];
  156. for (var i = 0; i < len; i++) {
  157. if (i in t) {
  158. var val = t[i]; // in case fun mutates this
  159. if (fun.call(thisp, val, i, t))
  160. res.push(val);
  161. }
  162. }
  163. return res;
  164. };
  165. }
  166. // Internet Explorer 8 and older does not support Object.keys, so we define it
  167. // here in that case.
  168. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  169. if (!Object.keys) {
  170. Object.keys = (function () {
  171. var hasOwnProperty = Object.prototype.hasOwnProperty,
  172. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  173. dontEnums = [
  174. 'toString',
  175. 'toLocaleString',
  176. 'valueOf',
  177. 'hasOwnProperty',
  178. 'isPrototypeOf',
  179. 'propertyIsEnumerable',
  180. 'constructor'
  181. ],
  182. dontEnumsLength = dontEnums.length;
  183. return function (obj) {
  184. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  185. throw new TypeError('Object.keys called on non-object');
  186. }
  187. var result = [];
  188. for (var prop in obj) {
  189. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  190. }
  191. if (hasDontEnumBug) {
  192. for (var i=0; i < dontEnumsLength; i++) {
  193. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  194. }
  195. }
  196. return result;
  197. }
  198. })()
  199. }
  200. // Internet Explorer 8 and older does not support Array.isArray,
  201. // so we define it here in that case.
  202. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  203. if(!Array.isArray) {
  204. Array.isArray = function (vArg) {
  205. return Object.prototype.toString.call(vArg) === "[object Array]";
  206. };
  207. }
  208. // Internet Explorer 8 and older does not support Function.bind,
  209. // so we define it here in that case.
  210. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  211. if (!Function.prototype.bind) {
  212. Function.prototype.bind = function (oThis) {
  213. if (typeof this !== "function") {
  214. // closest thing possible to the ECMAScript 5 internal IsCallable function
  215. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  216. }
  217. var aArgs = Array.prototype.slice.call(arguments, 1),
  218. fToBind = this,
  219. fNOP = function () {},
  220. fBound = function () {
  221. return fToBind.apply(this instanceof fNOP && oThis
  222. ? this
  223. : oThis,
  224. aArgs.concat(Array.prototype.slice.call(arguments)));
  225. };
  226. fNOP.prototype = this.prototype;
  227. fBound.prototype = new fNOP();
  228. return fBound;
  229. };
  230. }
  231. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  232. if (!Object.create) {
  233. Object.create = function (o) {
  234. if (arguments.length > 1) {
  235. throw new Error('Object.create implementation only accepts the first parameter.');
  236. }
  237. function F() {}
  238. F.prototype = o;
  239. return new F();
  240. };
  241. }
  242. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  243. if (!Function.prototype.bind) {
  244. Function.prototype.bind = function (oThis) {
  245. if (typeof this !== "function") {
  246. // closest thing possible to the ECMAScript 5 internal IsCallable function
  247. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  248. }
  249. var aArgs = Array.prototype.slice.call(arguments, 1),
  250. fToBind = this,
  251. fNOP = function () {},
  252. fBound = function () {
  253. return fToBind.apply(this instanceof fNOP && oThis
  254. ? this
  255. : oThis,
  256. aArgs.concat(Array.prototype.slice.call(arguments)));
  257. };
  258. fNOP.prototype = this.prototype;
  259. fBound.prototype = new fNOP();
  260. return fBound;
  261. };
  262. }
  263. /**
  264. * utility functions
  265. */
  266. var util = {};
  267. /**
  268. * Test whether given object is a number
  269. * @param {*} object
  270. * @return {Boolean} isNumber
  271. */
  272. util.isNumber = function isNumber(object) {
  273. return (object instanceof Number || typeof object == 'number');
  274. };
  275. /**
  276. * Test whether given object is a string
  277. * @param {*} object
  278. * @return {Boolean} isString
  279. */
  280. util.isString = function isString(object) {
  281. return (object instanceof String || typeof object == 'string');
  282. };
  283. /**
  284. * Test whether given object is a Date, or a String containing a Date
  285. * @param {Date | String} object
  286. * @return {Boolean} isDate
  287. */
  288. util.isDate = function isDate(object) {
  289. if (object instanceof Date) {
  290. return true;
  291. }
  292. else if (util.isString(object)) {
  293. // test whether this string contains a date
  294. var match = ASPDateRegex.exec(object);
  295. if (match) {
  296. return true;
  297. }
  298. else if (!isNaN(Date.parse(object))) {
  299. return true;
  300. }
  301. }
  302. return false;
  303. };
  304. /**
  305. * Test whether given object is an instance of google.visualization.DataTable
  306. * @param {*} object
  307. * @return {Boolean} isDataTable
  308. */
  309. util.isDataTable = function isDataTable(object) {
  310. return (typeof (google) !== 'undefined') &&
  311. (google.visualization) &&
  312. (google.visualization.DataTable) &&
  313. (object instanceof google.visualization.DataTable);
  314. };
  315. /**
  316. * Create a semi UUID
  317. * source: http://stackoverflow.com/a/105074/1262753
  318. * @return {String} uuid
  319. */
  320. util.randomUUID = function randomUUID () {
  321. var S4 = function () {
  322. return Math.floor(
  323. Math.random() * 0x10000 /* 65536 */
  324. ).toString(16);
  325. };
  326. return (
  327. S4() + S4() + '-' +
  328. S4() + '-' +
  329. S4() + '-' +
  330. S4() + '-' +
  331. S4() + S4() + S4()
  332. );
  333. };
  334. /**
  335. * Extend object a with the properties of object b or a series of objects
  336. * Only properties with defined values are copied
  337. * @param {Object} a
  338. * @param {... Object} b
  339. * @return {Object} a
  340. */
  341. util.extend = function (a, b) {
  342. for (var i = 1, len = arguments.length; i < len; i++) {
  343. var other = arguments[i];
  344. for (var prop in other) {
  345. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  346. a[prop] = other[prop];
  347. }
  348. }
  349. }
  350. return a;
  351. };
  352. /**
  353. * Test whether all elements in two arrays are equal.
  354. * @param {Array} a
  355. * @param {Array} b
  356. * @return {boolean} Returns true if both arrays have the same length and same
  357. * elements.
  358. */
  359. util.equalArray = function (a, b) {
  360. if (a.length != b.length) return false;
  361. for (var i = 0, len = a.length; i < len; i++) {
  362. if (a[i] != b[i]) return false;
  363. }
  364. return true;
  365. };
  366. /**
  367. * Convert an object to another type
  368. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  369. * @param {String | undefined} type Name of the type. Available types:
  370. * 'Boolean', 'Number', 'String',
  371. * 'Date', 'Moment', ISODate', 'ASPDate'.
  372. * @return {*} object
  373. * @throws Error
  374. */
  375. util.convert = function convert(object, type) {
  376. var match;
  377. if (object === undefined) {
  378. return undefined;
  379. }
  380. if (object === null) {
  381. return null;
  382. }
  383. if (!type) {
  384. return object;
  385. }
  386. if (!(typeof type === 'string') && !(type instanceof String)) {
  387. throw new Error('Type must be a string');
  388. }
  389. //noinspection FallthroughInSwitchStatementJS
  390. switch (type) {
  391. case 'boolean':
  392. case 'Boolean':
  393. return Boolean(object);
  394. case 'number':
  395. case 'Number':
  396. return Number(object.valueOf());
  397. case 'string':
  398. case 'String':
  399. return String(object);
  400. case 'Date':
  401. if (util.isNumber(object)) {
  402. return new Date(object);
  403. }
  404. if (object instanceof Date) {
  405. return new Date(object.valueOf());
  406. }
  407. else if (moment.isMoment(object)) {
  408. return new Date(object.valueOf());
  409. }
  410. if (util.isString(object)) {
  411. match = ASPDateRegex.exec(object);
  412. if (match) {
  413. // object is an ASP date
  414. return new Date(Number(match[1])); // parse number
  415. }
  416. else {
  417. return moment(object).toDate(); // parse string
  418. }
  419. }
  420. else {
  421. throw new Error(
  422. 'Cannot convert object of type ' + util.getType(object) +
  423. ' to type Date');
  424. }
  425. case 'Moment':
  426. if (util.isNumber(object)) {
  427. return moment(object);
  428. }
  429. if (object instanceof Date) {
  430. return moment(object.valueOf());
  431. }
  432. else if (moment.isMoment(object)) {
  433. return moment(object);
  434. }
  435. if (util.isString(object)) {
  436. match = ASPDateRegex.exec(object);
  437. if (match) {
  438. // object is an ASP date
  439. return moment(Number(match[1])); // parse number
  440. }
  441. else {
  442. return moment(object); // parse string
  443. }
  444. }
  445. else {
  446. throw new Error(
  447. 'Cannot convert object of type ' + util.getType(object) +
  448. ' to type Date');
  449. }
  450. case 'ISODate':
  451. if (util.isNumber(object)) {
  452. return new Date(object);
  453. }
  454. else if (object instanceof Date) {
  455. return object.toISOString();
  456. }
  457. else if (moment.isMoment(object)) {
  458. return object.toDate().toISOString();
  459. }
  460. else if (util.isString(object)) {
  461. match = ASPDateRegex.exec(object);
  462. if (match) {
  463. // object is an ASP date
  464. return new Date(Number(match[1])).toISOString(); // parse number
  465. }
  466. else {
  467. return new Date(object).toISOString(); // parse string
  468. }
  469. }
  470. else {
  471. throw new Error(
  472. 'Cannot convert object of type ' + util.getType(object) +
  473. ' to type ISODate');
  474. }
  475. case 'ASPDate':
  476. if (util.isNumber(object)) {
  477. return '/Date(' + object + ')/';
  478. }
  479. else if (object instanceof Date) {
  480. return '/Date(' + object.valueOf() + ')/';
  481. }
  482. else if (util.isString(object)) {
  483. match = ASPDateRegex.exec(object);
  484. var value;
  485. if (match) {
  486. // object is an ASP date
  487. value = new Date(Number(match[1])).valueOf(); // parse number
  488. }
  489. else {
  490. value = new Date(object).valueOf(); // parse string
  491. }
  492. return '/Date(' + value + ')/';
  493. }
  494. else {
  495. throw new Error(
  496. 'Cannot convert object of type ' + util.getType(object) +
  497. ' to type ASPDate');
  498. }
  499. default:
  500. throw new Error('Cannot convert object of type ' + util.getType(object) +
  501. ' to type "' + type + '"');
  502. }
  503. };
  504. // parse ASP.Net Date pattern,
  505. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  506. // code from http://momentjs.com/
  507. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  508. /**
  509. * Get the type of an object, for example util.getType([]) returns 'Array'
  510. * @param {*} object
  511. * @return {String} type
  512. */
  513. util.getType = function getType(object) {
  514. var type = typeof object;
  515. if (type == 'object') {
  516. if (object == null) {
  517. return 'null';
  518. }
  519. if (object instanceof Boolean) {
  520. return 'Boolean';
  521. }
  522. if (object instanceof Number) {
  523. return 'Number';
  524. }
  525. if (object instanceof String) {
  526. return 'String';
  527. }
  528. if (object instanceof Array) {
  529. return 'Array';
  530. }
  531. if (object instanceof Date) {
  532. return 'Date';
  533. }
  534. return 'Object';
  535. }
  536. else if (type == 'number') {
  537. return 'Number';
  538. }
  539. else if (type == 'boolean') {
  540. return 'Boolean';
  541. }
  542. else if (type == 'string') {
  543. return 'String';
  544. }
  545. return type;
  546. };
  547. /**
  548. * Retrieve the absolute left value of a DOM element
  549. * @param {Element} elem A dom element, for example a div
  550. * @return {number} left The absolute left position of this element
  551. * in the browser page.
  552. */
  553. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  554. var doc = document.documentElement;
  555. var body = document.body;
  556. var left = elem.offsetLeft;
  557. var e = elem.offsetParent;
  558. while (e != null && e != body && e != doc) {
  559. left += e.offsetLeft;
  560. left -= e.scrollLeft;
  561. e = e.offsetParent;
  562. }
  563. return left;
  564. };
  565. /**
  566. * Retrieve the absolute top value of a DOM element
  567. * @param {Element} elem A dom element, for example a div
  568. * @return {number} top The absolute top position of this element
  569. * in the browser page.
  570. */
  571. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  572. var doc = document.documentElement;
  573. var body = document.body;
  574. var top = elem.offsetTop;
  575. var e = elem.offsetParent;
  576. while (e != null && e != body && e != doc) {
  577. top += e.offsetTop;
  578. top -= e.scrollTop;
  579. e = e.offsetParent;
  580. }
  581. return top;
  582. };
  583. /**
  584. * Get the absolute, vertical mouse position from an event.
  585. * @param {Event} event
  586. * @return {Number} pageY
  587. */
  588. util.getPageY = function getPageY (event) {
  589. if ('pageY' in event) {
  590. return event.pageY;
  591. }
  592. else {
  593. var clientY;
  594. if (('targetTouches' in event) && event.targetTouches.length) {
  595. clientY = event.targetTouches[0].clientY;
  596. }
  597. else {
  598. clientY = event.clientY;
  599. }
  600. var doc = document.documentElement;
  601. var body = document.body;
  602. return clientY +
  603. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  604. ( doc && doc.clientTop || body && body.clientTop || 0 );
  605. }
  606. };
  607. /**
  608. * Get the absolute, horizontal mouse position from an event.
  609. * @param {Event} event
  610. * @return {Number} pageX
  611. */
  612. util.getPageX = function getPageX (event) {
  613. if ('pageY' in event) {
  614. return event.pageX;
  615. }
  616. else {
  617. var clientX;
  618. if (('targetTouches' in event) && event.targetTouches.length) {
  619. clientX = event.targetTouches[0].clientX;
  620. }
  621. else {
  622. clientX = event.clientX;
  623. }
  624. var doc = document.documentElement;
  625. var body = document.body;
  626. return clientX +
  627. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  628. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  629. }
  630. };
  631. /**
  632. * add a className to the given elements style
  633. * @param {Element} elem
  634. * @param {String} className
  635. */
  636. util.addClassName = function addClassName(elem, className) {
  637. var classes = elem.className.split(' ');
  638. if (classes.indexOf(className) == -1) {
  639. classes.push(className); // add the class to the array
  640. elem.className = classes.join(' ');
  641. }
  642. };
  643. /**
  644. * add a className to the given elements style
  645. * @param {Element} elem
  646. * @param {String} className
  647. */
  648. util.removeClassName = function removeClassname(elem, className) {
  649. var classes = elem.className.split(' ');
  650. var index = classes.indexOf(className);
  651. if (index != -1) {
  652. classes.splice(index, 1); // remove the class from the array
  653. elem.className = classes.join(' ');
  654. }
  655. };
  656. /**
  657. * For each method for both arrays and objects.
  658. * In case of an array, the built-in Array.forEach() is applied.
  659. * In case of an Object, the method loops over all properties of the object.
  660. * @param {Object | Array} object An Object or Array
  661. * @param {function} callback Callback method, called for each item in
  662. * the object or array with three parameters:
  663. * callback(value, index, object)
  664. */
  665. util.forEach = function forEach (object, callback) {
  666. var i,
  667. len;
  668. if (object instanceof Array) {
  669. // array
  670. for (i = 0, len = object.length; i < len; i++) {
  671. callback(object[i], i, object);
  672. }
  673. }
  674. else {
  675. // object
  676. for (i in object) {
  677. if (object.hasOwnProperty(i)) {
  678. callback(object[i], i, object);
  679. }
  680. }
  681. }
  682. };
  683. /**
  684. * Convert an object into an array: all objects properties are put into the
  685. * array. The resulting array is unordered.
  686. * @param {Object} object
  687. * @param {Array} array
  688. */
  689. util.toArray = function toArray(object) {
  690. var array = [];
  691. for (var prop in object) {
  692. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  693. }
  694. return array;
  695. }
  696. /**
  697. * Update a property in an object
  698. * @param {Object} object
  699. * @param {String} key
  700. * @param {*} value
  701. * @return {Boolean} changed
  702. */
  703. util.updateProperty = function updateProperty (object, key, value) {
  704. if (object[key] !== value) {
  705. object[key] = value;
  706. return true;
  707. }
  708. else {
  709. return false;
  710. }
  711. };
  712. /**
  713. * Add and event listener. Works for all browsers
  714. * @param {Element} element An html element
  715. * @param {string} action The action, for example "click",
  716. * without the prefix "on"
  717. * @param {function} listener The callback function to be executed
  718. * @param {boolean} [useCapture]
  719. */
  720. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  721. if (element.addEventListener) {
  722. if (useCapture === undefined)
  723. useCapture = false;
  724. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  725. action = "DOMMouseScroll"; // For Firefox
  726. }
  727. element.addEventListener(action, listener, useCapture);
  728. } else {
  729. element.attachEvent("on" + action, listener); // IE browsers
  730. }
  731. };
  732. /**
  733. * Remove an event listener from an element
  734. * @param {Element} element An html dom element
  735. * @param {string} action The name of the event, for example "mousedown"
  736. * @param {function} listener The listener function
  737. * @param {boolean} [useCapture]
  738. */
  739. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  740. if (element.removeEventListener) {
  741. // non-IE browsers
  742. if (useCapture === undefined)
  743. useCapture = false;
  744. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  745. action = "DOMMouseScroll"; // For Firefox
  746. }
  747. element.removeEventListener(action, listener, useCapture);
  748. } else {
  749. // IE browsers
  750. element.detachEvent("on" + action, listener);
  751. }
  752. };
  753. /**
  754. * Get HTML element which is the target of the event
  755. * @param {Event} event
  756. * @return {Element} target element
  757. */
  758. util.getTarget = function getTarget(event) {
  759. // code from http://www.quirksmode.org/js/events_properties.html
  760. if (!event) {
  761. event = window.event;
  762. }
  763. var target;
  764. if (event.target) {
  765. target = event.target;
  766. }
  767. else if (event.srcElement) {
  768. target = event.srcElement;
  769. }
  770. if (target.nodeType != undefined && target.nodeType == 3) {
  771. // defeat Safari bug
  772. target = target.parentNode;
  773. }
  774. return target;
  775. };
  776. /**
  777. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  778. * @param {Element} element
  779. * @param {Event} event
  780. */
  781. util.fakeGesture = function fakeGesture (element, event) {
  782. var eventType = null;
  783. // for hammer.js 1.0.5
  784. var gesture = Hammer.event.collectEventData(this, eventType, event);
  785. // for hammer.js 1.0.6
  786. //var touches = Hammer.event.getTouchList(event, eventType);
  787. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  788. // on IE in standards mode, no touches are recognized by hammer.js,
  789. // resulting in NaN values for center.pageX and center.pageY
  790. if (isNaN(gesture.center.pageX)) {
  791. gesture.center.pageX = event.pageX;
  792. }
  793. if (isNaN(gesture.center.pageY)) {
  794. gesture.center.pageY = event.pageY;
  795. }
  796. return gesture;
  797. };
  798. util.option = {};
  799. /**
  800. * Convert a value into a boolean
  801. * @param {Boolean | function | undefined} value
  802. * @param {Boolean} [defaultValue]
  803. * @returns {Boolean} bool
  804. */
  805. util.option.asBoolean = function (value, defaultValue) {
  806. if (typeof value == 'function') {
  807. value = value();
  808. }
  809. if (value != null) {
  810. return (value != false);
  811. }
  812. return defaultValue || null;
  813. };
  814. /**
  815. * Convert a value into a number
  816. * @param {Boolean | function | undefined} value
  817. * @param {Number} [defaultValue]
  818. * @returns {Number} number
  819. */
  820. util.option.asNumber = function (value, defaultValue) {
  821. if (typeof value == 'function') {
  822. value = value();
  823. }
  824. if (value != null) {
  825. return Number(value) || defaultValue || null;
  826. }
  827. return defaultValue || null;
  828. };
  829. /**
  830. * Convert a value into a string
  831. * @param {String | function | undefined} value
  832. * @param {String} [defaultValue]
  833. * @returns {String} str
  834. */
  835. util.option.asString = function (value, defaultValue) {
  836. if (typeof value == 'function') {
  837. value = value();
  838. }
  839. if (value != null) {
  840. return String(value);
  841. }
  842. return defaultValue || null;
  843. };
  844. /**
  845. * Convert a size or location into a string with pixels or a percentage
  846. * @param {String | Number | function | undefined} value
  847. * @param {String} [defaultValue]
  848. * @returns {String} size
  849. */
  850. util.option.asSize = function (value, defaultValue) {
  851. if (typeof value == 'function') {
  852. value = value();
  853. }
  854. if (util.isString(value)) {
  855. return value;
  856. }
  857. else if (util.isNumber(value)) {
  858. return value + 'px';
  859. }
  860. else {
  861. return defaultValue || null;
  862. }
  863. };
  864. /**
  865. * Convert a value into a DOM element
  866. * @param {HTMLElement | function | undefined} value
  867. * @param {HTMLElement} [defaultValue]
  868. * @returns {HTMLElement | null} dom
  869. */
  870. util.option.asElement = function (value, defaultValue) {
  871. if (typeof value == 'function') {
  872. value = value();
  873. }
  874. return value || defaultValue || null;
  875. };
  876. util.GiveDec = function GiveDec(Hex) {
  877. var Value;
  878. if (Hex == "A")
  879. Value = 10;
  880. else if (Hex == "B")
  881. Value = 11;
  882. else if (Hex == "C")
  883. Value = 12;
  884. else if (Hex == "D")
  885. Value = 13;
  886. else if (Hex == "E")
  887. Value = 14;
  888. else if (Hex == "F")
  889. Value = 15;
  890. else
  891. Value = eval(Hex);
  892. return Value;
  893. };
  894. util.GiveHex = function GiveHex(Dec) {
  895. var Value;
  896. if(Dec == 10)
  897. Value = "A";
  898. else if (Dec == 11)
  899. Value = "B";
  900. else if (Dec == 12)
  901. Value = "C";
  902. else if (Dec == 13)
  903. Value = "D";
  904. else if (Dec == 14)
  905. Value = "E";
  906. else if (Dec == 15)
  907. Value = "F";
  908. else
  909. Value = "" + Dec;
  910. return Value;
  911. };
  912. /**
  913. * Parse a color property into an object with border, background, and
  914. * highlight colors
  915. * @param {Object | String} color
  916. * @return {Object} colorObject
  917. */
  918. util.parseColor = function(color) {
  919. var c;
  920. if (util.isString(color)) {
  921. if (util.isValidHex(color)) {
  922. var hsv = util.hexToHSV(color);
  923. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  924. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  925. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  926. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  927. c = {
  928. background: color,
  929. border:darkerColorHex,
  930. highlight: {
  931. background:lighterColorHex,
  932. border:darkerColorHex
  933. }
  934. };
  935. }
  936. else {
  937. c = {
  938. background:color,
  939. border:color,
  940. highlight: {
  941. background:color,
  942. border:color
  943. }
  944. };
  945. }
  946. }
  947. else {
  948. c = {};
  949. c.background = color.background || 'white';
  950. c.border = color.border || c.background;
  951. if (util.isString(color.highlight)) {
  952. c.highlight = {
  953. border: color.highlight,
  954. background: color.highlight
  955. }
  956. }
  957. else {
  958. c.highlight = {};
  959. c.highlight.background = color.highlight && color.highlight.background || c.background;
  960. c.highlight.border = color.highlight && color.highlight.border || c.border;
  961. }
  962. }
  963. return c;
  964. };
  965. /**
  966. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  967. *
  968. * @param {String} hex
  969. * @returns {{r: *, g: *, b: *}}
  970. */
  971. util.hexToRGB = function hexToRGB(hex) {
  972. hex = hex.replace("#","").toUpperCase();
  973. var a = util.GiveDec(hex.substring(0, 1));
  974. var b = util.GiveDec(hex.substring(1, 2));
  975. var c = util.GiveDec(hex.substring(2, 3));
  976. var d = util.GiveDec(hex.substring(3, 4));
  977. var e = util.GiveDec(hex.substring(4, 5));
  978. var f = util.GiveDec(hex.substring(5, 6));
  979. var r = (a * 16) + b;
  980. var g = (c * 16) + d;
  981. var b = (e * 16) + f;
  982. return {r:r,g:g,b:b};
  983. };
  984. util.RGBToHex = function RGBToHex(red,green,blue) {
  985. var a = util.GiveHex(Math.floor(red / 16));
  986. var b = util.GiveHex(red % 16);
  987. var c = util.GiveHex(Math.floor(green / 16));
  988. var d = util.GiveHex(green % 16);
  989. var e = util.GiveHex(Math.floor(blue / 16));
  990. var f = util.GiveHex(blue % 16);
  991. var hex = a + b + c + d + e + f;
  992. return "#" + hex;
  993. };
  994. /**
  995. * http://www.javascripter.net/faq/rgb2hsv.htm
  996. *
  997. * @param red
  998. * @param green
  999. * @param blue
  1000. * @returns {*}
  1001. * @constructor
  1002. */
  1003. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  1004. red=red/255; green=green/255; blue=blue/255;
  1005. var minRGB = Math.min(red,Math.min(green,blue));
  1006. var maxRGB = Math.max(red,Math.max(green,blue));
  1007. // Black-gray-white
  1008. if (minRGB == maxRGB) {
  1009. return {h:0,s:0,v:minRGB};
  1010. }
  1011. // Colors other than black-gray-white:
  1012. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  1013. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  1014. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  1015. var saturation = (maxRGB - minRGB)/maxRGB;
  1016. var value = maxRGB;
  1017. return {h:hue,s:saturation,v:value};
  1018. };
  1019. /**
  1020. * https://gist.github.com/mjijackson/5311256
  1021. * @param hue
  1022. * @param saturation
  1023. * @param value
  1024. * @returns {{r: number, g: number, b: number}}
  1025. * @constructor
  1026. */
  1027. util.HSVToRGB = function HSVToRGB(h, s, v) {
  1028. var r, g, b;
  1029. var i = Math.floor(h * 6);
  1030. var f = h * 6 - i;
  1031. var p = v * (1 - s);
  1032. var q = v * (1 - f * s);
  1033. var t = v * (1 - (1 - f) * s);
  1034. switch (i % 6) {
  1035. case 0: r = v, g = t, b = p; break;
  1036. case 1: r = q, g = v, b = p; break;
  1037. case 2: r = p, g = v, b = t; break;
  1038. case 3: r = p, g = q, b = v; break;
  1039. case 4: r = t, g = p, b = v; break;
  1040. case 5: r = v, g = p, b = q; break;
  1041. }
  1042. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1043. };
  1044. util.HSVToHex = function HSVToHex(h, s, v) {
  1045. var rgb = util.HSVToRGB(h, s, v);
  1046. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1047. };
  1048. util.hexToHSV = function hexToHSV(hex) {
  1049. var rgb = util.hexToRGB(hex);
  1050. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1051. };
  1052. util.isValidHex = function isValidHex(hex) {
  1053. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1054. return isOk;
  1055. };
  1056. util.copyObject = function copyObject(objectFrom, objectTo) {
  1057. for (var i in objectFrom) {
  1058. if (objectFrom.hasOwnProperty(i)) {
  1059. if (typeof objectFrom[i] == "object") {
  1060. objectTo[i] = {};
  1061. util.copyObject(objectFrom[i], objectTo[i]);
  1062. }
  1063. else {
  1064. objectTo[i] = objectFrom[i];
  1065. }
  1066. }
  1067. }
  1068. };
  1069. /**
  1070. * DataSet
  1071. *
  1072. * Usage:
  1073. * var dataSet = new DataSet({
  1074. * fieldId: '_id',
  1075. * convert: {
  1076. * // ...
  1077. * }
  1078. * });
  1079. *
  1080. * dataSet.add(item);
  1081. * dataSet.add(data);
  1082. * dataSet.update(item);
  1083. * dataSet.update(data);
  1084. * dataSet.remove(id);
  1085. * dataSet.remove(ids);
  1086. * var data = dataSet.get();
  1087. * var data = dataSet.get(id);
  1088. * var data = dataSet.get(ids);
  1089. * var data = dataSet.get(ids, options, data);
  1090. * dataSet.clear();
  1091. *
  1092. * A data set can:
  1093. * - add/remove/update data
  1094. * - gives triggers upon changes in the data
  1095. * - can import/export data in various data formats
  1096. *
  1097. * @param {Array | DataTable} [data] Optional array with initial data
  1098. * @param {Object} [options] Available options:
  1099. * {String} fieldId Field name of the id in the
  1100. * items, 'id' by default.
  1101. * {Object.<String, String} convert
  1102. * A map with field names as key,
  1103. * and the field type as value.
  1104. * @constructor DataSet
  1105. */
  1106. // TODO: add a DataSet constructor DataSet(data, options)
  1107. function DataSet (data, options) {
  1108. this.id = util.randomUUID();
  1109. // correctly read optional arguments
  1110. if (data && !Array.isArray(data) && !util.isDataTable(data)) {
  1111. options = data;
  1112. data = null;
  1113. }
  1114. this.options = options || {};
  1115. this.data = {}; // map with data indexed by id
  1116. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1117. this.convert = {}; // field types by field name
  1118. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1119. if (this.options.convert) {
  1120. for (var field in this.options.convert) {
  1121. if (this.options.convert.hasOwnProperty(field)) {
  1122. var value = this.options.convert[field];
  1123. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1124. this.convert[field] = 'Date';
  1125. }
  1126. else {
  1127. this.convert[field] = value;
  1128. }
  1129. }
  1130. }
  1131. }
  1132. this.subscribers = {}; // event subscribers
  1133. this.internalIds = {}; // internally generated id's
  1134. // add initial data when provided
  1135. if (data) {
  1136. this.add(data);
  1137. }
  1138. }
  1139. /**
  1140. * Subscribe to an event, add an event listener
  1141. * @param {String} event Event name. Available events: 'put', 'update',
  1142. * 'remove'
  1143. * @param {function} callback Callback method. Called with three parameters:
  1144. * {String} event
  1145. * {Object | null} params
  1146. * {String | Number} senderId
  1147. */
  1148. DataSet.prototype.on = function on (event, callback) {
  1149. var subscribers = this.subscribers[event];
  1150. if (!subscribers) {
  1151. subscribers = [];
  1152. this.subscribers[event] = subscribers;
  1153. }
  1154. subscribers.push({
  1155. callback: callback
  1156. });
  1157. };
  1158. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1159. DataSet.prototype.subscribe = DataSet.prototype.on;
  1160. /**
  1161. * Unsubscribe from an event, remove an event listener
  1162. * @param {String} event
  1163. * @param {function} callback
  1164. */
  1165. DataSet.prototype.off = function off(event, callback) {
  1166. var subscribers = this.subscribers[event];
  1167. if (subscribers) {
  1168. this.subscribers[event] = subscribers.filter(function (listener) {
  1169. return (listener.callback != callback);
  1170. });
  1171. }
  1172. };
  1173. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1174. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1175. /**
  1176. * Trigger an event
  1177. * @param {String} event
  1178. * @param {Object | null} params
  1179. * @param {String} [senderId] Optional id of the sender.
  1180. * @private
  1181. */
  1182. DataSet.prototype._trigger = function (event, params, senderId) {
  1183. if (event == '*') {
  1184. throw new Error('Cannot trigger event *');
  1185. }
  1186. var subscribers = [];
  1187. if (event in this.subscribers) {
  1188. subscribers = subscribers.concat(this.subscribers[event]);
  1189. }
  1190. if ('*' in this.subscribers) {
  1191. subscribers = subscribers.concat(this.subscribers['*']);
  1192. }
  1193. for (var i = 0; i < subscribers.length; i++) {
  1194. var subscriber = subscribers[i];
  1195. if (subscriber.callback) {
  1196. subscriber.callback(event, params, senderId || null);
  1197. }
  1198. }
  1199. };
  1200. /**
  1201. * Add data.
  1202. * Adding an item will fail when there already is an item with the same id.
  1203. * @param {Object | Array | DataTable} data
  1204. * @param {String} [senderId] Optional sender id
  1205. * @return {Array} addedIds Array with the ids of the added items
  1206. */
  1207. DataSet.prototype.add = function (data, senderId) {
  1208. var addedIds = [],
  1209. id,
  1210. me = this;
  1211. if (data instanceof Array) {
  1212. // Array
  1213. for (var i = 0, len = data.length; i < len; i++) {
  1214. id = me._addItem(data[i]);
  1215. addedIds.push(id);
  1216. }
  1217. }
  1218. else if (util.isDataTable(data)) {
  1219. // Google DataTable
  1220. var columns = this._getColumnNames(data);
  1221. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1222. var item = {};
  1223. for (var col = 0, cols = columns.length; col < cols; col++) {
  1224. var field = columns[col];
  1225. item[field] = data.getValue(row, col);
  1226. }
  1227. id = me._addItem(item);
  1228. addedIds.push(id);
  1229. }
  1230. }
  1231. else if (data instanceof Object) {
  1232. // Single item
  1233. id = me._addItem(data);
  1234. addedIds.push(id);
  1235. }
  1236. else {
  1237. throw new Error('Unknown dataType');
  1238. }
  1239. if (addedIds.length) {
  1240. this._trigger('add', {items: addedIds}, senderId);
  1241. }
  1242. return addedIds;
  1243. };
  1244. /**
  1245. * Update existing items. When an item does not exist, it will be created
  1246. * @param {Object | Array | DataTable} data
  1247. * @param {String} [senderId] Optional sender id
  1248. * @return {Array} updatedIds The ids of the added or updated items
  1249. */
  1250. DataSet.prototype.update = function (data, senderId) {
  1251. var addedIds = [],
  1252. updatedIds = [],
  1253. me = this,
  1254. fieldId = me.fieldId;
  1255. var addOrUpdate = function (item) {
  1256. var id = item[fieldId];
  1257. if (me.data[id]) {
  1258. // update item
  1259. id = me._updateItem(item);
  1260. updatedIds.push(id);
  1261. }
  1262. else {
  1263. // add new item
  1264. id = me._addItem(item);
  1265. addedIds.push(id);
  1266. }
  1267. };
  1268. if (data instanceof Array) {
  1269. // Array
  1270. for (var i = 0, len = data.length; i < len; i++) {
  1271. addOrUpdate(data[i]);
  1272. }
  1273. }
  1274. else if (util.isDataTable(data)) {
  1275. // Google DataTable
  1276. var columns = this._getColumnNames(data);
  1277. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1278. var item = {};
  1279. for (var col = 0, cols = columns.length; col < cols; col++) {
  1280. var field = columns[col];
  1281. item[field] = data.getValue(row, col);
  1282. }
  1283. addOrUpdate(item);
  1284. }
  1285. }
  1286. else if (data instanceof Object) {
  1287. // Single item
  1288. addOrUpdate(data);
  1289. }
  1290. else {
  1291. throw new Error('Unknown dataType');
  1292. }
  1293. if (addedIds.length) {
  1294. this._trigger('add', {items: addedIds}, senderId);
  1295. }
  1296. if (updatedIds.length) {
  1297. this._trigger('update', {items: updatedIds}, senderId);
  1298. }
  1299. return addedIds.concat(updatedIds);
  1300. };
  1301. /**
  1302. * Get a data item or multiple items.
  1303. *
  1304. * Usage:
  1305. *
  1306. * get()
  1307. * get(options: Object)
  1308. * get(options: Object, data: Array | DataTable)
  1309. *
  1310. * get(id: Number | String)
  1311. * get(id: Number | String, options: Object)
  1312. * get(id: Number | String, options: Object, data: Array | DataTable)
  1313. *
  1314. * get(ids: Number[] | String[])
  1315. * get(ids: Number[] | String[], options: Object)
  1316. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1317. *
  1318. * Where:
  1319. *
  1320. * {Number | String} id The id of an item
  1321. * {Number[] | String{}} ids An array with ids of items
  1322. * {Object} options An Object with options. Available options:
  1323. * {String} [type] Type of data to be returned. Can
  1324. * be 'DataTable' or 'Array' (default)
  1325. * {Object.<String, String>} [convert]
  1326. * {String[]} [fields] field names to be returned
  1327. * {function} [filter] filter items
  1328. * {String | function} [order] Order the items by
  1329. * a field name or custom sort function.
  1330. * {Array | DataTable} [data] If provided, items will be appended to this
  1331. * array or table. Required in case of Google
  1332. * DataTable.
  1333. *
  1334. * @throws Error
  1335. */
  1336. DataSet.prototype.get = function (args) {
  1337. var me = this;
  1338. var globalShowInternalIds = this.showInternalIds;
  1339. // parse the arguments
  1340. var id, ids, options, data;
  1341. var firstType = util.getType(arguments[0]);
  1342. if (firstType == 'String' || firstType == 'Number') {
  1343. // get(id [, options] [, data])
  1344. id = arguments[0];
  1345. options = arguments[1];
  1346. data = arguments[2];
  1347. }
  1348. else if (firstType == 'Array') {
  1349. // get(ids [, options] [, data])
  1350. ids = arguments[0];
  1351. options = arguments[1];
  1352. data = arguments[2];
  1353. }
  1354. else {
  1355. // get([, options] [, data])
  1356. options = arguments[0];
  1357. data = arguments[1];
  1358. }
  1359. // determine the return type
  1360. var type;
  1361. if (options && options.type) {
  1362. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1363. if (data && (type != util.getType(data))) {
  1364. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1365. 'does not correspond with specified options.type (' + options.type + ')');
  1366. }
  1367. if (type == 'DataTable' && !util.isDataTable(data)) {
  1368. throw new Error('Parameter "data" must be a DataTable ' +
  1369. 'when options.type is "DataTable"');
  1370. }
  1371. }
  1372. else if (data) {
  1373. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1374. }
  1375. else {
  1376. type = 'Array';
  1377. }
  1378. // we allow the setting of this value for a single get request.
  1379. if (options != undefined) {
  1380. if (options.showInternalIds != undefined) {
  1381. this.showInternalIds = options.showInternalIds;
  1382. }
  1383. }
  1384. // build options
  1385. var convert = options && options.convert || this.options.convert;
  1386. var filter = options && options.filter;
  1387. var items = [], item, itemId, i, len;
  1388. // convert items
  1389. if (id != undefined) {
  1390. // return a single item
  1391. item = me._getItem(id, convert);
  1392. if (filter && !filter(item)) {
  1393. item = null;
  1394. }
  1395. }
  1396. else if (ids != undefined) {
  1397. // return a subset of items
  1398. for (i = 0, len = ids.length; i < len; i++) {
  1399. item = me._getItem(ids[i], convert);
  1400. if (!filter || filter(item)) {
  1401. items.push(item);
  1402. }
  1403. }
  1404. }
  1405. else {
  1406. // return all items
  1407. for (itemId in this.data) {
  1408. if (this.data.hasOwnProperty(itemId)) {
  1409. item = me._getItem(itemId, convert);
  1410. if (!filter || filter(item)) {
  1411. items.push(item);
  1412. }
  1413. }
  1414. }
  1415. }
  1416. // restore the global value of showInternalIds
  1417. this.showInternalIds = globalShowInternalIds;
  1418. // order the results
  1419. if (options && options.order && id == undefined) {
  1420. this._sort(items, options.order);
  1421. }
  1422. // filter fields of the items
  1423. if (options && options.fields) {
  1424. var fields = options.fields;
  1425. if (id != undefined) {
  1426. item = this._filterFields(item, fields);
  1427. }
  1428. else {
  1429. for (i = 0, len = items.length; i < len; i++) {
  1430. items[i] = this._filterFields(items[i], fields);
  1431. }
  1432. }
  1433. }
  1434. // return the results
  1435. if (type == 'DataTable') {
  1436. var columns = this._getColumnNames(data);
  1437. if (id != undefined) {
  1438. // append a single item to the data table
  1439. me._appendRow(data, columns, item);
  1440. }
  1441. else {
  1442. // copy the items to the provided data table
  1443. for (i = 0, len = items.length; i < len; i++) {
  1444. me._appendRow(data, columns, items[i]);
  1445. }
  1446. }
  1447. return data;
  1448. }
  1449. else {
  1450. // return an array
  1451. if (id != undefined) {
  1452. // a single item
  1453. return item;
  1454. }
  1455. else {
  1456. // multiple items
  1457. if (data) {
  1458. // copy the items to the provided array
  1459. for (i = 0, len = items.length; i < len; i++) {
  1460. data.push(items[i]);
  1461. }
  1462. return data;
  1463. }
  1464. else {
  1465. // just return our array
  1466. return items;
  1467. }
  1468. }
  1469. }
  1470. };
  1471. /**
  1472. * Get ids of all items or from a filtered set of items.
  1473. * @param {Object} [options] An Object with options. Available options:
  1474. * {function} [filter] filter items
  1475. * {String | function} [order] Order the items by
  1476. * a field name or custom sort function.
  1477. * @return {Array} ids
  1478. */
  1479. DataSet.prototype.getIds = function (options) {
  1480. var data = this.data,
  1481. filter = options && options.filter,
  1482. order = options && options.order,
  1483. convert = options && options.convert || this.options.convert,
  1484. i,
  1485. len,
  1486. id,
  1487. item,
  1488. items,
  1489. ids = [];
  1490. if (filter) {
  1491. // get filtered items
  1492. if (order) {
  1493. // create ordered list
  1494. items = [];
  1495. for (id in data) {
  1496. if (data.hasOwnProperty(id)) {
  1497. item = this._getItem(id, convert);
  1498. if (filter(item)) {
  1499. items.push(item);
  1500. }
  1501. }
  1502. }
  1503. this._sort(items, order);
  1504. for (i = 0, len = items.length; i < len; i++) {
  1505. ids[i] = items[i][this.fieldId];
  1506. }
  1507. }
  1508. else {
  1509. // create unordered list
  1510. for (id in data) {
  1511. if (data.hasOwnProperty(id)) {
  1512. item = this._getItem(id, convert);
  1513. if (filter(item)) {
  1514. ids.push(item[this.fieldId]);
  1515. }
  1516. }
  1517. }
  1518. }
  1519. }
  1520. else {
  1521. // get all items
  1522. if (order) {
  1523. // create an ordered list
  1524. items = [];
  1525. for (id in data) {
  1526. if (data.hasOwnProperty(id)) {
  1527. items.push(data[id]);
  1528. }
  1529. }
  1530. this._sort(items, order);
  1531. for (i = 0, len = items.length; i < len; i++) {
  1532. ids[i] = items[i][this.fieldId];
  1533. }
  1534. }
  1535. else {
  1536. // create unordered list
  1537. for (id in data) {
  1538. if (data.hasOwnProperty(id)) {
  1539. item = data[id];
  1540. ids.push(item[this.fieldId]);
  1541. }
  1542. }
  1543. }
  1544. }
  1545. return ids;
  1546. };
  1547. /**
  1548. * Execute a callback function for every item in the dataset.
  1549. * @param {function} callback
  1550. * @param {Object} [options] Available options:
  1551. * {Object.<String, String>} [convert]
  1552. * {String[]} [fields] filter fields
  1553. * {function} [filter] filter items
  1554. * {String | function} [order] Order the items by
  1555. * a field name or custom sort function.
  1556. */
  1557. DataSet.prototype.forEach = function (callback, options) {
  1558. var filter = options && options.filter,
  1559. convert = options && options.convert || this.options.convert,
  1560. data = this.data,
  1561. item,
  1562. id;
  1563. if (options && options.order) {
  1564. // execute forEach on ordered list
  1565. var items = this.get(options);
  1566. for (var i = 0, len = items.length; i < len; i++) {
  1567. item = items[i];
  1568. id = item[this.fieldId];
  1569. callback(item, id);
  1570. }
  1571. }
  1572. else {
  1573. // unordered
  1574. for (id in data) {
  1575. if (data.hasOwnProperty(id)) {
  1576. item = this._getItem(id, convert);
  1577. if (!filter || filter(item)) {
  1578. callback(item, id);
  1579. }
  1580. }
  1581. }
  1582. }
  1583. };
  1584. /**
  1585. * Map every item in the dataset.
  1586. * @param {function} callback
  1587. * @param {Object} [options] Available options:
  1588. * {Object.<String, String>} [convert]
  1589. * {String[]} [fields] filter fields
  1590. * {function} [filter] filter items
  1591. * {String | function} [order] Order the items by
  1592. * a field name or custom sort function.
  1593. * @return {Object[]} mappedItems
  1594. */
  1595. DataSet.prototype.map = function (callback, options) {
  1596. var filter = options && options.filter,
  1597. convert = options && options.convert || this.options.convert,
  1598. mappedItems = [],
  1599. data = this.data,
  1600. item;
  1601. // convert and filter items
  1602. for (var id in data) {
  1603. if (data.hasOwnProperty(id)) {
  1604. item = this._getItem(id, convert);
  1605. if (!filter || filter(item)) {
  1606. mappedItems.push(callback(item, id));
  1607. }
  1608. }
  1609. }
  1610. // order items
  1611. if (options && options.order) {
  1612. this._sort(mappedItems, options.order);
  1613. }
  1614. return mappedItems;
  1615. };
  1616. /**
  1617. * Filter the fields of an item
  1618. * @param {Object} item
  1619. * @param {String[]} fields Field names
  1620. * @return {Object} filteredItem
  1621. * @private
  1622. */
  1623. DataSet.prototype._filterFields = function (item, fields) {
  1624. var filteredItem = {};
  1625. for (var field in item) {
  1626. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1627. filteredItem[field] = item[field];
  1628. }
  1629. }
  1630. return filteredItem;
  1631. };
  1632. /**
  1633. * Sort the provided array with items
  1634. * @param {Object[]} items
  1635. * @param {String | function} order A field name or custom sort function.
  1636. * @private
  1637. */
  1638. DataSet.prototype._sort = function (items, order) {
  1639. if (util.isString(order)) {
  1640. // order by provided field name
  1641. var name = order; // field name
  1642. items.sort(function (a, b) {
  1643. var av = a[name];
  1644. var bv = b[name];
  1645. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1646. });
  1647. }
  1648. else if (typeof order === 'function') {
  1649. // order by sort function
  1650. items.sort(order);
  1651. }
  1652. // TODO: extend order by an Object {field:String, direction:String}
  1653. // where direction can be 'asc' or 'desc'
  1654. else {
  1655. throw new TypeError('Order must be a function or a string');
  1656. }
  1657. };
  1658. /**
  1659. * Remove an object by pointer or by id
  1660. * @param {String | Number | Object | Array} id Object or id, or an array with
  1661. * objects or ids to be removed
  1662. * @param {String} [senderId] Optional sender id
  1663. * @return {Array} removedIds
  1664. */
  1665. DataSet.prototype.remove = function (id, senderId) {
  1666. var removedIds = [],
  1667. i, len, removedId;
  1668. if (id instanceof Array) {
  1669. for (i = 0, len = id.length; i < len; i++) {
  1670. removedId = this._remove(id[i]);
  1671. if (removedId != null) {
  1672. removedIds.push(removedId);
  1673. }
  1674. }
  1675. }
  1676. else {
  1677. removedId = this._remove(id);
  1678. if (removedId != null) {
  1679. removedIds.push(removedId);
  1680. }
  1681. }
  1682. if (removedIds.length) {
  1683. this._trigger('remove', {items: removedIds}, senderId);
  1684. }
  1685. return removedIds;
  1686. };
  1687. /**
  1688. * Remove an item by its id
  1689. * @param {Number | String | Object} id id or item
  1690. * @returns {Number | String | null} id
  1691. * @private
  1692. */
  1693. DataSet.prototype._remove = function (id) {
  1694. if (util.isNumber(id) || util.isString(id)) {
  1695. if (this.data[id]) {
  1696. delete this.data[id];
  1697. delete this.internalIds[id];
  1698. return id;
  1699. }
  1700. }
  1701. else if (id instanceof Object) {
  1702. var itemId = id[this.fieldId];
  1703. if (itemId && this.data[itemId]) {
  1704. delete this.data[itemId];
  1705. delete this.internalIds[itemId];
  1706. return itemId;
  1707. }
  1708. }
  1709. return null;
  1710. };
  1711. /**
  1712. * Clear the data
  1713. * @param {String} [senderId] Optional sender id
  1714. * @return {Array} removedIds The ids of all removed items
  1715. */
  1716. DataSet.prototype.clear = function (senderId) {
  1717. var ids = Object.keys(this.data);
  1718. this.data = {};
  1719. this.internalIds = {};
  1720. this._trigger('remove', {items: ids}, senderId);
  1721. return ids;
  1722. };
  1723. /**
  1724. * Find the item with maximum value of a specified field
  1725. * @param {String} field
  1726. * @return {Object | null} item Item containing max value, or null if no items
  1727. */
  1728. DataSet.prototype.max = function (field) {
  1729. var data = this.data,
  1730. max = null,
  1731. maxField = null;
  1732. for (var id in data) {
  1733. if (data.hasOwnProperty(id)) {
  1734. var item = data[id];
  1735. var itemField = item[field];
  1736. if (itemField != null && (!max || itemField > maxField)) {
  1737. max = item;
  1738. maxField = itemField;
  1739. }
  1740. }
  1741. }
  1742. return max;
  1743. };
  1744. /**
  1745. * Find the item with minimum value of a specified field
  1746. * @param {String} field
  1747. * @return {Object | null} item Item containing max value, or null if no items
  1748. */
  1749. DataSet.prototype.min = function (field) {
  1750. var data = this.data,
  1751. min = null,
  1752. minField = null;
  1753. for (var id in data) {
  1754. if (data.hasOwnProperty(id)) {
  1755. var item = data[id];
  1756. var itemField = item[field];
  1757. if (itemField != null && (!min || itemField < minField)) {
  1758. min = item;
  1759. minField = itemField;
  1760. }
  1761. }
  1762. }
  1763. return min;
  1764. };
  1765. /**
  1766. * Find all distinct values of a specified field
  1767. * @param {String} field
  1768. * @return {Array} values Array containing all distinct values. If data items
  1769. * do not contain the specified field are ignored.
  1770. * The returned array is unordered.
  1771. */
  1772. DataSet.prototype.distinct = function (field) {
  1773. var data = this.data,
  1774. values = [],
  1775. fieldType = this.options.convert[field],
  1776. count = 0;
  1777. for (var prop in data) {
  1778. if (data.hasOwnProperty(prop)) {
  1779. var item = data[prop];
  1780. var value = util.convert(item[field], fieldType);
  1781. var exists = false;
  1782. for (var i = 0; i < count; i++) {
  1783. if (values[i] == value) {
  1784. exists = true;
  1785. break;
  1786. }
  1787. }
  1788. if (!exists && (value !== undefined)) {
  1789. values[count] = value;
  1790. count++;
  1791. }
  1792. }
  1793. }
  1794. return values;
  1795. };
  1796. /**
  1797. * Add a single item. Will fail when an item with the same id already exists.
  1798. * @param {Object} item
  1799. * @return {String} id
  1800. * @private
  1801. */
  1802. DataSet.prototype._addItem = function (item) {
  1803. var id = item[this.fieldId];
  1804. if (id != undefined) {
  1805. // check whether this id is already taken
  1806. if (this.data[id]) {
  1807. // item already exists
  1808. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1809. }
  1810. }
  1811. else {
  1812. // generate an id
  1813. id = util.randomUUID();
  1814. item[this.fieldId] = id;
  1815. this.internalIds[id] = item;
  1816. }
  1817. var d = {};
  1818. for (var field in item) {
  1819. if (item.hasOwnProperty(field)) {
  1820. var fieldType = this.convert[field]; // type may be undefined
  1821. d[field] = util.convert(item[field], fieldType);
  1822. }
  1823. }
  1824. this.data[id] = d;
  1825. return id;
  1826. };
  1827. /**
  1828. * Get an item. Fields can be converted to a specific type
  1829. * @param {String} id
  1830. * @param {Object.<String, String>} [convert] field types to convert
  1831. * @return {Object | null} item
  1832. * @private
  1833. */
  1834. DataSet.prototype._getItem = function (id, convert) {
  1835. var field, value;
  1836. // get the item from the dataset
  1837. var raw = this.data[id];
  1838. if (!raw) {
  1839. return null;
  1840. }
  1841. // convert the items field types
  1842. var converted = {},
  1843. fieldId = this.fieldId,
  1844. internalIds = this.internalIds;
  1845. if (convert) {
  1846. for (field in raw) {
  1847. if (raw.hasOwnProperty(field)) {
  1848. value = raw[field];
  1849. // output all fields, except internal ids
  1850. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1851. converted[field] = util.convert(value, convert[field]);
  1852. }
  1853. }
  1854. }
  1855. }
  1856. else {
  1857. // no field types specified, no converting needed
  1858. for (field in raw) {
  1859. if (raw.hasOwnProperty(field)) {
  1860. value = raw[field];
  1861. // output all fields, except internal ids
  1862. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1863. converted[field] = value;
  1864. }
  1865. }
  1866. }
  1867. }
  1868. return converted;
  1869. };
  1870. /**
  1871. * Update a single item: merge with existing item.
  1872. * Will fail when the item has no id, or when there does not exist an item
  1873. * with the same id.
  1874. * @param {Object} item
  1875. * @return {String} id
  1876. * @private
  1877. */
  1878. DataSet.prototype._updateItem = function (item) {
  1879. var id = item[this.fieldId];
  1880. if (id == undefined) {
  1881. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1882. }
  1883. var d = this.data[id];
  1884. if (!d) {
  1885. // item doesn't exist
  1886. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1887. }
  1888. // merge with current item
  1889. for (var field in item) {
  1890. if (item.hasOwnProperty(field)) {
  1891. var fieldType = this.convert[field]; // type may be undefined
  1892. d[field] = util.convert(item[field], fieldType);
  1893. }
  1894. }
  1895. return id;
  1896. };
  1897. /**
  1898. * check if an id is an internal or external id
  1899. * @param id
  1900. * @returns {boolean}
  1901. * @private
  1902. */
  1903. DataSet.prototype.isInternalId = function(id) {
  1904. return (id in this.internalIds);
  1905. };
  1906. /**
  1907. * Get an array with the column names of a Google DataTable
  1908. * @param {DataTable} dataTable
  1909. * @return {String[]} columnNames
  1910. * @private
  1911. */
  1912. DataSet.prototype._getColumnNames = function (dataTable) {
  1913. var columns = [];
  1914. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1915. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1916. }
  1917. return columns;
  1918. };
  1919. /**
  1920. * Append an item as a row to the dataTable
  1921. * @param dataTable
  1922. * @param columns
  1923. * @param item
  1924. * @private
  1925. */
  1926. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1927. var row = dataTable.addRow();
  1928. for (var col = 0, cols = columns.length; col < cols; col++) {
  1929. var field = columns[col];
  1930. dataTable.setValue(row, col, item[field]);
  1931. }
  1932. };
  1933. /**
  1934. * DataView
  1935. *
  1936. * a dataview offers a filtered view on a dataset or an other dataview.
  1937. *
  1938. * @param {DataSet | DataView} data
  1939. * @param {Object} [options] Available options: see method get
  1940. *
  1941. * @constructor DataView
  1942. */
  1943. function DataView (data, options) {
  1944. this.id = util.randomUUID();
  1945. this.data = null;
  1946. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1947. this.options = options || {};
  1948. this.fieldId = 'id'; // name of the field containing id
  1949. this.subscribers = {}; // event subscribers
  1950. var me = this;
  1951. this.listener = function () {
  1952. me._onEvent.apply(me, arguments);
  1953. };
  1954. this.setData(data);
  1955. }
  1956. // TODO: implement a function .config() to dynamically update things like configured filter
  1957. // and trigger changes accordingly
  1958. /**
  1959. * Set a data source for the view
  1960. * @param {DataSet | DataView} data
  1961. */
  1962. DataView.prototype.setData = function (data) {
  1963. var ids, dataItems, i, len;
  1964. if (this.data) {
  1965. // unsubscribe from current dataset
  1966. if (this.data.unsubscribe) {
  1967. this.data.unsubscribe('*', this.listener);
  1968. }
  1969. // trigger a remove of all items in memory
  1970. ids = [];
  1971. for (var id in this.ids) {
  1972. if (this.ids.hasOwnProperty(id)) {
  1973. ids.push(id);
  1974. }
  1975. }
  1976. this.ids = {};
  1977. this._trigger('remove', {items: ids});
  1978. }
  1979. this.data = data;
  1980. if (this.data) {
  1981. // update fieldId
  1982. this.fieldId = this.options.fieldId ||
  1983. (this.data && this.data.options && this.data.options.fieldId) ||
  1984. 'id';
  1985. // trigger an add of all added items
  1986. ids = this.data.getIds({filter: this.options && this.options.filter});
  1987. for (i = 0, len = ids.length; i < len; i++) {
  1988. id = ids[i];
  1989. this.ids[id] = true;
  1990. }
  1991. this._trigger('add', {items: ids});
  1992. // subscribe to new dataset
  1993. if (this.data.on) {
  1994. this.data.on('*', this.listener);
  1995. }
  1996. }
  1997. };
  1998. /**
  1999. * Get data from the data view
  2000. *
  2001. * Usage:
  2002. *
  2003. * get()
  2004. * get(options: Object)
  2005. * get(options: Object, data: Array | DataTable)
  2006. *
  2007. * get(id: Number)
  2008. * get(id: Number, options: Object)
  2009. * get(id: Number, options: Object, data: Array | DataTable)
  2010. *
  2011. * get(ids: Number[])
  2012. * get(ids: Number[], options: Object)
  2013. * get(ids: Number[], options: Object, data: Array | DataTable)
  2014. *
  2015. * Where:
  2016. *
  2017. * {Number | String} id The id of an item
  2018. * {Number[] | String{}} ids An array with ids of items
  2019. * {Object} options An Object with options. Available options:
  2020. * {String} [type] Type of data to be returned. Can
  2021. * be 'DataTable' or 'Array' (default)
  2022. * {Object.<String, String>} [convert]
  2023. * {String[]} [fields] field names to be returned
  2024. * {function} [filter] filter items
  2025. * {String | function} [order] Order the items by
  2026. * a field name or custom sort function.
  2027. * {Array | DataTable} [data] If provided, items will be appended to this
  2028. * array or table. Required in case of Google
  2029. * DataTable.
  2030. * @param args
  2031. */
  2032. DataView.prototype.get = function (args) {
  2033. var me = this;
  2034. // parse the arguments
  2035. var ids, options, data;
  2036. var firstType = util.getType(arguments[0]);
  2037. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2038. // get(id(s) [, options] [, data])
  2039. ids = arguments[0]; // can be a single id or an array with ids
  2040. options = arguments[1];
  2041. data = arguments[2];
  2042. }
  2043. else {
  2044. // get([, options] [, data])
  2045. options = arguments[0];
  2046. data = arguments[1];
  2047. }
  2048. // extend the options with the default options and provided options
  2049. var viewOptions = util.extend({}, this.options, options);
  2050. // create a combined filter method when needed
  2051. if (this.options.filter && options && options.filter) {
  2052. viewOptions.filter = function (item) {
  2053. return me.options.filter(item) && options.filter(item);
  2054. }
  2055. }
  2056. // build up the call to the linked data set
  2057. var getArguments = [];
  2058. if (ids != undefined) {
  2059. getArguments.push(ids);
  2060. }
  2061. getArguments.push(viewOptions);
  2062. getArguments.push(data);
  2063. return this.data && this.data.get.apply(this.data, getArguments);
  2064. };
  2065. /**
  2066. * Get ids of all items or from a filtered set of items.
  2067. * @param {Object} [options] An Object with options. Available options:
  2068. * {function} [filter] filter items
  2069. * {String | function} [order] Order the items by
  2070. * a field name or custom sort function.
  2071. * @return {Array} ids
  2072. */
  2073. DataView.prototype.getIds = function (options) {
  2074. var ids;
  2075. if (this.data) {
  2076. var defaultFilter = this.options.filter;
  2077. var filter;
  2078. if (options && options.filter) {
  2079. if (defaultFilter) {
  2080. filter = function (item) {
  2081. return defaultFilter(item) && options.filter(item);
  2082. }
  2083. }
  2084. else {
  2085. filter = options.filter;
  2086. }
  2087. }
  2088. else {
  2089. filter = defaultFilter;
  2090. }
  2091. ids = this.data.getIds({
  2092. filter: filter,
  2093. order: options && options.order
  2094. });
  2095. }
  2096. else {
  2097. ids = [];
  2098. }
  2099. return ids;
  2100. };
  2101. /**
  2102. * Event listener. Will propagate all events from the connected data set to
  2103. * the subscribers of the DataView, but will filter the items and only trigger
  2104. * when there are changes in the filtered data set.
  2105. * @param {String} event
  2106. * @param {Object | null} params
  2107. * @param {String} senderId
  2108. * @private
  2109. */
  2110. DataView.prototype._onEvent = function (event, params, senderId) {
  2111. var i, len, id, item,
  2112. ids = params && params.items,
  2113. data = this.data,
  2114. added = [],
  2115. updated = [],
  2116. removed = [];
  2117. if (ids && data) {
  2118. switch (event) {
  2119. case 'add':
  2120. // filter the ids of the added items
  2121. for (i = 0, len = ids.length; i < len; i++) {
  2122. id = ids[i];
  2123. item = this.get(id);
  2124. if (item) {
  2125. this.ids[id] = true;
  2126. added.push(id);
  2127. }
  2128. }
  2129. break;
  2130. case 'update':
  2131. // determine the event from the views viewpoint: an updated
  2132. // item can be added, updated, or removed from this view.
  2133. for (i = 0, len = ids.length; i < len; i++) {
  2134. id = ids[i];
  2135. item = this.get(id);
  2136. if (item) {
  2137. if (this.ids[id]) {
  2138. updated.push(id);
  2139. }
  2140. else {
  2141. this.ids[id] = true;
  2142. added.push(id);
  2143. }
  2144. }
  2145. else {
  2146. if (this.ids[id]) {
  2147. delete this.ids[id];
  2148. removed.push(id);
  2149. }
  2150. else {
  2151. // nothing interesting for me :-(
  2152. }
  2153. }
  2154. }
  2155. break;
  2156. case 'remove':
  2157. // filter the ids of the removed items
  2158. for (i = 0, len = ids.length; i < len; i++) {
  2159. id = ids[i];
  2160. if (this.ids[id]) {
  2161. delete this.ids[id];
  2162. removed.push(id);
  2163. }
  2164. }
  2165. break;
  2166. }
  2167. if (added.length) {
  2168. this._trigger('add', {items: added}, senderId);
  2169. }
  2170. if (updated.length) {
  2171. this._trigger('update', {items: updated}, senderId);
  2172. }
  2173. if (removed.length) {
  2174. this._trigger('remove', {items: removed}, senderId);
  2175. }
  2176. }
  2177. };
  2178. // copy subscription functionality from DataSet
  2179. DataView.prototype.on = DataSet.prototype.on;
  2180. DataView.prototype.off = DataSet.prototype.off;
  2181. DataView.prototype._trigger = DataSet.prototype._trigger;
  2182. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2183. DataView.prototype.subscribe = DataView.prototype.on;
  2184. DataView.prototype.unsubscribe = DataView.prototype.off;
  2185. /**
  2186. * Utility functions for ordering and stacking of items
  2187. */
  2188. var stack = {};
  2189. /**
  2190. * Order items by their start data
  2191. * @param {Item[]} items
  2192. */
  2193. stack.orderByStart = function orderByStart(items) {
  2194. items.sort(function (a, b) {
  2195. return a.data.start - b.data.start;
  2196. });
  2197. };
  2198. /**
  2199. * Order items by their end date. If they have no end date, their start date
  2200. * is used.
  2201. * @param {Item[]} items
  2202. */
  2203. stack.orderByEnd = function orderByEnd(items) {
  2204. items.sort(function (a, b) {
  2205. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  2206. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  2207. return aTime - bTime;
  2208. });
  2209. };
  2210. /**
  2211. * Adjust vertical positions of the items such that they don't overlap each
  2212. * other.
  2213. * @param {Item[]} items
  2214. * All visible items
  2215. * @param {{item: number, axis: number}} margin
  2216. * Margins between items and between items and the axis.
  2217. * @param {boolean} [force=false]
  2218. * If true, all items will be repositioned. If false (default), only
  2219. * items having a top===null will be re-stacked
  2220. */
  2221. stack.stack = function _stack (items, margin, force) {
  2222. var i, iMax;
  2223. if (force) {
  2224. // reset top position of all items
  2225. for (i = 0, iMax = items.length; i < iMax; i++) {
  2226. items[i].top = null;
  2227. }
  2228. }
  2229. // calculate new, non-overlapping positions
  2230. for (i = 0, iMax = items.length; i < iMax; i++) {
  2231. var item = items[i];
  2232. if (item.top === null) {
  2233. // initialize top position
  2234. item.top = margin.axis;
  2235. do {
  2236. // TODO: optimize checking for overlap. when there is a gap without items,
  2237. // you only need to check for items from the next item on, not from zero
  2238. var collidingItem = null;
  2239. for (var j = 0, jj = items.length; j < jj; j++) {
  2240. var other = items[j];
  2241. if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
  2242. collidingItem = other;
  2243. break;
  2244. }
  2245. }
  2246. if (collidingItem != null) {
  2247. // There is a collision. Reposition the items above the colliding element
  2248. item.top = collidingItem.top + collidingItem.height + margin.item;
  2249. }
  2250. } while (collidingItem);
  2251. }
  2252. }
  2253. };
  2254. /**
  2255. * Adjust vertical positions of the items without stacking them
  2256. * @param {Item[]} items
  2257. * All visible items
  2258. * @param {{item: number, axis: number}} margin
  2259. * Margins between items and between items and the axis.
  2260. */
  2261. stack.nostack = function nostack (items, margin) {
  2262. var i, iMax;
  2263. // reset top position of all items
  2264. for (i = 0, iMax = items.length; i < iMax; i++) {
  2265. items[i].top = margin.axis;
  2266. }
  2267. };
  2268. /**
  2269. * Test if the two provided items collide
  2270. * The items must have parameters left, width, top, and height.
  2271. * @param {Item} a The first item
  2272. * @param {Item} b The second item
  2273. * @param {Number} margin A minimum required margin.
  2274. * If margin is provided, the two items will be
  2275. * marked colliding when they overlap or
  2276. * when the margin between the two is smaller than
  2277. * the requested margin.
  2278. * @return {boolean} true if a and b collide, else false
  2279. */
  2280. stack.collision = function collision (a, b, margin) {
  2281. return ((a.left - margin) < (b.left + b.width) &&
  2282. (a.left + a.width + margin) > b.left &&
  2283. (a.top - margin) < (b.top + b.height) &&
  2284. (a.top + a.height + margin) > b.top);
  2285. };
  2286. /**
  2287. * @constructor TimeStep
  2288. * The class TimeStep is an iterator for dates. You provide a start date and an
  2289. * end date. The class itself determines the best scale (step size) based on the
  2290. * provided start Date, end Date, and minimumStep.
  2291. *
  2292. * If minimumStep is provided, the step size is chosen as close as possible
  2293. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2294. * provided, the scale is set to 1 DAY.
  2295. * The minimumStep should correspond with the onscreen size of about 6 characters
  2296. *
  2297. * Alternatively, you can set a scale by hand.
  2298. * After creation, you can initialize the class by executing first(). Then you
  2299. * can iterate from the start date to the end date via next(). You can check if
  2300. * the end date is reached with the function hasNext(). After each step, you can
  2301. * retrieve the current date via getCurrent().
  2302. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2303. * days, to years.
  2304. *
  2305. * Version: 1.2
  2306. *
  2307. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2308. * or new Date(2010, 9, 21, 23, 45, 00)
  2309. * @param {Date} [end] The end date
  2310. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2311. */
  2312. function TimeStep(start, end, minimumStep) {
  2313. // variables
  2314. this.current = new Date();
  2315. this._start = new Date();
  2316. this._end = new Date();
  2317. this.autoScale = true;
  2318. this.scale = TimeStep.SCALE.DAY;
  2319. this.step = 1;
  2320. // initialize the range
  2321. this.setRange(start, end, minimumStep);
  2322. }
  2323. /// enum scale
  2324. TimeStep.SCALE = {
  2325. MILLISECOND: 1,
  2326. SECOND: 2,
  2327. MINUTE: 3,
  2328. HOUR: 4,
  2329. DAY: 5,
  2330. WEEKDAY: 6,
  2331. MONTH: 7,
  2332. YEAR: 8
  2333. };
  2334. /**
  2335. * Set a new range
  2336. * If minimumStep is provided, the step size is chosen as close as possible
  2337. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2338. * provided, the scale is set to 1 DAY.
  2339. * The minimumStep should correspond with the onscreen size of about 6 characters
  2340. * @param {Date} [start] The start date and time.
  2341. * @param {Date} [end] The end date and time.
  2342. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2343. */
  2344. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2345. if (!(start instanceof Date) || !(end instanceof Date)) {
  2346. throw "No legal start or end date in method setRange";
  2347. }
  2348. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2349. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2350. if (this.autoScale) {
  2351. this.setMinimumStep(minimumStep);
  2352. }
  2353. };
  2354. /**
  2355. * Set the range iterator to the start date.
  2356. */
  2357. TimeStep.prototype.first = function() {
  2358. this.current = new Date(this._start.valueOf());
  2359. this.roundToMinor();
  2360. };
  2361. /**
  2362. * Round the current date to the first minor date value
  2363. * This must be executed once when the current date is set to start Date
  2364. */
  2365. TimeStep.prototype.roundToMinor = function() {
  2366. // round to floor
  2367. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2368. //noinspection FallthroughInSwitchStatementJS
  2369. switch (this.scale) {
  2370. case TimeStep.SCALE.YEAR:
  2371. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2372. this.current.setMonth(0);
  2373. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2374. case TimeStep.SCALE.DAY: // intentional fall through
  2375. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2376. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2377. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2378. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2379. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2380. }
  2381. if (this.step != 1) {
  2382. // round down to the first minor value that is a multiple of the current step size
  2383. switch (this.scale) {
  2384. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2385. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2386. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2387. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2388. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2389. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2390. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2391. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2392. default: break;
  2393. }
  2394. }
  2395. };
  2396. /**
  2397. * Check if the there is a next step
  2398. * @return {boolean} true if the current date has not passed the end date
  2399. */
  2400. TimeStep.prototype.hasNext = function () {
  2401. return (this.current.valueOf() <= this._end.valueOf());
  2402. };
  2403. /**
  2404. * Do the next step
  2405. */
  2406. TimeStep.prototype.next = function() {
  2407. var prev = this.current.valueOf();
  2408. // Two cases, needed to prevent issues with switching daylight savings
  2409. // (end of March and end of October)
  2410. if (this.current.getMonth() < 6) {
  2411. switch (this.scale) {
  2412. case TimeStep.SCALE.MILLISECOND:
  2413. this.current = new Date(this.current.valueOf() + this.step); break;
  2414. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2415. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2416. case TimeStep.SCALE.HOUR:
  2417. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2418. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2419. var h = this.current.getHours();
  2420. this.current.setHours(h - (h % this.step));
  2421. break;
  2422. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2423. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2424. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2425. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2426. default: break;
  2427. }
  2428. }
  2429. else {
  2430. switch (this.scale) {
  2431. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2432. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2433. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2434. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2435. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2436. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2437. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2438. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2439. default: break;
  2440. }
  2441. }
  2442. if (this.step != 1) {
  2443. // round down to the correct major value
  2444. switch (this.scale) {
  2445. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2446. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2447. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2448. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2449. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2450. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2451. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2452. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2453. default: break;
  2454. }
  2455. }
  2456. // safety mechanism: if current time is still unchanged, move to the end
  2457. if (this.current.valueOf() == prev) {
  2458. this.current = new Date(this._end.valueOf());
  2459. }
  2460. };
  2461. /**
  2462. * Get the current datetime
  2463. * @return {Date} current The current date
  2464. */
  2465. TimeStep.prototype.getCurrent = function() {
  2466. return this.current;
  2467. };
  2468. /**
  2469. * Set a custom scale. Autoscaling will be disabled.
  2470. * For example setScale(SCALE.MINUTES, 5) will result
  2471. * in minor steps of 5 minutes, and major steps of an hour.
  2472. *
  2473. * @param {TimeStep.SCALE} newScale
  2474. * A scale. Choose from SCALE.MILLISECOND,
  2475. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2476. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2477. * SCALE.YEAR.
  2478. * @param {Number} newStep A step size, by default 1. Choose for
  2479. * example 1, 2, 5, or 10.
  2480. */
  2481. TimeStep.prototype.setScale = function(newScale, newStep) {
  2482. this.scale = newScale;
  2483. if (newStep > 0) {
  2484. this.step = newStep;
  2485. }
  2486. this.autoScale = false;
  2487. };
  2488. /**
  2489. * Enable or disable autoscaling
  2490. * @param {boolean} enable If true, autoascaling is set true
  2491. */
  2492. TimeStep.prototype.setAutoScale = function (enable) {
  2493. this.autoScale = enable;
  2494. };
  2495. /**
  2496. * Automatically determine the scale that bests fits the provided minimum step
  2497. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2498. */
  2499. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2500. if (minimumStep == undefined) {
  2501. return;
  2502. }
  2503. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2504. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2505. var stepDay = (1000 * 60 * 60 * 24);
  2506. var stepHour = (1000 * 60 * 60);
  2507. var stepMinute = (1000 * 60);
  2508. var stepSecond = (1000);
  2509. var stepMillisecond= (1);
  2510. // find the smallest step that is larger than the provided minimumStep
  2511. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2512. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2513. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2514. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2515. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2516. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2517. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2518. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2519. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2520. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2521. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2522. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2523. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2524. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2525. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2526. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2527. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2528. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2529. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2530. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2531. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2532. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2533. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2534. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2535. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2536. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2537. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2538. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2539. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2540. };
  2541. /**
  2542. * Snap a date to a rounded value.
  2543. * The snap intervals are dependent on the current scale and step.
  2544. * @param {Date} date the date to be snapped.
  2545. * @return {Date} snappedDate
  2546. */
  2547. TimeStep.prototype.snap = function(date) {
  2548. var clone = new Date(date.valueOf());
  2549. if (this.scale == TimeStep.SCALE.YEAR) {
  2550. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2551. clone.setFullYear(Math.round(year / this.step) * this.step);
  2552. clone.setMonth(0);
  2553. clone.setDate(0);
  2554. clone.setHours(0);
  2555. clone.setMinutes(0);
  2556. clone.setSeconds(0);
  2557. clone.setMilliseconds(0);
  2558. }
  2559. else if (this.scale == TimeStep.SCALE.MONTH) {
  2560. if (clone.getDate() > 15) {
  2561. clone.setDate(1);
  2562. clone.setMonth(clone.getMonth() + 1);
  2563. // important: first set Date to 1, after that change the month.
  2564. }
  2565. else {
  2566. clone.setDate(1);
  2567. }
  2568. clone.setHours(0);
  2569. clone.setMinutes(0);
  2570. clone.setSeconds(0);
  2571. clone.setMilliseconds(0);
  2572. }
  2573. else if (this.scale == TimeStep.SCALE.DAY) {
  2574. //noinspection FallthroughInSwitchStatementJS
  2575. switch (this.step) {
  2576. case 5:
  2577. case 2:
  2578. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2579. default:
  2580. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2581. }
  2582. clone.setMinutes(0);
  2583. clone.setSeconds(0);
  2584. clone.setMilliseconds(0);
  2585. }
  2586. else if (this.scale == TimeStep.SCALE.WEEKDAY) {
  2587. //noinspection FallthroughInSwitchStatementJS
  2588. switch (this.step) {
  2589. case 5:
  2590. case 2:
  2591. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2592. default:
  2593. clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
  2594. }
  2595. clone.setMinutes(0);
  2596. clone.setSeconds(0);
  2597. clone.setMilliseconds(0);
  2598. }
  2599. else if (this.scale == TimeStep.SCALE.HOUR) {
  2600. switch (this.step) {
  2601. case 4:
  2602. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2603. default:
  2604. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2605. }
  2606. clone.setSeconds(0);
  2607. clone.setMilliseconds(0);
  2608. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2609. //noinspection FallthroughInSwitchStatementJS
  2610. switch (this.step) {
  2611. case 15:
  2612. case 10:
  2613. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2614. clone.setSeconds(0);
  2615. break;
  2616. case 5:
  2617. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2618. default:
  2619. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2620. }
  2621. clone.setMilliseconds(0);
  2622. }
  2623. else if (this.scale == TimeStep.SCALE.SECOND) {
  2624. //noinspection FallthroughInSwitchStatementJS
  2625. switch (this.step) {
  2626. case 15:
  2627. case 10:
  2628. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2629. clone.setMilliseconds(0);
  2630. break;
  2631. case 5:
  2632. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2633. default:
  2634. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2635. }
  2636. }
  2637. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2638. var step = this.step > 5 ? this.step / 2 : 1;
  2639. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2640. }
  2641. return clone;
  2642. };
  2643. /**
  2644. * Check if the current value is a major value (for example when the step
  2645. * is DAY, a major value is each first day of the MONTH)
  2646. * @return {boolean} true if current date is major, else false.
  2647. */
  2648. TimeStep.prototype.isMajor = function() {
  2649. switch (this.scale) {
  2650. case TimeStep.SCALE.MILLISECOND:
  2651. return (this.current.getMilliseconds() == 0);
  2652. case TimeStep.SCALE.SECOND:
  2653. return (this.current.getSeconds() == 0);
  2654. case TimeStep.SCALE.MINUTE:
  2655. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2656. // Note: this is no bug. Major label is equal for both minute and hour scale
  2657. case TimeStep.SCALE.HOUR:
  2658. return (this.current.getHours() == 0);
  2659. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2660. case TimeStep.SCALE.DAY:
  2661. return (this.current.getDate() == 1);
  2662. case TimeStep.SCALE.MONTH:
  2663. return (this.current.getMonth() == 0);
  2664. case TimeStep.SCALE.YEAR:
  2665. return false;
  2666. default:
  2667. return false;
  2668. }
  2669. };
  2670. /**
  2671. * Returns formatted text for the minor axislabel, depending on the current
  2672. * date and the scale. For example when scale is MINUTE, the current time is
  2673. * formatted as "hh:mm".
  2674. * @param {Date} [date] custom date. if not provided, current date is taken
  2675. */
  2676. TimeStep.prototype.getLabelMinor = function(date) {
  2677. if (date == undefined) {
  2678. date = this.current;
  2679. }
  2680. switch (this.scale) {
  2681. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2682. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2683. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2684. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2685. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2686. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2687. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2688. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2689. default: return '';
  2690. }
  2691. };
  2692. /**
  2693. * Returns formatted text for the major axis label, depending on the current
  2694. * date and the scale. For example when scale is MINUTE, the major scale is
  2695. * hours, and the hour will be formatted as "hh".
  2696. * @param {Date} [date] custom date. if not provided, current date is taken
  2697. */
  2698. TimeStep.prototype.getLabelMajor = function(date) {
  2699. if (date == undefined) {
  2700. date = this.current;
  2701. }
  2702. return "";
  2703. };
  2704. /**
  2705. * @constructor DataStep
  2706. * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
  2707. * end data point. The class itself determines the best scale (step size) based on the
  2708. * provided start Date, end Date, and minimumStep.
  2709. *
  2710. * If minimumStep is provided, the step size is chosen as close as possible
  2711. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2712. * provided, the scale is set to 1 DAY.
  2713. * The minimumStep should correspond with the onscreen size of about 6 characters
  2714. *
  2715. * Alternatively, you can set a scale by hand.
  2716. * After creation, you can initialize the class by executing first(). Then you
  2717. * can iterate from the start date to the end date via next(). You can check if
  2718. * the end date is reached with the function hasNext(). After each step, you can
  2719. * retrieve the current date via getCurrent().
  2720. * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
  2721. * days, to years.
  2722. *
  2723. * Version: 1.2
  2724. *
  2725. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2726. * or new Date(2010, 9, 21, 23, 45, 00)
  2727. * @param {Date} [end] The end date
  2728. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2729. */
  2730. function DataStep(start, end, minimumStep, containerHeight) {
  2731. // variables
  2732. this.current = 0;
  2733. this.containerHeight = containerHeight;
  2734. this.autoScale = true;
  2735. this.stepIndex = 0;
  2736. this.step = 1;
  2737. this.scale = 1;
  2738. this.marginStart;
  2739. this.marginEnd;
  2740. this.majorSteps = [1, 2, 5, 10];
  2741. this.minorSteps = [0.25, 0.5, 1, 2];
  2742. this.setRange(start,end,minimumStep, containerHeight);
  2743. }
  2744. /**
  2745. * Set a new range
  2746. * If minimumStep is provided, the step size is chosen as close as possible
  2747. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2748. * provided, the scale is set to 1 DAY.
  2749. * The minimumStep should correspond with the onscreen size of about 6 characters
  2750. * @param {Number} [start] The start date and time.
  2751. * @param {Number} [end] The end date and time.
  2752. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2753. */
  2754. DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight) {
  2755. this._start = start;
  2756. this._end = end;
  2757. this.setFirst();
  2758. if (this.autoScale) {
  2759. this.setMinimumStep(minimumStep, containerHeight);
  2760. }
  2761. };
  2762. /**
  2763. * Set the range iterator to the start date.
  2764. */
  2765. DataStep.prototype.first = function() {
  2766. this.setFirst();
  2767. };
  2768. /**
  2769. * Round the current date to the first minor date value
  2770. * This must be executed once when the current date is set to start Date
  2771. */
  2772. DataStep.prototype.setFirst = function() {
  2773. var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
  2774. var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
  2775. this.marginEnd = this.roundToMinor(niceEnd);
  2776. this.marginStart = this.roundToMinor(niceStart);
  2777. this.marginRange = this.marginEnd - this.marginStart;
  2778. this.current = this.marginEnd;
  2779. };
  2780. DataStep.prototype.roundToMinor = function(value) {
  2781. var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
  2782. if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
  2783. return rounded + (this.scale * this.minorSteps[this.stepIndex]);
  2784. }
  2785. else {
  2786. return rounded;
  2787. }
  2788. }
  2789. /**
  2790. * Check if the there is a next step
  2791. * @return {boolean} true if the current date has not passed the end date
  2792. */
  2793. DataStep.prototype.hasNext = function () {
  2794. return (this.current >= this.marginStart);
  2795. };
  2796. /**
  2797. * Do the next step
  2798. */
  2799. DataStep.prototype.next = function() {
  2800. var prev = this.current;
  2801. this.current -= this.step;
  2802. // safety mechanism: if current time is still unchanged, move to the end
  2803. if (this.current == prev) {
  2804. this.current = this._end;
  2805. }
  2806. };
  2807. /**
  2808. * Get the current datetime
  2809. * @return {Date} current The current date
  2810. */
  2811. DataStep.prototype.getCurrent = function() {
  2812. return this.current;
  2813. };
  2814. /**
  2815. * Automatically determine the scale that bests fits the provided minimum step
  2816. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2817. */
  2818. DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
  2819. // round to floor
  2820. var size = this._end - this._start;
  2821. var safeSize = size * 1.1;
  2822. var minimumStepValue = minimumStep * (safeSize / containerHeight);
  2823. var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
  2824. var minorStepIdx = -1;
  2825. var magnitudefactor = Math.pow(10,orderOfMagnitude);
  2826. var solutionFound = false;
  2827. for (var i = 0; i <= orderOfMagnitude; i++) {
  2828. magnitudefactor = Math.pow(10,i);
  2829. for (var j = 0; j < this.minorSteps.length; j++) {
  2830. var stepSize = magnitudefactor * this.minorSteps[j];
  2831. if (stepSize >= minimumStepValue) {
  2832. solutionFound = true;
  2833. minorStepIdx = j;
  2834. break;
  2835. }
  2836. }
  2837. if (solutionFound == true) {
  2838. break;
  2839. }
  2840. }
  2841. this.stepIndex = minorStepIdx;
  2842. this.scale = magnitudefactor;
  2843. this.step = magnitudefactor * this.minorSteps[minorStepIdx];
  2844. };
  2845. /**
  2846. * Snap a date to a rounded value.
  2847. * The snap intervals are dependent on the current scale and step.
  2848. * @param {Date} date the date to be snapped.
  2849. * @return {Date} snappedDate
  2850. */
  2851. DataStep.prototype.snap = function(date) {
  2852. };
  2853. /**
  2854. * Check if the current value is a major value (for example when the step
  2855. * is DAY, a major value is each first day of the MONTH)
  2856. * @return {boolean} true if current date is major, else false.
  2857. */
  2858. DataStep.prototype.isMajor = function() {
  2859. return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
  2860. };
  2861. /**
  2862. * Returns formatted text for the minor axislabel, depending on the current
  2863. * date and the scale. For example when scale is MINUTE, the current time is
  2864. * formatted as "hh:mm".
  2865. * @param {Date} [date] custom date. if not provided, current date is taken
  2866. */
  2867. DataStep.prototype.getLabelMinor = function() {
  2868. return this.current;
  2869. };
  2870. /**
  2871. * Returns formatted text for the major axis label, depending on the current
  2872. * date and the scale. For example when scale is MINUTE, the major scale is
  2873. * hours, and the hour will be formatted as "hh".
  2874. * @param {Date} [date] custom date. if not provided, current date is taken
  2875. */
  2876. DataStep.prototype.getLabelMajor = function() {
  2877. return this.current;
  2878. };
  2879. /**
  2880. * @constructor Range
  2881. * A Range controls a numeric range with a start and end value.
  2882. * The Range adjusts the range based on mouse events or programmatic changes,
  2883. * and triggers events when the range is changing or has been changed.
  2884. * @param {RootPanel} root Root panel, used to subscribe to events
  2885. * @param {Panel} parent Parent panel, used to attach to the DOM
  2886. * @param {Object} [options] See description at Range.setOptions
  2887. */
  2888. function Range(root, parent, options) {
  2889. this.id = util.randomUUID();
  2890. this.start = null; // Number
  2891. this.end = null; // Number
  2892. this.root = root;
  2893. this.parent = parent;
  2894. this.options = options || {};
  2895. // drag listeners for dragging
  2896. this.root.on('dragstart', this._onDragStart.bind(this));
  2897. this.root.on('drag', this._onDrag.bind(this));
  2898. this.root.on('dragend', this._onDragEnd.bind(this));
  2899. // ignore dragging when holding
  2900. this.root.on('hold', this._onHold.bind(this));
  2901. // mouse wheel for zooming
  2902. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  2903. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  2904. // pinch to zoom
  2905. this.root.on('touch', this._onTouch.bind(this));
  2906. this.root.on('pinch', this._onPinch.bind(this));
  2907. this.setOptions(options);
  2908. }
  2909. // turn Range into an event emitter
  2910. Emitter(Range.prototype);
  2911. /**
  2912. * Set options for the range controller
  2913. * @param {Object} options Available options:
  2914. * {Number} min Minimum value for start
  2915. * {Number} max Maximum value for end
  2916. * {Number} zoomMin Set a minimum value for
  2917. * (end - start).
  2918. * {Number} zoomMax Set a maximum value for
  2919. * (end - start).
  2920. */
  2921. Range.prototype.setOptions = function (options) {
  2922. util.extend(this.options, options);
  2923. // re-apply range with new limitations
  2924. if (this.start !== null && this.end !== null) {
  2925. this.setRange(this.start, this.end);
  2926. }
  2927. };
  2928. /**
  2929. * Test whether direction has a valid value
  2930. * @param {String} direction 'horizontal' or 'vertical'
  2931. */
  2932. function validateDirection (direction) {
  2933. if (direction != 'horizontal' && direction != 'vertical') {
  2934. throw new TypeError('Unknown direction "' + direction + '". ' +
  2935. 'Choose "horizontal" or "vertical".');
  2936. }
  2937. }
  2938. /**
  2939. * Set a new start and end range
  2940. * @param {Number} [start]
  2941. * @param {Number} [end]
  2942. */
  2943. Range.prototype.setRange = function(start, end) {
  2944. var changed = this._applyRange(start, end);
  2945. if (changed) {
  2946. var params = {
  2947. start: new Date(this.start),
  2948. end: new Date(this.end)
  2949. };
  2950. this.emit('rangechange', params);
  2951. this.emit('rangechanged', params);
  2952. }
  2953. };
  2954. /**
  2955. * Set a new start and end range. This method is the same as setRange, but
  2956. * does not trigger a range change and range changed event, and it returns
  2957. * true when the range is changed
  2958. * @param {Number} [start]
  2959. * @param {Number} [end]
  2960. * @return {Boolean} changed
  2961. * @private
  2962. */
  2963. Range.prototype._applyRange = function(start, end) {
  2964. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2965. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2966. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2967. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2968. diff;
  2969. // check for valid number
  2970. if (isNaN(newStart) || newStart === null) {
  2971. throw new Error('Invalid start "' + start + '"');
  2972. }
  2973. if (isNaN(newEnd) || newEnd === null) {
  2974. throw new Error('Invalid end "' + end + '"');
  2975. }
  2976. // prevent start < end
  2977. if (newEnd < newStart) {
  2978. newEnd = newStart;
  2979. }
  2980. // prevent start < min
  2981. if (min !== null) {
  2982. if (newStart < min) {
  2983. diff = (min - newStart);
  2984. newStart += diff;
  2985. newEnd += diff;
  2986. // prevent end > max
  2987. if (max != null) {
  2988. if (newEnd > max) {
  2989. newEnd = max;
  2990. }
  2991. }
  2992. }
  2993. }
  2994. // prevent end > max
  2995. if (max !== null) {
  2996. if (newEnd > max) {
  2997. diff = (newEnd - max);
  2998. newStart -= diff;
  2999. newEnd -= diff;
  3000. // prevent start < min
  3001. if (min != null) {
  3002. if (newStart < min) {
  3003. newStart = min;
  3004. }
  3005. }
  3006. }
  3007. }
  3008. // prevent (end-start) < zoomMin
  3009. if (this.options.zoomMin !== null) {
  3010. var zoomMin = parseFloat(this.options.zoomMin);
  3011. if (zoomMin < 0) {
  3012. zoomMin = 0;
  3013. }
  3014. if ((newEnd - newStart) < zoomMin) {
  3015. if ((this.end - this.start) === zoomMin) {
  3016. // ignore this action, we are already zoomed to the minimum
  3017. newStart = this.start;
  3018. newEnd = this.end;
  3019. }
  3020. else {
  3021. // zoom to the minimum
  3022. diff = (zoomMin - (newEnd - newStart));
  3023. newStart -= diff / 2;
  3024. newEnd += diff / 2;
  3025. }
  3026. }
  3027. }
  3028. // prevent (end-start) > zoomMax
  3029. if (this.options.zoomMax !== null) {
  3030. var zoomMax = parseFloat(this.options.zoomMax);
  3031. if (zoomMax < 0) {
  3032. zoomMax = 0;
  3033. }
  3034. if ((newEnd - newStart) > zoomMax) {
  3035. if ((this.end - this.start) === zoomMax) {
  3036. // ignore this action, we are already zoomed to the maximum
  3037. newStart = this.start;
  3038. newEnd = this.end;
  3039. }
  3040. else {
  3041. // zoom to the maximum
  3042. diff = ((newEnd - newStart) - zoomMax);
  3043. newStart += diff / 2;
  3044. newEnd -= diff / 2;
  3045. }
  3046. }
  3047. }
  3048. var changed = (this.start != newStart || this.end != newEnd);
  3049. this.start = newStart;
  3050. this.end = newEnd;
  3051. return changed;
  3052. };
  3053. /**
  3054. * Retrieve the current range.
  3055. * @return {Object} An object with start and end properties
  3056. */
  3057. Range.prototype.getRange = function() {
  3058. return {
  3059. start: this.start,
  3060. end: this.end
  3061. };
  3062. };
  3063. /**
  3064. * Calculate the conversion offset and scale for current range, based on
  3065. * the provided width
  3066. * @param {Number} width
  3067. * @returns {{offset: number, scale: number}} conversion
  3068. */
  3069. Range.prototype.conversion = function (width) {
  3070. return Range.conversion(this.start, this.end, width);
  3071. };
  3072. /**
  3073. * Static method to calculate the conversion offset and scale for a range,
  3074. * based on the provided start, end, and width
  3075. * @param {Number} start
  3076. * @param {Number} end
  3077. * @param {Number} width
  3078. * @returns {{offset: number, scale: number}} conversion
  3079. */
  3080. Range.conversion = function (start, end, width) {
  3081. if (width != 0 && (end - start != 0)) {
  3082. return {
  3083. offset: start,
  3084. scale: width / (end - start)
  3085. }
  3086. }
  3087. else {
  3088. return {
  3089. offset: 0,
  3090. scale: 1
  3091. };
  3092. }
  3093. };
  3094. // global (private) object to store drag params
  3095. var touchParams = {};
  3096. /**
  3097. * Start dragging horizontally or vertically
  3098. * @param {Event} event
  3099. * @private
  3100. */
  3101. Range.prototype._onDragStart = function(event) {
  3102. // refuse to drag when we where pinching to prevent the timeline make a jump
  3103. // when releasing the fingers in opposite order from the touch screen
  3104. if (touchParams.ignore) return;
  3105. // TODO: reckon with option movable
  3106. touchParams.start = this.start;
  3107. touchParams.end = this.end;
  3108. var frame = this.parent.frame;
  3109. if (frame) {
  3110. frame.style.cursor = 'move';
  3111. }
  3112. };
  3113. /**
  3114. * Perform dragging operating.
  3115. * @param {Event} event
  3116. * @private
  3117. */
  3118. Range.prototype._onDrag = function (event) {
  3119. var direction = this.options.direction;
  3120. validateDirection(direction);
  3121. // TODO: reckon with option movable
  3122. // refuse to drag when we where pinching to prevent the timeline make a jump
  3123. // when releasing the fingers in opposite order from the touch screen
  3124. if (touchParams.ignore) return;
  3125. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3126. interval = (touchParams.end - touchParams.start),
  3127. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  3128. diffRange = -delta / width * interval;
  3129. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3130. this.emit('rangechange', {
  3131. start: new Date(this.start),
  3132. end: new Date(this.end)
  3133. });
  3134. };
  3135. /**
  3136. * Stop dragging operating.
  3137. * @param {event} event
  3138. * @private
  3139. */
  3140. Range.prototype._onDragEnd = function (event) {
  3141. // refuse to drag when we where pinching to prevent the timeline make a jump
  3142. // when releasing the fingers in opposite order from the touch screen
  3143. if (touchParams.ignore) return;
  3144. // TODO: reckon with option movable
  3145. if (this.parent.frame) {
  3146. this.parent.frame.style.cursor = 'auto';
  3147. }
  3148. // fire a rangechanged event
  3149. this.emit('rangechanged', {
  3150. start: new Date(this.start),
  3151. end: new Date(this.end)
  3152. });
  3153. };
  3154. /**
  3155. * Event handler for mouse wheel event, used to zoom
  3156. * Code from http://adomas.org/javascript-mouse-wheel/
  3157. * @param {Event} event
  3158. * @private
  3159. */
  3160. Range.prototype._onMouseWheel = function(event) {
  3161. // TODO: reckon with option zoomable
  3162. // retrieve delta
  3163. var delta = 0;
  3164. if (event.wheelDelta) { /* IE/Opera. */
  3165. delta = event.wheelDelta / 120;
  3166. } else if (event.detail) { /* Mozilla case. */
  3167. // In Mozilla, sign of delta is different than in IE.
  3168. // Also, delta is multiple of 3.
  3169. delta = -event.detail / 3;
  3170. }
  3171. // If delta is nonzero, handle it.
  3172. // Basically, delta is now positive if wheel was scrolled up,
  3173. // and negative, if wheel was scrolled down.
  3174. if (delta) {
  3175. // perform the zoom action. Delta is normally 1 or -1
  3176. // adjust a negative delta such that zooming in with delta 0.1
  3177. // equals zooming out with a delta -0.1
  3178. var scale;
  3179. if (delta < 0) {
  3180. scale = 1 - (delta / 5);
  3181. }
  3182. else {
  3183. scale = 1 / (1 + (delta / 5)) ;
  3184. }
  3185. // calculate center, the date to zoom around
  3186. var gesture = util.fakeGesture(this, event),
  3187. pointer = getPointer(gesture.center, this.parent.frame),
  3188. pointerDate = this._pointerToDate(pointer);
  3189. this.zoom(scale, pointerDate);
  3190. }
  3191. // Prevent default actions caused by mouse wheel
  3192. // (else the page and timeline both zoom and scroll)
  3193. event.preventDefault();
  3194. };
  3195. /**
  3196. * Start of a touch gesture
  3197. * @private
  3198. */
  3199. Range.prototype._onTouch = function (event) {
  3200. touchParams.start = this.start;
  3201. touchParams.end = this.end;
  3202. touchParams.ignore = false;
  3203. touchParams.center = null;
  3204. // don't move the range when dragging a selected event
  3205. // TODO: it's not so neat to have to know about the state of the ItemSet
  3206. var item = ItemSet.itemFromTarget(event);
  3207. if (item && item.selected && this.options.editable) {
  3208. touchParams.ignore = true;
  3209. }
  3210. };
  3211. /**
  3212. * On start of a hold gesture
  3213. * @private
  3214. */
  3215. Range.prototype._onHold = function () {
  3216. touchParams.ignore = true;
  3217. };
  3218. /**
  3219. * Handle pinch event
  3220. * @param {Event} event
  3221. * @private
  3222. */
  3223. Range.prototype._onPinch = function (event) {
  3224. var direction = this.options.direction;
  3225. touchParams.ignore = true;
  3226. // TODO: reckon with option zoomable
  3227. if (event.gesture.touches.length > 1) {
  3228. if (!touchParams.center) {
  3229. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  3230. }
  3231. var scale = 1 / event.gesture.scale,
  3232. initDate = this._pointerToDate(touchParams.center),
  3233. center = getPointer(event.gesture.center, this.parent.frame),
  3234. date = this._pointerToDate(this.parent, center),
  3235. delta = date - initDate; // TODO: utilize delta
  3236. // calculate new start and end
  3237. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3238. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3239. // apply new range
  3240. this.setRange(newStart, newEnd);
  3241. }
  3242. };
  3243. /**
  3244. * Helper function to calculate the center date for zooming
  3245. * @param {{x: Number, y: Number}} pointer
  3246. * @return {number} date
  3247. * @private
  3248. */
  3249. Range.prototype._pointerToDate = function (pointer) {
  3250. var conversion;
  3251. var direction = this.options.direction;
  3252. validateDirection(direction);
  3253. if (direction == 'horizontal') {
  3254. var width = this.parent.width;
  3255. conversion = this.conversion(width);
  3256. return pointer.x / conversion.scale + conversion.offset;
  3257. }
  3258. else {
  3259. var height = this.parent.height;
  3260. conversion = this.conversion(height);
  3261. return pointer.y / conversion.scale + conversion.offset;
  3262. }
  3263. };
  3264. /**
  3265. * Get the pointer location relative to the location of the dom element
  3266. * @param {{pageX: Number, pageY: Number}} touch
  3267. * @param {Element} element HTML DOM element
  3268. * @return {{x: Number, y: Number}} pointer
  3269. * @private
  3270. */
  3271. function getPointer (touch, element) {
  3272. return {
  3273. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3274. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3275. };
  3276. }
  3277. /**
  3278. * Zoom the range the given scale in or out. Start and end date will
  3279. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3280. * date around which to zoom.
  3281. * For example, try scale = 0.9 or 1.1
  3282. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3283. * values below 1 will zoom in.
  3284. * @param {Number} [center] Value representing a date around which will
  3285. * be zoomed.
  3286. */
  3287. Range.prototype.zoom = function(scale, center) {
  3288. // if centerDate is not provided, take it half between start Date and end Date
  3289. if (center == null) {
  3290. center = (this.start + this.end) / 2;
  3291. }
  3292. // calculate new start and end
  3293. var newStart = center + (this.start - center) * scale;
  3294. var newEnd = center + (this.end - center) * scale;
  3295. this.setRange(newStart, newEnd);
  3296. };
  3297. /**
  3298. * Move the range with a given delta to the left or right. Start and end
  3299. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3300. * @param {Number} delta Moving amount. Positive value will move right,
  3301. * negative value will move left
  3302. */
  3303. Range.prototype.move = function(delta) {
  3304. // zoom start Date and end Date relative to the centerDate
  3305. var diff = (this.end - this.start);
  3306. // apply new values
  3307. var newStart = this.start + diff * delta;
  3308. var newEnd = this.end + diff * delta;
  3309. // TODO: reckon with min and max range
  3310. this.start = newStart;
  3311. this.end = newEnd;
  3312. };
  3313. /**
  3314. * Move the range to a new center point
  3315. * @param {Number} moveTo New center point of the range
  3316. */
  3317. Range.prototype.moveTo = function(moveTo) {
  3318. var center = (this.start + this.end) / 2;
  3319. var diff = center - moveTo;
  3320. // calculate new start and end
  3321. var newStart = this.start - diff;
  3322. var newEnd = this.end - diff;
  3323. this.setRange(newStart, newEnd);
  3324. };
  3325. /**
  3326. * Prototype for visual components
  3327. */
  3328. function Component () {
  3329. this.id = null;
  3330. this.parent = null;
  3331. this.childs = null;
  3332. this.options = null;
  3333. this.top = 0;
  3334. this.left = 0;
  3335. this.width = 0;
  3336. this.height = 0;
  3337. }
  3338. // Turn the Component into an event emitter
  3339. Emitter(Component.prototype);
  3340. /**
  3341. * Set parameters for the frame. Parameters will be merged in current parameter
  3342. * set.
  3343. * @param {Object} options Available parameters:
  3344. * {String | function} [className]
  3345. * {String | Number | function} [left]
  3346. * {String | Number | function} [top]
  3347. * {String | Number | function} [width]
  3348. * {String | Number | function} [height]
  3349. */
  3350. Component.prototype.setOptions = function setOptions(options) {
  3351. if (options) {
  3352. util.extend(this.options, options);
  3353. this.repaint();
  3354. }
  3355. };
  3356. /**
  3357. * Get an option value by name
  3358. * The function will first check this.options object, and else will check
  3359. * this.defaultOptions.
  3360. * @param {String} name
  3361. * @return {*} value
  3362. */
  3363. Component.prototype.getOption = function getOption(name) {
  3364. var value;
  3365. if (this.options) {
  3366. value = this.options[name];
  3367. }
  3368. if (value === undefined && this.defaultOptions) {
  3369. value = this.defaultOptions[name];
  3370. }
  3371. return value;
  3372. };
  3373. /**
  3374. * Get the frame element of the component, the outer HTML DOM element.
  3375. * @returns {HTMLElement | null} frame
  3376. */
  3377. Component.prototype.getFrame = function getFrame() {
  3378. // should be implemented by the component
  3379. return null;
  3380. };
  3381. /**
  3382. * Repaint the component
  3383. * @return {boolean} Returns true if the component is resized
  3384. */
  3385. Component.prototype.repaint = function repaint() {
  3386. // should be implemented by the component
  3387. return false;
  3388. };
  3389. /**
  3390. * Test whether the component is resized since the last time _isResized() was
  3391. * called.
  3392. * @return {Boolean} Returns true if the component is resized
  3393. * @protected
  3394. */
  3395. Component.prototype._isResized = function _isResized() {
  3396. var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
  3397. this._previousWidth = this.width;
  3398. this._previousHeight = this.height;
  3399. return resized;
  3400. };
  3401. /**
  3402. * A panel can contain components
  3403. * @param {Object} [options] Available parameters:
  3404. * {String | Number | function} [left]
  3405. * {String | Number | function} [top]
  3406. * {String | Number | function} [width]
  3407. * {String | Number | function} [height]
  3408. * {String | function} [className]
  3409. * @constructor Panel
  3410. * @extends Component
  3411. */
  3412. function Panel(options) {
  3413. this.id = util.randomUUID();
  3414. this.parent = null;
  3415. this.childs = [];
  3416. this.visibilityForced = false;
  3417. this.visibility = false;
  3418. this.options = options || {};
  3419. // create frame
  3420. this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
  3421. }
  3422. Panel.prototype = new Component();
  3423. /**
  3424. * Set options. Will extend the current options.
  3425. * @param {Object} [options] Available parameters:
  3426. * {String | function} [className]
  3427. * {String | Number | function} [left]
  3428. * {String | Number | function} [top]
  3429. * {String | Number | function} [width]
  3430. * {String | Number | function} [height]
  3431. */
  3432. Panel.prototype.setOptions = Component.prototype.setOptions;
  3433. /**
  3434. * Get the outer frame of the panel
  3435. * @returns {HTMLElement} frame
  3436. */
  3437. Panel.prototype.getFrame = function () {
  3438. return this.frame;
  3439. };
  3440. /**
  3441. * Append a child to the panel
  3442. * @param {Component} child
  3443. */
  3444. Panel.prototype.appendChild = function (child) {
  3445. this.childs.push(child);
  3446. child.parent = this;
  3447. // attach to the DOM
  3448. var frame = child.getFrame();
  3449. if (frame) {
  3450. if (frame.parentNode) {
  3451. frame.parentNode.removeChild(frame);
  3452. }
  3453. this.frame.appendChild(frame);
  3454. }
  3455. };
  3456. /**
  3457. * Insert a child to the panel
  3458. * @param {Component} child
  3459. * @param {Component} beforeChild
  3460. */
  3461. Panel.prototype.insertBefore = function (child, beforeChild) {
  3462. var index = this.childs.indexOf(beforeChild);
  3463. if (index != -1) {
  3464. this.childs.splice(index, 0, child);
  3465. child.parent = this;
  3466. // attach to the DOM
  3467. var frame = child.getFrame();
  3468. if (frame) {
  3469. if (frame.parentNode) {
  3470. frame.parentNode.removeChild(frame);
  3471. }
  3472. var beforeFrame = beforeChild.getFrame();
  3473. if (beforeFrame) {
  3474. this.frame.insertBefore(frame, beforeFrame);
  3475. }
  3476. else {
  3477. this.frame.appendChild(frame);
  3478. }
  3479. }
  3480. }
  3481. };
  3482. /**
  3483. * Remove a child from the panel
  3484. * @param {Component} child
  3485. */
  3486. Panel.prototype.removeChild = function (child) {
  3487. var index = this.childs.indexOf(child);
  3488. if (index != -1) {
  3489. this.childs.splice(index, 1);
  3490. child.parent = null;
  3491. // remove from the DOM
  3492. var frame = child.getFrame();
  3493. if (frame && frame.parentNode) {
  3494. this.frame.removeChild(frame);
  3495. }
  3496. }
  3497. };
  3498. /**
  3499. * Test whether the panel contains given child
  3500. * @param {Component} child
  3501. */
  3502. Panel.prototype.hasChild = function (child) {
  3503. var index = this.childs.indexOf(child);
  3504. return (index != -1);
  3505. };
  3506. /**
  3507. * Test whether the panel contains given child
  3508. * @param {Component} child
  3509. */
  3510. Panel.prototype.hidePanel = function () {
  3511. this.visibilityForced = true;
  3512. this.visibility = false;
  3513. var frame = this.getFrame();
  3514. if (frame.className.search(" hidden") == -1) {
  3515. frame.className += " hidden";
  3516. }
  3517. };
  3518. /**
  3519. * Test whether the panel contains given child
  3520. * @param {Component} child
  3521. */
  3522. Panel.prototype.showPanel = function () {
  3523. this.visibilityForced = true;
  3524. this.visibility = true;
  3525. var frame = this.getFrame();
  3526. frame.className = frame.className.replace(" hidden","");
  3527. };
  3528. /**
  3529. * Repaint the component
  3530. * @return {boolean} Returns true if the component was resized since previous repaint
  3531. */
  3532. Panel.prototype.repaint = function () {
  3533. var asString = util.option.asString,
  3534. options = this.options,
  3535. frame = this.getFrame();
  3536. // update className
  3537. frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
  3538. if (this.visibilityForced == true && this.visibility == true) {
  3539. this.showPanel();
  3540. }
  3541. else if (this.visibilityForced == true) {
  3542. this.hidePanel();
  3543. }
  3544. // repaint the child components
  3545. var childsResized = this._repaintChilds();
  3546. // update frame size
  3547. this._updateSize();
  3548. return this._isResized() || childsResized;
  3549. };
  3550. /**
  3551. * Repaint all childs of the panel
  3552. * @return {boolean} Returns true if the component is resized
  3553. * @private
  3554. */
  3555. Panel.prototype._repaintChilds = function () {
  3556. var resized = false;
  3557. for (var i = 0, ii = this.childs.length; i < ii; i++) {
  3558. resized = this.childs[i].repaint() || resized;
  3559. }
  3560. return resized;
  3561. };
  3562. /**
  3563. * Apply the size from options to the panel, and recalculate it's actual size.
  3564. * @private
  3565. */
  3566. Panel.prototype._updateSize = function () {
  3567. // apply size
  3568. this.frame.style.top = util.option.asSize(this.options.top);
  3569. this.frame.style.bottom = util.option.asSize(this.options.bottom);
  3570. this.frame.style.left = util.option.asSize(this.options.left);
  3571. this.frame.style.right = util.option.asSize(this.options.right);
  3572. this.frame.style.width = util.option.asSize(this.options.width, '100%');
  3573. this.frame.style.height = util.option.asSize(this.options.height, '');
  3574. // get actual size
  3575. this.top = this.frame.offsetTop;
  3576. this.left = this.frame.offsetLeft;
  3577. this.width = this.frame.offsetWidth;
  3578. this.height = this.frame.offsetHeight;
  3579. };
  3580. /**
  3581. * A root panel can hold components. The root panel must be initialized with
  3582. * a DOM element as container.
  3583. * @param {HTMLElement} container
  3584. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3585. * @constructor RootPanel
  3586. * @extends Panel
  3587. */
  3588. function RootPanel(container, options) {
  3589. this.id = util.randomUUID();
  3590. this.container = container;
  3591. this.options = options || {};
  3592. this.defaultOptions = {
  3593. autoResize: true
  3594. };
  3595. // create the HTML DOM
  3596. this._create();
  3597. // attach the root panel to the provided container
  3598. if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
  3599. this.container.appendChild(this.getFrame());
  3600. this._initWatch();
  3601. }
  3602. RootPanel.prototype = new Panel();
  3603. /**
  3604. * Create the HTML DOM for the root panel
  3605. */
  3606. RootPanel.prototype._create = function _create() {
  3607. // create frame
  3608. this.frame = document.createElement('div');
  3609. // create event listeners for all interesting events, these events will be
  3610. // emitted via emitter
  3611. this.hammer = Hammer(this.frame, {
  3612. prevent_default: true
  3613. });
  3614. this.listeners = {};
  3615. var me = this;
  3616. var events = [
  3617. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3618. 'dragstart', 'drag', 'dragend',
  3619. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3620. ];
  3621. events.forEach(function (event) {
  3622. var listener = function () {
  3623. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3624. me.emit.apply(me, args);
  3625. };
  3626. me.hammer.on(event, listener);
  3627. me.listeners[event] = listener;
  3628. });
  3629. };
  3630. /**
  3631. * Set options. Will extend the current options.
  3632. * @param {Object} [options] Available parameters:
  3633. * {String | function} [className]
  3634. * {String | Number | function} [left]
  3635. * {String | Number | function} [top]
  3636. * {String | Number | function} [width]
  3637. * {String | Number | function} [height]
  3638. * {Boolean | function} [autoResize]
  3639. */
  3640. RootPanel.prototype.setOptions = function setOptions(options) {
  3641. if (options) {
  3642. util.extend(this.options, options);
  3643. this.repaint();
  3644. this._initWatch();
  3645. }
  3646. };
  3647. /**
  3648. * Get the frame of the root panel
  3649. */
  3650. RootPanel.prototype.getFrame = function getFrame() {
  3651. return this.frame;
  3652. };
  3653. /**
  3654. * Repaint the root panel
  3655. */
  3656. RootPanel.prototype.repaint = function repaint() {
  3657. // update class name
  3658. var options = this.options;
  3659. var editable = options.editable.updateTime || options.editable.updateGroup;
  3660. var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
  3661. if (options.className) className += ' ' + util.option.asString(className);
  3662. this.frame.className = className;
  3663. // repaint the child components
  3664. var childsResized = this._repaintChilds();
  3665. // update frame size
  3666. this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
  3667. this.frame.style.minHeight = util.option.asSize(this.options.minHeight, '');
  3668. this._updateSize();
  3669. // if the root panel or any of its childs is resized, repaint again,
  3670. // as other components may need to be resized accordingly
  3671. var resized = this._isResized() || childsResized;
  3672. if (resized) {
  3673. setTimeout(this.repaint.bind(this), 0);
  3674. }
  3675. };
  3676. /**
  3677. * Initialize watching when option autoResize is true
  3678. * @private
  3679. */
  3680. RootPanel.prototype._initWatch = function _initWatch() {
  3681. var autoResize = this.getOption('autoResize');
  3682. if (autoResize) {
  3683. this._watch();
  3684. }
  3685. else {
  3686. this._unwatch();
  3687. }
  3688. };
  3689. /**
  3690. * Watch for changes in the size of the frame. On resize, the Panel will
  3691. * automatically redraw itself.
  3692. * @private
  3693. */
  3694. RootPanel.prototype._watch = function _watch() {
  3695. var me = this;
  3696. this._unwatch();
  3697. var checkSize = function checkSize() {
  3698. var autoResize = me.getOption('autoResize');
  3699. if (!autoResize) {
  3700. // stop watching when the option autoResize is changed to false
  3701. me._unwatch();
  3702. return;
  3703. }
  3704. if (me.frame) {
  3705. // check whether the frame is resized
  3706. if ((me.frame.clientWidth != me.lastWidth) ||
  3707. (me.frame.clientHeight != me.lastHeight)) {
  3708. me.lastWidth = me.frame.clientWidth;
  3709. me.lastHeight = me.frame.clientHeight;
  3710. me.repaint();
  3711. // TODO: emit a resize event instead?
  3712. }
  3713. }
  3714. };
  3715. // TODO: automatically cleanup the event listener when the frame is deleted
  3716. util.addEventListener(window, 'resize', checkSize);
  3717. this.watchTimer = setInterval(checkSize, 1000);
  3718. };
  3719. /**
  3720. * Stop watching for a resize of the frame.
  3721. * @private
  3722. */
  3723. RootPanel.prototype._unwatch = function _unwatch() {
  3724. if (this.watchTimer) {
  3725. clearInterval(this.watchTimer);
  3726. this.watchTimer = undefined;
  3727. }
  3728. // TODO: remove event listener on window.resize
  3729. };
  3730. /**
  3731. * A horizontal time axis
  3732. * @param {Object} [options] See DataAxis.setOptions for the available
  3733. * options.
  3734. * @constructor DataAxis
  3735. * @extends Component
  3736. */
  3737. function DataAxis (options) {
  3738. this.id = util.randomUUID();
  3739. this.dom = {
  3740. majorLines: [],
  3741. majorTexts: [],
  3742. minorLines: [],
  3743. minorTexts: [],
  3744. redundant: {
  3745. majorLines: [],
  3746. majorTexts: [],
  3747. minorLines: [],
  3748. minorTexts: []
  3749. }
  3750. };
  3751. this.props = {
  3752. range: {
  3753. start: 0,
  3754. end: 0,
  3755. minimumStep: 0
  3756. },
  3757. lineTop: 0
  3758. };
  3759. this.options = options || {};
  3760. this.defaultOptions = {
  3761. orientation: 'left', // supported: 'left'
  3762. showMinorLabels: true,
  3763. showMajorLabels: true
  3764. };
  3765. this.range = null;
  3766. this.conversionFactor = 1;
  3767. // create the HTML DOM
  3768. this._create();
  3769. }
  3770. DataAxis.prototype = new Component();
  3771. // TODO: comment options
  3772. DataAxis.prototype.setOptions = Component.prototype.setOptions;
  3773. /**
  3774. * Create the HTML DOM for the DataAxis
  3775. */
  3776. DataAxis.prototype._create = function _create() {
  3777. this.frame = document.createElement('div');
  3778. };
  3779. /**
  3780. * Set a range (start and end)
  3781. * @param {Range | Object} range A Range or an object containing start and end.
  3782. */
  3783. DataAxis.prototype.setRange = function (range) {
  3784. if (!(range instanceof Range) && (!range || range.start === undefined || range.end === undefined)) {
  3785. throw new TypeError('Range must be an instance of Range, ' +
  3786. 'or an object containing start and end.');
  3787. }
  3788. this.range = range;
  3789. };
  3790. /**
  3791. * Get the outer frame of the time axis
  3792. * @return {HTMLElement} frame
  3793. */
  3794. DataAxis.prototype.getFrame = function getFrame() {
  3795. return this.frame;
  3796. };
  3797. /**
  3798. * Repaint the component
  3799. * @return {boolean} Returns true if the component is resized
  3800. */
  3801. DataAxis.prototype.repaint = function () {
  3802. var asSize = util.option.asSize;
  3803. var options = this.options;
  3804. var props = this.props;
  3805. var frame = this.frame;
  3806. // update classname
  3807. frame.className = 'dataaxis'; // TODO: add className from options if defined
  3808. // calculate character width and height
  3809. this._calculateCharSize();
  3810. // TODO: recalculate sizes only needed when parent is resized or options is changed
  3811. var orientation = this.getOption('orientation');
  3812. var showMinorLabels = this.getOption('showMinorLabels');
  3813. var showMajorLabels = this.getOption('showMajorLabels');
  3814. // determine the width and height of the elemens for the axis
  3815. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3816. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3817. this.height = this.options.height;
  3818. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  3819. props.minorLineWidth = this.options.svg.offsetWidth;
  3820. props.minorLineHeight = 1; // TODO: really calculate width
  3821. props.majorLineWidth = this.options.svg.offsetWidth;
  3822. props.majorLineHeight = 1; // TODO: really calculate width
  3823. // take frame offline while updating (is almost twice as fast)
  3824. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  3825. if (orientation == 'left') {
  3826. frame.style.top = '0';
  3827. frame.style.left = '0';
  3828. frame.style.bottom = '';
  3829. frame.style.width = this.width + 'px';
  3830. frame.style.height = this.height + "px";
  3831. }
  3832. else { // right
  3833. frame.style.top = '';
  3834. frame.style.bottom = '0';
  3835. frame.style.left = '0';
  3836. frame.style.width = this.width + 'px';
  3837. frame.style.height = this.height + "px";
  3838. }
  3839. this._repaintLabels();
  3840. };
  3841. /**
  3842. * Repaint major and minor text labels and vertical grid lines
  3843. * @private
  3844. */
  3845. DataAxis.prototype._repaintLabels = function () {
  3846. var orientation = this.getOption('orientation');
  3847. // calculate range and step (step such that we have space for 7 characters per label)
  3848. var start = this.range.start;
  3849. var end = this.range.end;
  3850. var minimumStep = (this.props.minorCharHeight || 10); //in pixels
  3851. var step = new DataStep(start, end, minimumStep, this.options.svg.offsetHeight);
  3852. this.step = step;
  3853. // Move all DOM elements to a "redundant" list, where they
  3854. // can be picked for re-use, and clear the lists with lines and texts.
  3855. // At the end of the function _repaintLabels, left over elements will be cleaned up
  3856. var dom = this.dom;
  3857. dom.redundant.majorLines = dom.majorLines;
  3858. dom.redundant.majorTexts = dom.majorTexts;
  3859. dom.redundant.minorLines = dom.minorLines;
  3860. dom.redundant.minorTexts = dom.minorTexts;
  3861. dom.majorLines = [];
  3862. dom.majorTexts = [];
  3863. dom.minorLines = [];
  3864. dom.minorTexts = [];
  3865. step.first();
  3866. var stepPixels = this.options.svg.offsetHeight / ((step.marginRange / step.step) + 1);
  3867. var xFirstMajorLabel = undefined;
  3868. this.valueAtZero = step.marginEnd;
  3869. var marginStartPos = 0;
  3870. var max = 0;
  3871. while (step.hasNext() && max < 1000) {
  3872. var y = max * stepPixels;
  3873. y = y.toPrecision(5)
  3874. var isMajor = step.isMajor();
  3875. if (this.getOption('showMinorLabels') && isMajor == false) {
  3876. this._repaintMinorText(y, step.getLabelMinor(), orientation);
  3877. }
  3878. if (isMajor && this.getOption('showMajorLabels')) {
  3879. if (y > 0) {
  3880. if (xFirstMajorLabel == undefined) {
  3881. xFirstMajorLabel = y;
  3882. }
  3883. this._repaintMajorText(y, step.getLabelMajor(), orientation);
  3884. }
  3885. this._repaintMajorLine(y, orientation);
  3886. }
  3887. else {
  3888. this._repaintMinorLine(y, orientation);
  3889. }
  3890. step.next();
  3891. marginStartPos = y;
  3892. max++;
  3893. }
  3894. this.conversionFactor = marginStartPos/step.marginRange;
  3895. console.log(marginStartPos, step.marginRange, this.conversionFactor);
  3896. // create a major label on the left when needed
  3897. if (this.getOption('showMajorLabels')) {
  3898. var leftPoint = this._start;
  3899. var leftText = step.getLabelMajor(leftPoint);
  3900. var widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  3901. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3902. this._repaintMajorText(0, leftText, orientation);
  3903. }
  3904. }
  3905. // Cleanup leftover DOM elements from the redundant list
  3906. util.forEach(this.dom.redundant, function (arr) {
  3907. while (arr.length) {
  3908. var elem = arr.pop();
  3909. if (elem && elem.parentNode) {
  3910. elem.parentNode.removeChild(elem);
  3911. }
  3912. }
  3913. });
  3914. };
  3915. DataAxis.prototype.convertValues = function(data) {
  3916. for (var i = 0; i < data.length; i++) {
  3917. data[i].y = this._getPos(data[i].y);
  3918. }
  3919. return data;
  3920. }
  3921. DataAxis.prototype._getPos = function(value) {
  3922. var invertedValue = this.valueAtZero - value;
  3923. var convertedValue = invertedValue * this.conversionFactor;
  3924. return convertedValue
  3925. }
  3926. /**
  3927. * Create a minor label for the axis at position x
  3928. * @param {Number} x
  3929. * @param {String} text
  3930. * @param {String} orientation "top" or "bottom" (default)
  3931. * @private
  3932. */
  3933. DataAxis.prototype._repaintMinorText = function (x, text, orientation) {
  3934. // reuse redundant label
  3935. var label = this.dom.redundant.minorTexts.shift();
  3936. if (!label) {
  3937. // create new label
  3938. var content = document.createTextNode('');
  3939. label = document.createElement('div');
  3940. label.appendChild(content);
  3941. label.className = 'yAxis minor';
  3942. this.frame.appendChild(label);
  3943. }
  3944. this.dom.minorTexts.push(label);
  3945. label.childNodes[0].nodeValue = text;
  3946. if (orientation == 'left') {
  3947. label.style.left = '-2px';
  3948. label.style.textAlign = "right";
  3949. }
  3950. else {
  3951. label.style.left = '2px';
  3952. label.style.textAlign = "left";
  3953. }
  3954. label.style.top = x + 'px';
  3955. //label.title = title; // TODO: this is a heavy operation
  3956. };
  3957. /**
  3958. * Create a Major label for the axis at position x
  3959. * @param {Number} x
  3960. * @param {String} text
  3961. * @param {String} orientation "top" or "bottom" (default)
  3962. * @private
  3963. */
  3964. DataAxis.prototype._repaintMajorText = function (x, text, orientation) {
  3965. // reuse redundant label
  3966. var label = this.dom.redundant.majorTexts.shift();
  3967. if (!label) {
  3968. // create label
  3969. var content = document.createTextNode(text);
  3970. label = document.createElement('div');
  3971. label.className = 'yAxis major';
  3972. label.appendChild(content);
  3973. this.frame.appendChild(label);
  3974. }
  3975. this.dom.majorTexts.push(label);
  3976. label.childNodes[0].nodeValue = text;
  3977. //label.title = title; // TODO: this is a heavy operation
  3978. if (orientation == 'left') {
  3979. label.style.left = '-2px';
  3980. label.style.textAlign = "right";
  3981. }
  3982. else {
  3983. label.style.left = '2';
  3984. label.style.textAlign = "left";
  3985. }
  3986. label.style.top = x + 'px';
  3987. };
  3988. /**
  3989. * Create a minor line for the axis at position y
  3990. * @param {Number} y
  3991. * @param {String} orientation "top" or "bottom" (default)
  3992. * @private
  3993. */
  3994. DataAxis.prototype._repaintMinorLine = function (y, orientation) {
  3995. // reuse redundant line
  3996. var line = this.dom.redundant.minorLines.shift();
  3997. if (!line) {
  3998. // create vertical line
  3999. line = document.createElement('div');
  4000. line.className = 'grid horizontal minor';
  4001. this.frame.appendChild(line);
  4002. }
  4003. this.dom.minorLines.push(line);
  4004. var props = this.props;
  4005. if (orientation == 'left') {
  4006. line.style.left = (this.width - 15) + 'px';
  4007. }
  4008. else {
  4009. line.style.left = -1*(this.width - 15) + 'px';
  4010. }
  4011. line.style.width = props.minorLineWidth + 'px';
  4012. line.style.top = y + 'px';
  4013. };
  4014. /**
  4015. * Create a Major line for the axis at position x
  4016. * @param {Number} x
  4017. * @param {String} orientation "top" or "bottom" (default)
  4018. * @private
  4019. */
  4020. DataAxis.prototype._repaintMajorLine = function (y, orientation) {
  4021. // reuse redundant line
  4022. var line = this.dom.redundant.majorLines.shift();
  4023. if (!line) {
  4024. // create vertical line
  4025. line = document.createElement('div');
  4026. line.className = 'grid horizontal major';
  4027. this.frame.appendChild(line);
  4028. }
  4029. this.dom.majorLines.push(line);
  4030. var props = this.props;
  4031. if (orientation == 'left') {
  4032. line.style.left = (this.width - 25) + 'px';
  4033. }
  4034. else {
  4035. line.style.left = -1*(this.width - 25) + 'px';
  4036. }
  4037. line.style.top = y + 'px';
  4038. line.style.width = props.majorLineWidth + 'px';
  4039. };
  4040. /**
  4041. * Determine the size of text on the axis (both major and minor axis).
  4042. * The size is calculated only once and then cached in this.props.
  4043. * @private
  4044. */
  4045. DataAxis.prototype._calculateCharSize = function () {
  4046. // determine the char width and height on the minor axis
  4047. if (!('minorCharHeight' in this.props)) {
  4048. var textMinor = document.createTextNode('0');
  4049. var measureCharMinor = document.createElement('DIV');
  4050. measureCharMinor.className = 'text minor measure';
  4051. measureCharMinor.appendChild(textMinor);
  4052. this.frame.appendChild(measureCharMinor);
  4053. this.props.minorCharHeight = measureCharMinor.clientHeight;
  4054. this.props.minorCharWidth = measureCharMinor.clientWidth;
  4055. this.frame.removeChild(measureCharMinor);
  4056. }
  4057. if (!('majorCharHeight' in this.props)) {
  4058. var textMajor = document.createTextNode('0');
  4059. var measureCharMajor = document.createElement('DIV');
  4060. measureCharMajor.className = 'text major measure';
  4061. measureCharMajor.appendChild(textMajor);
  4062. this.frame.appendChild(measureCharMajor);
  4063. this.props.majorCharHeight = measureCharMajor.clientHeight;
  4064. this.props.majorCharWidth = measureCharMajor.clientWidth;
  4065. this.frame.removeChild(measureCharMajor);
  4066. }
  4067. };
  4068. /**
  4069. * Snap a date to a rounded value.
  4070. * The snap intervals are dependent on the current scale and step.
  4071. * @param {Date} date the date to be snapped.
  4072. * @return {Date} snappedDate
  4073. */
  4074. DataAxis.prototype.snap = function snap (date) {
  4075. return this.step.snap(date);
  4076. };
  4077. /**
  4078. * A horizontal time axis
  4079. * @param {Object} [options] See TimeAxis.setOptions for the available
  4080. * options.
  4081. * @constructor TimeAxis
  4082. * @extends Component
  4083. */
  4084. function TimeAxis (options) {
  4085. this.id = util.randomUUID();
  4086. this.dom = {
  4087. majorLines: [],
  4088. majorTexts: [],
  4089. minorLines: [],
  4090. minorTexts: [],
  4091. redundant: {
  4092. majorLines: [],
  4093. majorTexts: [],
  4094. minorLines: [],
  4095. minorTexts: []
  4096. }
  4097. };
  4098. this.props = {
  4099. range: {
  4100. start: 0,
  4101. end: 0,
  4102. minimumStep: 0
  4103. },
  4104. lineTop: 0
  4105. };
  4106. this.options = options || {};
  4107. this.defaultOptions = {
  4108. orientation: 'bottom', // supported: 'top', 'bottom'
  4109. // TODO: implement timeaxis orientations 'left' and 'right'
  4110. showMinorLabels: true,
  4111. showMajorLabels: true
  4112. };
  4113. this.range = null;
  4114. // create the HTML DOM
  4115. this._create();
  4116. }
  4117. TimeAxis.prototype = new Component();
  4118. // TODO: comment options
  4119. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  4120. /**
  4121. * Create the HTML DOM for the TimeAxis
  4122. */
  4123. TimeAxis.prototype._create = function _create() {
  4124. this.frame = document.createElement('div');
  4125. };
  4126. /**
  4127. * Set a range (start and end)
  4128. * @param {Range | Object} range A Range or an object containing start and end.
  4129. */
  4130. TimeAxis.prototype.setRange = function (range) {
  4131. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4132. throw new TypeError('Range must be an instance of Range, ' +
  4133. 'or an object containing start and end.');
  4134. }
  4135. this.range = range;
  4136. };
  4137. /**
  4138. * Get the outer frame of the time axis
  4139. * @return {HTMLElement} frame
  4140. */
  4141. TimeAxis.prototype.getFrame = function getFrame() {
  4142. return this.frame;
  4143. };
  4144. /**
  4145. * Repaint the component
  4146. * @return {boolean} Returns true if the component is resized
  4147. */
  4148. TimeAxis.prototype.repaint = function () {
  4149. var asSize = util.option.asSize,
  4150. options = this.options,
  4151. props = this.props,
  4152. frame = this.frame;
  4153. // update classname
  4154. frame.className = 'timeaxis'; // TODO: add className from options if defined
  4155. var parent = frame.parentNode;
  4156. if (parent) {
  4157. // calculate character width and height
  4158. this._calculateCharSize();
  4159. // TODO: recalculate sizes only needed when parent is resized or options is changed
  4160. var orientation = this.getOption('orientation'),
  4161. showMinorLabels = this.getOption('showMinorLabels'),
  4162. showMajorLabels = this.getOption('showMajorLabels');
  4163. // determine the width and height of the elemens for the axis
  4164. var parentHeight = this.parent.height;
  4165. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4166. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4167. this.height = props.minorLabelHeight + props.majorLabelHeight;
  4168. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  4169. props.minorLineHeight = parentHeight + props.minorLabelHeight;
  4170. props.minorLineWidth = 1; // TODO: really calculate width
  4171. props.majorLineHeight = parentHeight + this.height;
  4172. props.majorLineWidth = 1; // TODO: really calculate width
  4173. // take frame offline while updating (is almost twice as fast)
  4174. var beforeChild = frame.nextSibling;
  4175. parent.removeChild(frame);
  4176. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  4177. if (orientation == 'top') {
  4178. frame.style.top = '0';
  4179. frame.style.left = '0';
  4180. frame.style.bottom = '';
  4181. frame.style.width = asSize(options.width, '100%');
  4182. frame.style.height = this.height + 'px';
  4183. }
  4184. else { // bottom
  4185. frame.style.top = '';
  4186. frame.style.bottom = '0';
  4187. frame.style.left = '0';
  4188. frame.style.width = asSize(options.width, '100%');
  4189. frame.style.height = this.height + 'px';
  4190. }
  4191. this._repaintLabels();
  4192. this._repaintLine();
  4193. // put frame online again
  4194. if (beforeChild) {
  4195. parent.insertBefore(frame, beforeChild);
  4196. }
  4197. else {
  4198. parent.appendChild(frame)
  4199. }
  4200. }
  4201. return this._isResized();
  4202. };
  4203. /**
  4204. * Repaint major and minor text labels and vertical grid lines
  4205. * @private
  4206. */
  4207. TimeAxis.prototype._repaintLabels = function () {
  4208. var orientation = this.getOption('orientation');
  4209. // calculate range and step (step such that we have space for 7 characters per label)
  4210. var start = util.convert(this.range.start, 'Number'),
  4211. end = util.convert(this.range.end, 'Number'),
  4212. minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
  4213. -this.options.toTime(0).valueOf();
  4214. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4215. this.step = step;
  4216. // Move all DOM elements to a "redundant" list, where they
  4217. // can be picked for re-use, and clear the lists with lines and texts.
  4218. // At the end of the function _repaintLabels, left over elements will be cleaned up
  4219. var dom = this.dom;
  4220. dom.redundant.majorLines = dom.majorLines;
  4221. dom.redundant.majorTexts = dom.majorTexts;
  4222. dom.redundant.minorLines = dom.minorLines;
  4223. dom.redundant.minorTexts = dom.minorTexts;
  4224. dom.majorLines = [];
  4225. dom.majorTexts = [];
  4226. dom.minorLines = [];
  4227. dom.minorTexts = [];
  4228. step.first();
  4229. var xFirstMajorLabel = undefined;
  4230. var max = 0;
  4231. while (step.hasNext() && max < 1000) {
  4232. max++;
  4233. var cur = step.getCurrent(),
  4234. x = this.options.toScreen(cur),
  4235. isMajor = step.isMajor();
  4236. // TODO: lines must have a width, such that we can create css backgrounds
  4237. if (this.getOption('showMinorLabels')) {
  4238. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  4239. }
  4240. if (isMajor && this.getOption('showMajorLabels')) {
  4241. if (x > 0) {
  4242. if (xFirstMajorLabel == undefined) {
  4243. xFirstMajorLabel = x;
  4244. }
  4245. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  4246. }
  4247. this._repaintMajorLine(x, orientation);
  4248. }
  4249. else {
  4250. this._repaintMinorLine(x, orientation);
  4251. }
  4252. step.next();
  4253. }
  4254. // create a major label on the left when needed
  4255. if (this.getOption('showMajorLabels')) {
  4256. var leftTime = this.options.toTime(0),
  4257. leftText = step.getLabelMajor(leftTime),
  4258. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  4259. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  4260. this._repaintMajorText(0, leftText, orientation);
  4261. }
  4262. }
  4263. // Cleanup leftover DOM elements from the redundant list
  4264. util.forEach(this.dom.redundant, function (arr) {
  4265. while (arr.length) {
  4266. var elem = arr.pop();
  4267. if (elem && elem.parentNode) {
  4268. elem.parentNode.removeChild(elem);
  4269. }
  4270. }
  4271. });
  4272. };
  4273. /**
  4274. * Create a minor label for the axis at position x
  4275. * @param {Number} x
  4276. * @param {String} text
  4277. * @param {String} orientation "top" or "bottom" (default)
  4278. * @private
  4279. */
  4280. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  4281. // reuse redundant label
  4282. var label = this.dom.redundant.minorTexts.shift();
  4283. if (!label) {
  4284. // create new label
  4285. var content = document.createTextNode('');
  4286. label = document.createElement('div');
  4287. label.appendChild(content);
  4288. label.className = 'text minor';
  4289. this.frame.appendChild(label);
  4290. }
  4291. this.dom.minorTexts.push(label);
  4292. label.childNodes[0].nodeValue = text;
  4293. if (orientation == 'top') {
  4294. label.style.top = this.props.majorLabelHeight + 'px';
  4295. label.style.bottom = '';
  4296. }
  4297. else {
  4298. label.style.top = '';
  4299. label.style.bottom = this.props.majorLabelHeight + 'px';
  4300. }
  4301. label.style.left = x + 'px';
  4302. //label.title = title; // TODO: this is a heavy operation
  4303. };
  4304. /**
  4305. * Create a Major label for the axis at position x
  4306. * @param {Number} x
  4307. * @param {String} text
  4308. * @param {String} orientation "top" or "bottom" (default)
  4309. * @private
  4310. */
  4311. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  4312. // reuse redundant label
  4313. var label = this.dom.redundant.majorTexts.shift();
  4314. if (!label) {
  4315. // create label
  4316. var content = document.createTextNode(text);
  4317. label = document.createElement('div');
  4318. label.className = 'text major';
  4319. label.appendChild(content);
  4320. this.frame.appendChild(label);
  4321. }
  4322. this.dom.majorTexts.push(label);
  4323. label.childNodes[0].nodeValue = text;
  4324. //label.title = title; // TODO: this is a heavy operation
  4325. if (orientation == 'top') {
  4326. label.style.top = '0px';
  4327. label.style.bottom = '';
  4328. }
  4329. else {
  4330. label.style.top = '';
  4331. label.style.bottom = '0px';
  4332. }
  4333. label.style.left = x + 'px';
  4334. };
  4335. /**
  4336. * Create a minor line for the axis at position x
  4337. * @param {Number} x
  4338. * @param {String} orientation "top" or "bottom" (default)
  4339. * @private
  4340. */
  4341. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  4342. // reuse redundant line
  4343. var line = this.dom.redundant.minorLines.shift();
  4344. if (!line) {
  4345. // create vertical line
  4346. line = document.createElement('div');
  4347. line.className = 'grid vertical minor';
  4348. this.frame.appendChild(line);
  4349. }
  4350. this.dom.minorLines.push(line);
  4351. var props = this.props;
  4352. if (orientation == 'top') {
  4353. line.style.top = this.props.majorLabelHeight + 'px';
  4354. line.style.bottom = '';
  4355. }
  4356. else {
  4357. line.style.top = '';
  4358. line.style.bottom = this.props.majorLabelHeight + 'px';
  4359. }
  4360. line.style.height = props.minorLineHeight + 'px';
  4361. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4362. };
  4363. /**
  4364. * Create a Major line for the axis at position x
  4365. * @param {Number} x
  4366. * @param {String} orientation "top" or "bottom" (default)
  4367. * @private
  4368. */
  4369. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  4370. // reuse redundant line
  4371. var line = this.dom.redundant.majorLines.shift();
  4372. if (!line) {
  4373. // create vertical line
  4374. line = document.createElement('DIV');
  4375. line.className = 'grid vertical major';
  4376. this.frame.appendChild(line);
  4377. }
  4378. this.dom.majorLines.push(line);
  4379. var props = this.props;
  4380. if (orientation == 'top') {
  4381. line.style.top = '0px';
  4382. line.style.bottom = '';
  4383. }
  4384. else {
  4385. line.style.top = '';
  4386. line.style.bottom = '0px';
  4387. }
  4388. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4389. line.style.height = props.majorLineHeight + 'px';
  4390. };
  4391. /**
  4392. * Repaint the horizontal line for the axis
  4393. * @private
  4394. */
  4395. TimeAxis.prototype._repaintLine = function() {
  4396. var line = this.dom.line,
  4397. frame = this.frame,
  4398. orientation = this.getOption('orientation');
  4399. // line before all axis elements
  4400. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4401. if (line) {
  4402. // put this line at the end of all childs
  4403. frame.removeChild(line);
  4404. frame.appendChild(line);
  4405. }
  4406. else {
  4407. // create the axis line
  4408. line = document.createElement('div');
  4409. line.className = 'grid horizontal major';
  4410. frame.appendChild(line);
  4411. this.dom.line = line;
  4412. }
  4413. if (orientation == 'top') {
  4414. line.style.top = this.height + 'px';
  4415. line.style.bottom = '';
  4416. }
  4417. else {
  4418. line.style.top = '';
  4419. line.style.bottom = this.height + 'px';
  4420. }
  4421. }
  4422. else {
  4423. if (line && line.parentNode) {
  4424. line.parentNode.removeChild(line);
  4425. delete this.dom.line;
  4426. }
  4427. }
  4428. };
  4429. /**
  4430. * Determine the size of text on the axis (both major and minor axis).
  4431. * The size is calculated only once and then cached in this.props.
  4432. * @private
  4433. */
  4434. TimeAxis.prototype._calculateCharSize = function () {
  4435. // Note: We only calculate char size once, but in case it is calculated as zero,
  4436. // we will recalculate. This is the case if any of the timelines parents
  4437. // has display:none for example.
  4438. // determine the char width and height on the minor axis
  4439. if (!('minorCharHeight' in this.props) || this.props.minorCharHeight == 0) {
  4440. var textMinor = document.createTextNode('0');
  4441. var measureCharMinor = document.createElement('DIV');
  4442. measureCharMinor.className = 'text minor measure';
  4443. measureCharMinor.appendChild(textMinor);
  4444. this.frame.appendChild(measureCharMinor);
  4445. this.props.minorCharHeight = measureCharMinor.clientHeight;
  4446. this.props.minorCharWidth = measureCharMinor.clientWidth;
  4447. this.frame.removeChild(measureCharMinor);
  4448. }
  4449. // determine the char width and height on the major axis
  4450. if (!('majorCharHeight' in this.props) || this.props.majorCharHeight == 0) {
  4451. var textMajor = document.createTextNode('0');
  4452. var measureCharMajor = document.createElement('DIV');
  4453. measureCharMajor.className = 'text major measure';
  4454. measureCharMajor.appendChild(textMajor);
  4455. this.frame.appendChild(measureCharMajor);
  4456. this.props.majorCharHeight = measureCharMajor.clientHeight;
  4457. this.props.majorCharWidth = measureCharMajor.clientWidth;
  4458. this.frame.removeChild(measureCharMajor);
  4459. }
  4460. };
  4461. /**
  4462. * Snap a date to a rounded value.
  4463. * The snap intervals are dependent on the current scale and step.
  4464. * @param {Date} date the date to be snapped.
  4465. * @return {Date} snappedDate
  4466. */
  4467. TimeAxis.prototype.snap = function snap (date) {
  4468. return this.step.snap(date);
  4469. };
  4470. /**
  4471. * A current time bar
  4472. * @param {Range} range
  4473. * @param {Object} [options] Available parameters:
  4474. * {Boolean} [showCurrentTime]
  4475. * @constructor CurrentTime
  4476. * @extends Component
  4477. */
  4478. function CurrentTime (range, options) {
  4479. this.id = util.randomUUID();
  4480. this.range = range;
  4481. this.options = options || {};
  4482. this.defaultOptions = {
  4483. showCurrentTime: false
  4484. };
  4485. this._create();
  4486. }
  4487. CurrentTime.prototype = new Component();
  4488. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4489. /**
  4490. * Create the HTML DOM for the current time bar
  4491. * @private
  4492. */
  4493. CurrentTime.prototype._create = function _create () {
  4494. var bar = document.createElement('div');
  4495. bar.className = 'currenttime';
  4496. bar.style.position = 'absolute';
  4497. bar.style.top = '0px';
  4498. bar.style.height = '100%';
  4499. this.bar = bar;
  4500. };
  4501. /**
  4502. * Get the frame element of the current time bar
  4503. * @returns {HTMLElement} frame
  4504. */
  4505. CurrentTime.prototype.getFrame = function getFrame() {
  4506. return this.bar;
  4507. };
  4508. /**
  4509. * Repaint the component
  4510. * @return {boolean} Returns true if the component is resized
  4511. */
  4512. CurrentTime.prototype.repaint = function repaint() {
  4513. var parent = this.parent;
  4514. var now = new Date();
  4515. var x = this.options.toScreen(now);
  4516. this.bar.style.left = x + 'px';
  4517. this.bar.title = 'Current time: ' + now;
  4518. return false;
  4519. };
  4520. /**
  4521. * Start auto refreshing the current time bar
  4522. */
  4523. CurrentTime.prototype.start = function start() {
  4524. var me = this;
  4525. function update () {
  4526. me.stop();
  4527. // determine interval to refresh
  4528. var scale = me.range.conversion(me.parent.width).scale;
  4529. var interval = 1 / scale / 10;
  4530. if (interval < 30) interval = 30;
  4531. if (interval > 1000) interval = 1000;
  4532. me.repaint();
  4533. // start a timer to adjust for the new time
  4534. me.currentTimeTimer = setTimeout(update, interval);
  4535. }
  4536. update();
  4537. };
  4538. /**
  4539. * Stop auto refreshing the current time bar
  4540. */
  4541. CurrentTime.prototype.stop = function stop() {
  4542. if (this.currentTimeTimer !== undefined) {
  4543. clearTimeout(this.currentTimeTimer);
  4544. delete this.currentTimeTimer;
  4545. }
  4546. };
  4547. /**
  4548. * A custom time bar
  4549. * @param {Object} [options] Available parameters:
  4550. * {Boolean} [showCustomTime]
  4551. * @constructor CustomTime
  4552. * @extends Component
  4553. */
  4554. function CustomTime (options) {
  4555. this.id = util.randomUUID();
  4556. this.options = options || {};
  4557. this.defaultOptions = {
  4558. showCustomTime: false
  4559. };
  4560. this.customTime = new Date();
  4561. this.eventParams = {}; // stores state parameters while dragging the bar
  4562. // create the DOM
  4563. this._create();
  4564. }
  4565. CustomTime.prototype = new Component();
  4566. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4567. /**
  4568. * Create the DOM for the custom time
  4569. * @private
  4570. */
  4571. CustomTime.prototype._create = function _create () {
  4572. var bar = document.createElement('div');
  4573. bar.className = 'customtime';
  4574. bar.style.position = 'absolute';
  4575. bar.style.top = '0px';
  4576. bar.style.height = '100%';
  4577. this.bar = bar;
  4578. var drag = document.createElement('div');
  4579. drag.style.position = 'relative';
  4580. drag.style.top = '0px';
  4581. drag.style.left = '-10px';
  4582. drag.style.height = '100%';
  4583. drag.style.width = '20px';
  4584. bar.appendChild(drag);
  4585. // attach event listeners
  4586. this.hammer = Hammer(bar, {
  4587. prevent_default: true
  4588. });
  4589. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4590. this.hammer.on('drag', this._onDrag.bind(this));
  4591. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4592. };
  4593. /**
  4594. * Get the frame element of the custom time bar
  4595. * @returns {HTMLElement} frame
  4596. */
  4597. CustomTime.prototype.getFrame = function getFrame() {
  4598. return this.bar;
  4599. };
  4600. /**
  4601. * Repaint the component
  4602. * @return {boolean} Returns true if the component is resized
  4603. */
  4604. CustomTime.prototype.repaint = function () {
  4605. var x = this.options.toScreen(this.customTime);
  4606. this.bar.style.left = x + 'px';
  4607. this.bar.title = 'Time: ' + this.customTime;
  4608. return false;
  4609. };
  4610. /**
  4611. * Set custom time.
  4612. * @param {Date} time
  4613. */
  4614. CustomTime.prototype.setCustomTime = function(time) {
  4615. this.customTime = new Date(time.valueOf());
  4616. this.repaint();
  4617. };
  4618. /**
  4619. * Retrieve the current custom time.
  4620. * @return {Date} customTime
  4621. */
  4622. CustomTime.prototype.getCustomTime = function() {
  4623. return new Date(this.customTime.valueOf());
  4624. };
  4625. /**
  4626. * Start moving horizontally
  4627. * @param {Event} event
  4628. * @private
  4629. */
  4630. CustomTime.prototype._onDragStart = function(event) {
  4631. this.eventParams.dragging = true;
  4632. this.eventParams.customTime = this.customTime;
  4633. event.stopPropagation();
  4634. event.preventDefault();
  4635. };
  4636. /**
  4637. * Perform moving operating.
  4638. * @param {Event} event
  4639. * @private
  4640. */
  4641. CustomTime.prototype._onDrag = function (event) {
  4642. if (!this.eventParams.dragging) return;
  4643. var deltaX = event.gesture.deltaX,
  4644. x = this.options.toScreen(this.eventParams.customTime) + deltaX,
  4645. time = this.options.toTime(x);
  4646. this.setCustomTime(time);
  4647. // fire a timechange event
  4648. this.emit('timechange', {
  4649. time: new Date(this.customTime.valueOf())
  4650. });
  4651. event.stopPropagation();
  4652. event.preventDefault();
  4653. };
  4654. /**
  4655. * Stop moving operating.
  4656. * @param {event} event
  4657. * @private
  4658. */
  4659. CustomTime.prototype._onDragEnd = function (event) {
  4660. if (!this.eventParams.dragging) return;
  4661. // fire a timechanged event
  4662. this.emit('timechanged', {
  4663. time: new Date(this.customTime.valueOf())
  4664. });
  4665. event.stopPropagation();
  4666. event.preventDefault();
  4667. };
  4668. /**
  4669. * Created by Alex on 5/6/14.
  4670. */
  4671. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  4672. /**
  4673. * An ItemSet holds a set of items and ranges which can be displayed in a
  4674. * range. The width is determined by the parent of the ItemSet, and the height
  4675. * is determined by the size of the items.
  4676. * @param {Panel} backgroundPanel Panel which can be used to display the
  4677. * vertical lines of box items.
  4678. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  4679. * can be displayed.
  4680. * @param {Panel} sidePanel Left side panel holding labels
  4681. * @param {Object} [options] See ItemSet.setOptions for the available options.
  4682. * @constructor ItemSet
  4683. * @extends Panel
  4684. */
  4685. function Linegraph(backgroundPanel, axisPanel, sidePanel, options, timeline, sidePanelParent) {
  4686. this.id = util.randomUUID();
  4687. this.timeline = timeline;
  4688. // one options object is shared by this itemset and all its items
  4689. this.options = options || {};
  4690. this.backgroundPanel = backgroundPanel;
  4691. this.axisPanel = axisPanel;
  4692. this.sidePanel = sidePanel;
  4693. this.sidePanelParent = sidePanelParent;
  4694. this.itemOptions = Object.create(this.options);
  4695. this.dom = {};
  4696. this.hammer = null;
  4697. this.itemsData = null; // DataSet
  4698. this.groupsData = null; // DataSet
  4699. this.range = null; // Range or Object {start: number, end: number}
  4700. // listeners for the DataSet of the items
  4701. // this.itemListeners = {
  4702. // 'add': function(event, params, senderId) {
  4703. // if (senderId != me.id) me._onAdd(params.items);
  4704. // },
  4705. // 'update': function(event, params, senderId) {
  4706. // if (senderId != me.id) me._onUpdate(params.items);
  4707. // },
  4708. // 'remove': function(event, params, senderId) {
  4709. // if (senderId != me.id) me._onRemove(params.items);
  4710. // }
  4711. // };
  4712. //
  4713. // // listeners for the DataSet of the groups
  4714. // this.groupListeners = {
  4715. // 'add': function(event, params, senderId) {
  4716. // if (senderId != me.id) me._onAddGroups(params.items);
  4717. // },
  4718. // 'update': function(event, params, senderId) {
  4719. // if (senderId != me.id) me._onUpdateGroups(params.items);
  4720. // },
  4721. // 'remove': function(event, params, senderId) {
  4722. // if (senderId != me.id) me._onRemoveGroups(params.items);
  4723. // }
  4724. // };
  4725. this.items = {}; // object with an Item for every data item
  4726. this.groups = {}; // Group object for every group
  4727. this.groupIds = [];
  4728. this.selection = []; // list with the ids of all selected nodes
  4729. this.stackDirty = true; // if true, all items will be restacked on next repaint
  4730. this.touchParams = {}; // stores properties while dragging
  4731. // create the HTML DOM
  4732. this.lastStart = 0;
  4733. this._create();
  4734. var me = this;
  4735. this.timeline.on("rangechange", function() {
  4736. if (me.lastStart != 0) {
  4737. var offset = me.range.start - me.lastStart;
  4738. var range = me.range.end - me.range.start;
  4739. if (me.width != 0) {
  4740. var rangePerPixelInv = me.width/range;
  4741. var xOffset = offset * rangePerPixelInv;
  4742. me.svg.style.left = util.option.asSize(-me.width - xOffset);
  4743. }
  4744. }
  4745. })
  4746. this.timeline.on("rangechanged", function() {
  4747. me.lastStart = me.range.start;
  4748. me.svg.style.left = util.option.asSize(-me.width);
  4749. me.setData.apply(me);
  4750. });
  4751. // this.data = new DataView(this.items)
  4752. }
  4753. Linegraph.prototype = new Panel();
  4754. /**
  4755. * Create the HTML DOM for the ItemSet
  4756. */
  4757. Linegraph.prototype._create = function(){
  4758. var frame = document.createElement('div');
  4759. frame['timeline-linegraph'] = this;
  4760. this.frame = frame;
  4761. this.frame.className = 'itemset';
  4762. // create background panel
  4763. var background = document.createElement('div');
  4764. background.className = 'background';
  4765. this.backgroundPanel.frame.appendChild(background);
  4766. this.dom.background = background;
  4767. // create foreground panel
  4768. var foreground = document.createElement('div');
  4769. foreground.className = 'foreground';
  4770. frame.appendChild(foreground);
  4771. this.dom.foreground = foreground;
  4772. // // create axis panel
  4773. // var axis = document.createElement('div');
  4774. // axis.className = 'axis';
  4775. // this.dom.axis = axis;
  4776. // this.axisPanel.frame.appendChild(axis);
  4777. //
  4778. // // create labelset
  4779. // var labelSet = document.createElement('div');
  4780. // labelSet.className = 'labelset';
  4781. // this.dom.labelSet = labelSet;
  4782. // this.sidePanel.frame.appendChild(labelSet);
  4783. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  4784. this.svg.style.position = "relative"
  4785. this.svg.style.height = "300px";
  4786. this.svg.style.display = "block";
  4787. this.path = document.createElementNS('http://www.w3.org/2000/svg',"path");
  4788. this.path.setAttributeNS(null, "fill","none");
  4789. this.path.setAttributeNS(null, "stroke","blue");
  4790. this.path.setAttributeNS(null, "stroke-width","1");
  4791. this.path2 = document.createElementNS('http://www.w3.org/2000/svg',"path");
  4792. this.path2.setAttributeNS(null, "fill","none");
  4793. this.path2.setAttributeNS(null, "stroke","red");
  4794. this.path2.setAttributeNS(null, "stroke-width","1");
  4795. this.path3 = document.createElementNS('http://www.w3.org/2000/svg',"path");
  4796. this.path3.setAttributeNS(null, "fill","none");
  4797. this.path3.setAttributeNS(null, "stroke","green");
  4798. this.path3.setAttributeNS(null, "stroke-width","1");
  4799. this.dom.foreground.appendChild(this.svg);
  4800. this.svg.appendChild(this.path3);
  4801. this.svg.appendChild(this.path2);
  4802. this.svg.appendChild(this.path);
  4803. // this.yAxisDiv = document.createElement('div');
  4804. // this.yAxisDiv.style.backgroundColor = 'rgb(220,220,220)';
  4805. // this.yAxisDiv.style.width = '100px';
  4806. // this.yAxisDiv.style.height = this.svg.style.height;
  4807. this._createAxis();
  4808. // this.dom.yAxisDiv = this.yAxisDiv;
  4809. // this.sidePanel.frame.appendChild(this.yAxisDiv);
  4810. this.sidePanel.showPanel.apply(this.sidePanel);
  4811. this.sidePanelParent.showPanel();
  4812. };
  4813. Linegraph.prototype._createAxis = function() {
  4814. // panel with time axis
  4815. var dataAxisOptions = {
  4816. range: this.range,
  4817. left: null,
  4818. top: null,
  4819. width: null,
  4820. height: 300,
  4821. svg: this.svg
  4822. };
  4823. this.yAxis = new DataAxis(dataAxisOptions);
  4824. this.sidePanel.frame.appendChild(this.yAxis.getFrame());
  4825. }
  4826. Linegraph.prototype.setData = function() {
  4827. if (this.width != 0) {
  4828. var dataview = new DataView(this.timeline.itemsData,
  4829. {filter: function (item) {return (item.value);}})
  4830. var datapoints = dataview.get();
  4831. if (datapoints != null) {
  4832. if (datapoints.length > 0) {
  4833. var dataset = this._extractData(datapoints);
  4834. var data = dataset.data;
  4835. console.log("height",data,datapoints, dataset);
  4836. this.yAxis.setRange({start:dataset.range.low,end:dataset.range.high});
  4837. this.yAxis.repaint();
  4838. data = this.yAxis.convertValues(data);
  4839. var d, d2, d3;
  4840. d = this._catmullRom(data,0.5);
  4841. d3 = this._catmullRom(data,0);
  4842. d2 = this._catmullRom(data,1);
  4843. // var data2 = [];
  4844. // this.startTime = this.range.start;
  4845. // var min = Date.now() - 3600000 * 24 * 30;
  4846. // var max = Date.now() + 3600000 * 24 * 10;
  4847. // var count = 60;
  4848. // var step = (max-min) / count;
  4849. //
  4850. // var range = this.range.end - this.range.start;
  4851. // var rangePerPixel = range/this.width;
  4852. // var rangePerPixelInv = this.width/range;
  4853. // var xOffset = -this.range.start + this.width*rangePerPixel;
  4854. //
  4855. // for (var i = 0; i < count; i++) {
  4856. // data2.push({x:(min + i*step + xOffset) * rangePerPixelInv, y: 250*(i%2) + 25})
  4857. // }
  4858. //
  4859. // var d2 = this._catmullRom(data2);
  4860. this.path.setAttributeNS(null, "d",d);
  4861. this.path2.setAttributeNS(null, "d",d2);
  4862. this.path3.setAttributeNS(null, "d",d3);
  4863. }
  4864. }
  4865. }
  4866. }
  4867. /**
  4868. * Set options for the Linegraph. Existing options will be extended/overwritten.
  4869. * @param {Object} [options] The following options are available:
  4870. * {String | function} [className]
  4871. * class name for the itemset
  4872. * {String} [type]
  4873. * Default type for the items. Choose from 'box'
  4874. * (default), 'point', or 'range'. The default
  4875. * Style can be overwritten by individual items.
  4876. * {String} align
  4877. * Alignment for the items, only applicable for
  4878. * ItemBox. Choose 'center' (default), 'left', or
  4879. * 'right'.
  4880. * {String} orientation
  4881. * Orientation of the item set. Choose 'top' or
  4882. * 'bottom' (default).
  4883. * {Number} margin.axis
  4884. * Margin between the axis and the items in pixels.
  4885. * Default is 20.
  4886. * {Number} margin.item
  4887. * Margin between items in pixels. Default is 10.
  4888. * {Number} padding
  4889. * Padding of the contents of an item in pixels.
  4890. * Must correspond with the items css. Default is 5.
  4891. * {Function} snap
  4892. * Function to let items snap to nice dates when
  4893. * dragging items.
  4894. */
  4895. Linegraph.prototype.setOptions = function(options) {
  4896. Component.prototype.setOptions.call(this, options);
  4897. };
  4898. Linegraph.prototype._extractData = function(dataset) {
  4899. var extractedData = [];
  4900. var low = dataset[0].value;
  4901. var high = dataset[0].value;
  4902. var range = this.range.end - this.range.start;
  4903. var rangePerPixel = range/this.width;
  4904. var rangePerPixelInv = this.width/range;
  4905. var xOffset = -this.range.start + this.width*rangePerPixel;
  4906. for (var i = 0; i < dataset.length; i++) {
  4907. var val = new Date(dataset[i].start).getTime();
  4908. val += xOffset;
  4909. val *= rangePerPixelInv;
  4910. extractedData.push({x:val, y:dataset[i].value});
  4911. if (low > dataset[i].value) {
  4912. low = dataset[i].value;
  4913. }
  4914. if (high < dataset[i].value) {
  4915. high = dataset[i].value;
  4916. }
  4917. }
  4918. //extractedData.sort(function (a,b) {return a.x - b.x;})
  4919. return {range:{low:low,high:high},data:extractedData};
  4920. }
  4921. Linegraph.prototype._catmullRomUniform = function(data) {
  4922. // catmull rom
  4923. var p0, p1, p2, p3, bp1, bp2
  4924. var d = "M" + Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4925. var normalization = 1/6;
  4926. var length = data.length;
  4927. for (var i = 0; i < length - 1; i++) {
  4928. p0 = (i == 0) ? data[0] : data[i-1];
  4929. p1 = data[i];
  4930. p2 = data[i+1];
  4931. p3 = (i + 2 < length) ? data[i+2] : p2;
  4932. // Catmull-Rom to Cubic Bezier conversion matrix
  4933. // 0 1 0 0
  4934. // -1/6 1 1/6 0
  4935. // 0 1/6 1 -1/6
  4936. // 0 0 1 0
  4937. // bp0 = { x: p1.x, y: p1.y };
  4938. bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
  4939. bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
  4940. // bp0 = { x: p2.x, y: p2.y };
  4941. d += "C" +
  4942. Math.round(bp1.x) + "," +
  4943. Math.round(bp1.y) + " " +
  4944. Math.round(bp2.x) + "," +
  4945. Math.round(bp2.y) + " " +
  4946. Math.round(p2.x) + "," +
  4947. Math.round(p2.y) + " ";
  4948. }
  4949. return d;
  4950. };
  4951. Linegraph.prototype._catmullRom = function(data, alpha) {
  4952. if (alpha == 0 || alpha === undefined) {
  4953. return this._catmullRomUniform(data);
  4954. }
  4955. else {
  4956. var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
  4957. var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
  4958. var d = "M" + Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  4959. var length = data.length;
  4960. for (var i = 0; i < length - 1; i++) {
  4961. p0 = (i == 0) ? data[0] : data[i-1];
  4962. p1 = data[i];
  4963. p2 = data[i+1];
  4964. p3 = (i + 2 < length) ? data[i+2] : p2;
  4965. d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
  4966. d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
  4967. d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
  4968. // Catmull-Rom to Cubic Bezier conversion matrix
  4969. //
  4970. // A = 2d1^2a + 3d1^a * d2^a + d3^2a
  4971. // B = 2d3^2a + 3d3^a * d2^a + d2^2a
  4972. //
  4973. // [ 0 1 0 0 ]
  4974. // [ -d2^2a/N A/N d1^2a/N 0 ]
  4975. // [ 0 d3^2a/M B/M -d2^2a/M ]
  4976. // [ 0 0 1 0 ]
  4977. // [ 0 1 0 0 ]
  4978. // [ -d2pow2a/N A/N d1pow2a/N 0 ]
  4979. // [ 0 d3pow2a/M B/M -d2pow2a/M ]
  4980. // [ 0 0 1 0 ]
  4981. d3powA = Math.pow(d3, alpha);
  4982. d3pow2A = Math.pow(d3,2*alpha);
  4983. d2powA = Math.pow(d2, alpha);
  4984. d2pow2A = Math.pow(d2,2*alpha);
  4985. d1powA = Math.pow(d1, alpha);
  4986. d1pow2A = Math.pow(d1,2*alpha);
  4987. A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
  4988. B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
  4989. N = 3*d1powA * (d1powA + d2powA);
  4990. if (N > 0) {N = 1 / N;}
  4991. M = 3*d3powA * (d3powA + d2powA);
  4992. if (M > 0) {M = 1 / M;}
  4993. bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
  4994. y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
  4995. bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
  4996. y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
  4997. if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
  4998. if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
  4999. d += "C" +
  5000. Math.round(bp1.x) + "," +
  5001. Math.round(bp1.y) + " " +
  5002. Math.round(bp2.x) + "," +
  5003. Math.round(bp2.y) + " " +
  5004. Math.round(p2.x) + "," +
  5005. Math.round(p2.y) + " ";
  5006. }
  5007. return d;
  5008. }
  5009. };
  5010. Linegraph.prototype._linear = function(data) {
  5011. // linear
  5012. var d = "";
  5013. for (var i = 0; i < data.length; i++) {
  5014. if (i == 0) {
  5015. d += "M" + data[i].x + "," + data[i].y;
  5016. }
  5017. else {
  5018. d += " " + data[i].x + "," + data[i].y;
  5019. }
  5020. }
  5021. return d;
  5022. }
  5023. /**
  5024. * Set range (start and end).
  5025. * @param {Range | Object} range A Range or an object containing start and end.
  5026. */
  5027. Linegraph.prototype.setRange = function(range) {
  5028. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  5029. throw new TypeError('Range must be an instance of Range, ' +
  5030. 'or an object containing start and end.');
  5031. }
  5032. this.range = range;
  5033. };
  5034. Linegraph.prototype.repaint = function() {
  5035. var margin = this.options.margin,
  5036. range = this.range,
  5037. asSize = util.option.asSize,
  5038. asString = util.option.asString,
  5039. options = this.options,
  5040. orientation = this.getOption('orientation'),
  5041. resized = false,
  5042. frame = this.frame;
  5043. // TODO: document this feature to specify one margin for both item and axis distance
  5044. if (typeof margin === 'number') {
  5045. margin = {
  5046. item: margin,
  5047. axis: margin
  5048. };
  5049. }
  5050. // update className
  5051. this.frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  5052. // check whether zoomed (in that case we need to re-stack everything)
  5053. // TODO: would be nicer to get this as a trigger from Range
  5054. var visibleInterval = this.range.end - this.range.start;
  5055. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  5056. if (zoomed) this.stackDirty = true;
  5057. this.lastVisibleInterval = visibleInterval;
  5058. this.lastWidth = this.width;
  5059. // reposition frame
  5060. this.frame.style.left = asSize(options.left, '');
  5061. this.frame.style.right = asSize(options.right, '');
  5062. this.frame.style.top = asSize((orientation == 'top') ? '0' : '');
  5063. this.frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  5064. this.frame.style.width = asSize(options.width, '100%');
  5065. // frame.style.height = asSize(height);
  5066. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  5067. // calculate actual size and position
  5068. this.top = this.frame.offsetTop;
  5069. this.left = this.frame.offsetLeft;
  5070. this.width = this.frame.offsetWidth;
  5071. // this.height = height;
  5072. // check if this component is resized
  5073. resized = this._isResized() || resized;
  5074. if (resized) {
  5075. this.svg.style.width = asSize(3*this.width);
  5076. this.svg.style.left = asSize(-this.width);
  5077. }
  5078. if (zoomed) {
  5079. this.setData();
  5080. }
  5081. }
  5082. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  5083. /**
  5084. * An ItemSet holds a set of items and ranges which can be displayed in a
  5085. * range. The width is determined by the parent of the ItemSet, and the height
  5086. * is determined by the size of the items.
  5087. * @param {Panel} backgroundPanel Panel which can be used to display the
  5088. * vertical lines of box items.
  5089. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  5090. * can be displayed.
  5091. * @param {Panel} sidePanel Left side panel holding labels
  5092. * @param {Object} [options] See ItemSet.setOptions for the available options.
  5093. * @constructor ItemSet
  5094. * @extends Panel
  5095. */
  5096. function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
  5097. this.id = util.randomUUID();
  5098. // one options object is shared by this itemset and all its items
  5099. this.options = options || {};
  5100. this.backgroundPanel = backgroundPanel;
  5101. this.axisPanel = axisPanel;
  5102. this.sidePanel = sidePanel;
  5103. this.itemOptions = Object.create(this.options);
  5104. this.dom = {};
  5105. this.hammer = null;
  5106. var me = this;
  5107. this.itemsData = null; // DataSet
  5108. this.groupsData = null; // DataSet
  5109. this.range = null; // Range or Object {start: number, end: number}
  5110. // listeners for the DataSet of the items
  5111. this.itemListeners = {
  5112. 'add': function (event, params, senderId) {
  5113. if (senderId != me.id) me._onAdd(params.items);
  5114. },
  5115. 'update': function (event, params, senderId) {
  5116. if (senderId != me.id) me._onUpdate(params.items);
  5117. },
  5118. 'remove': function (event, params, senderId) {
  5119. if (senderId != me.id) me._onRemove(params.items);
  5120. }
  5121. };
  5122. // listeners for the DataSet of the groups
  5123. this.groupListeners = {
  5124. 'add': function (event, params, senderId) {
  5125. if (senderId != me.id) me._onAddGroups(params.items);
  5126. },
  5127. 'update': function (event, params, senderId) {
  5128. if (senderId != me.id) me._onUpdateGroups(params.items);
  5129. },
  5130. 'remove': function (event, params, senderId) {
  5131. if (senderId != me.id) me._onRemoveGroups(params.items);
  5132. }
  5133. };
  5134. this.items = {}; // object with an Item for every data item
  5135. this.groups = {}; // Group object for every group
  5136. this.groupIds = [];
  5137. this.selection = []; // list with the ids of all selected nodes
  5138. this.stackDirty = true; // if true, all items will be restacked on next repaint
  5139. this.touchParams = {}; // stores properties while dragging
  5140. // create the HTML DOM
  5141. this._create();
  5142. }
  5143. ItemSet.prototype = new Panel();
  5144. // available item types will be registered here
  5145. ItemSet.types = {
  5146. box: ItemBox,
  5147. range: ItemRange,
  5148. rangeoverflow: ItemRangeOverflow,
  5149. point: ItemPoint
  5150. };
  5151. /**
  5152. * Create the HTML DOM for the ItemSet
  5153. */
  5154. ItemSet.prototype._create = function _create(){
  5155. var frame = document.createElement('div');
  5156. frame['timeline-itemset'] = this;
  5157. this.frame = frame;
  5158. // create background panel
  5159. var background = document.createElement('div');
  5160. background.className = 'background';
  5161. this.backgroundPanel.frame.appendChild(background);
  5162. this.dom.background = background;
  5163. // create foreground panel
  5164. var foreground = document.createElement('div');
  5165. foreground.className = 'foreground';
  5166. frame.appendChild(foreground);
  5167. this.dom.foreground = foreground;
  5168. // create axis panel
  5169. var axis = document.createElement('div');
  5170. axis.className = 'axis';
  5171. this.dom.axis = axis;
  5172. this.axisPanel.frame.appendChild(axis);
  5173. // create labelset
  5174. var labelSet = document.createElement('div');
  5175. labelSet.className = 'labelset';
  5176. this.dom.labelSet = labelSet;
  5177. this.sidePanel.frame.appendChild(labelSet);
  5178. // create ungrouped Group
  5179. this._updateUngrouped();
  5180. // attach event listeners
  5181. // TODO: use event listeners from the rootpanel to improve performance?
  5182. this.hammer = Hammer(frame, {
  5183. prevent_default: true
  5184. });
  5185. this.hammer.on('dragstart', this._onDragStart.bind(this));
  5186. this.hammer.on('drag', this._onDrag.bind(this));
  5187. this.hammer.on('dragend', this._onDragEnd.bind(this));
  5188. };
  5189. /**
  5190. * Set options for the ItemSet. Existing options will be extended/overwritten.
  5191. * @param {Object} [options] The following options are available:
  5192. * {String | function} [className]
  5193. * class name for the itemset
  5194. * {String} [type]
  5195. * Default type for the items. Choose from 'box'
  5196. * (default), 'point', or 'range'. The default
  5197. * Style can be overwritten by individual items.
  5198. * {String} align
  5199. * Alignment for the items, only applicable for
  5200. * ItemBox. Choose 'center' (default), 'left', or
  5201. * 'right'.
  5202. * {String} orientation
  5203. * Orientation of the item set. Choose 'top' or
  5204. * 'bottom' (default).
  5205. * {Number} margin.axis
  5206. * Margin between the axis and the items in pixels.
  5207. * Default is 20.
  5208. * {Number} margin.item
  5209. * Margin between items in pixels. Default is 10.
  5210. * {Number} padding
  5211. * Padding of the contents of an item in pixels.
  5212. * Must correspond with the items css. Default is 5.
  5213. * {Function} snap
  5214. * Function to let items snap to nice dates when
  5215. * dragging items.
  5216. */
  5217. ItemSet.prototype.setOptions = function setOptions(options) {
  5218. Component.prototype.setOptions.call(this, options);
  5219. };
  5220. /**
  5221. * Mark the ItemSet dirty so it will refresh everything with next repaint
  5222. */
  5223. ItemSet.prototype.markDirty = function markDirty() {
  5224. this.groupIds = [];
  5225. this.stackDirty = true;
  5226. };
  5227. /**
  5228. * Hide the component from the DOM
  5229. */
  5230. ItemSet.prototype.hide = function hide() {
  5231. // remove the axis with dots
  5232. if (this.dom.axis.parentNode) {
  5233. this.dom.axis.parentNode.removeChild(this.dom.axis);
  5234. }
  5235. // remove the background with vertical lines
  5236. if (this.dom.background.parentNode) {
  5237. this.dom.background.parentNode.removeChild(this.dom.background);
  5238. }
  5239. // remove the labelset containing all group labels
  5240. if (this.dom.labelSet.parentNode) {
  5241. this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
  5242. }
  5243. };
  5244. /**
  5245. * Show the component in the DOM (when not already visible).
  5246. * @return {Boolean} changed
  5247. */
  5248. ItemSet.prototype.show = function show() {
  5249. // show axis with dots
  5250. if (!this.dom.axis.parentNode) {
  5251. this.axisPanel.frame.appendChild(this.dom.axis);
  5252. }
  5253. // show background with vertical lines
  5254. if (!this.dom.background.parentNode) {
  5255. this.backgroundPanel.frame.appendChild(this.dom.background);
  5256. }
  5257. // show labelset containing labels
  5258. if (!this.dom.labelSet.parentNode) {
  5259. this.sidePanel.frame.appendChild(this.dom.labelSet);
  5260. }
  5261. };
  5262. /**
  5263. * Set range (start and end).
  5264. * @param {Range | Object} range A Range or an object containing start and end.
  5265. */
  5266. ItemSet.prototype.setRange = function setRange(range) {
  5267. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  5268. throw new TypeError('Range must be an instance of Range, ' +
  5269. 'or an object containing start and end.');
  5270. }
  5271. this.range = range;
  5272. };
  5273. /**
  5274. * Set selected items by their id. Replaces the current selection
  5275. * Unknown id's are silently ignored.
  5276. * @param {Array} [ids] An array with zero or more id's of the items to be
  5277. * selected. If ids is an empty array, all items will be
  5278. * unselected.
  5279. */
  5280. ItemSet.prototype.setSelection = function setSelection(ids) {
  5281. var i, ii, id, item;
  5282. if (ids) {
  5283. if (!Array.isArray(ids)) {
  5284. throw new TypeError('Array expected');
  5285. }
  5286. // unselect currently selected items
  5287. for (i = 0, ii = this.selection.length; i < ii; i++) {
  5288. id = this.selection[i];
  5289. item = this.items[id];
  5290. if (item) item.unselect();
  5291. }
  5292. // select items
  5293. this.selection = [];
  5294. for (i = 0, ii = ids.length; i < ii; i++) {
  5295. id = ids[i];
  5296. item = this.items[id];
  5297. if (item) {
  5298. this.selection.push(id);
  5299. item.select();
  5300. }
  5301. }
  5302. }
  5303. };
  5304. /**
  5305. * Get the selected items by their id
  5306. * @return {Array} ids The ids of the selected items
  5307. */
  5308. ItemSet.prototype.getSelection = function getSelection() {
  5309. return this.selection.concat([]);
  5310. };
  5311. /**
  5312. * Deselect a selected item
  5313. * @param {String | Number} id
  5314. * @private
  5315. */
  5316. ItemSet.prototype._deselect = function _deselect(id) {
  5317. var selection = this.selection;
  5318. for (var i = 0, ii = selection.length; i < ii; i++) {
  5319. if (selection[i] == id) { // non-strict comparison!
  5320. selection.splice(i, 1);
  5321. break;
  5322. }
  5323. }
  5324. };
  5325. /**
  5326. * Return the item sets frame
  5327. * @returns {HTMLElement} frame
  5328. */
  5329. ItemSet.prototype.getFrame = function getFrame() {
  5330. return this.frame;
  5331. };
  5332. /**
  5333. * Repaint the component
  5334. * @return {boolean} Returns true if the component is resized
  5335. */
  5336. ItemSet.prototype.repaint = function repaint() {
  5337. var margin = this.options.margin,
  5338. range = this.range,
  5339. asSize = util.option.asSize,
  5340. asString = util.option.asString,
  5341. options = this.options,
  5342. orientation = this.getOption('orientation'),
  5343. resized = false,
  5344. frame = this.frame;
  5345. // TODO: document this feature to specify one margin for both item and axis distance
  5346. if (typeof margin === 'number') {
  5347. margin = {
  5348. item: margin,
  5349. axis: margin
  5350. };
  5351. }
  5352. // update className
  5353. frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  5354. // reorder the groups (if needed)
  5355. resized = this._orderGroups() || resized;
  5356. // check whether zoomed (in that case we need to re-stack everything)
  5357. // TODO: would be nicer to get this as a trigger from Range
  5358. var visibleInterval = this.range.end - this.range.start;
  5359. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  5360. if (zoomed) this.stackDirty = true;
  5361. this.lastVisibleInterval = visibleInterval;
  5362. this.lastWidth = this.width;
  5363. // repaint all groups
  5364. var restack = this.stackDirty,
  5365. firstGroup = this._firstGroup(),
  5366. firstMargin = {
  5367. item: margin.item,
  5368. axis: margin.axis
  5369. },
  5370. nonFirstMargin = {
  5371. item: margin.item,
  5372. axis: margin.item / 2
  5373. },
  5374. height = 0,
  5375. minHeight = margin.axis + margin.item;
  5376. util.forEach(this.groups, function (group) {
  5377. var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
  5378. resized = group.repaint(range, groupMargin, restack) || resized;
  5379. height += group.height;
  5380. });
  5381. height = Math.max(height, minHeight);
  5382. this.stackDirty = false;
  5383. // reposition frame
  5384. frame.style.left = asSize(options.left, '');
  5385. frame.style.right = asSize(options.right, '');
  5386. frame.style.top = asSize((orientation == 'top') ? '0' : '');
  5387. frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  5388. frame.style.width = asSize(options.width, '100%');
  5389. frame.style.height = asSize(height);
  5390. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  5391. // calculate actual size and position
  5392. this.top = frame.offsetTop;
  5393. this.left = frame.offsetLeft;
  5394. this.width = frame.offsetWidth;
  5395. this.height = height;
  5396. // reposition axis
  5397. this.dom.axis.style.left = asSize(options.left, '0');
  5398. this.dom.axis.style.right = asSize(options.right, '');
  5399. this.dom.axis.style.width = asSize(options.width, '100%');
  5400. this.dom.axis.style.height = asSize(0);
  5401. this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
  5402. this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
  5403. // check if this component is resized
  5404. resized = this._isResized() || resized;
  5405. return resized;
  5406. };
  5407. /**
  5408. * Get the first group, aligned with the axis
  5409. * @return {Group | null} firstGroup
  5410. * @private
  5411. */
  5412. ItemSet.prototype._firstGroup = function _firstGroup() {
  5413. var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
  5414. var firstGroupId = this.groupIds[firstGroupIndex];
  5415. var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
  5416. return firstGroup || null;
  5417. };
  5418. /**
  5419. * Create or delete the group holding all ungrouped items. This group is used when
  5420. * there are no groups specified.
  5421. * @protected
  5422. */
  5423. ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
  5424. var ungrouped = this.groups[UNGROUPED];
  5425. if (this.groupsData) {
  5426. // remove the group holding all ungrouped items
  5427. if (ungrouped) {
  5428. ungrouped.hide();
  5429. delete this.groups[UNGROUPED];
  5430. }
  5431. }
  5432. else {
  5433. // create a group holding all (unfiltered) items
  5434. if (!ungrouped) {
  5435. var id = null;
  5436. var data = null;
  5437. ungrouped = new Group(id, data, this);
  5438. this.groups[UNGROUPED] = ungrouped;
  5439. for (var itemId in this.items) {
  5440. if (this.items.hasOwnProperty(itemId)) {
  5441. ungrouped.add(this.items[itemId]);
  5442. }
  5443. }
  5444. ungrouped.show();
  5445. }
  5446. }
  5447. };
  5448. /**
  5449. * Get the foreground container element
  5450. * @return {HTMLElement} foreground
  5451. */
  5452. ItemSet.prototype.getForeground = function getForeground() {
  5453. return this.dom.foreground;
  5454. };
  5455. /**
  5456. * Get the background container element
  5457. * @return {HTMLElement} background
  5458. */
  5459. ItemSet.prototype.getBackground = function getBackground() {
  5460. return this.dom.background;
  5461. };
  5462. /**
  5463. * Get the axis container element
  5464. * @return {HTMLElement} axis
  5465. */
  5466. ItemSet.prototype.getAxis = function getAxis() {
  5467. return this.dom.axis;
  5468. };
  5469. /**
  5470. * Get the element for the labelset
  5471. * @return {HTMLElement} labelSet
  5472. */
  5473. ItemSet.prototype.getLabelSet = function getLabelSet() {
  5474. return this.dom.labelSet;
  5475. };
  5476. /**
  5477. * Set items
  5478. * @param {vis.DataSet | null} items
  5479. */
  5480. ItemSet.prototype.setItems = function setItems(items) {
  5481. var me = this,
  5482. ids,
  5483. oldItemsData = this.itemsData;
  5484. // replace the dataset
  5485. if (!items) {
  5486. this.itemsData = null;
  5487. }
  5488. else if (items instanceof DataSet || items instanceof DataView) {
  5489. this.itemsData = items;
  5490. }
  5491. else {
  5492. throw new TypeError('Data must be an instance of DataSet or DataView');
  5493. }
  5494. if (oldItemsData) {
  5495. // unsubscribe from old dataset
  5496. util.forEach(this.itemListeners, function (callback, event) {
  5497. oldItemsData.unsubscribe(event, callback);
  5498. });
  5499. // remove all drawn items
  5500. ids = oldItemsData.getIds();
  5501. this._onRemove(ids);
  5502. }
  5503. if (this.itemsData) {
  5504. // subscribe to new dataset
  5505. var id = this.id;
  5506. util.forEach(this.itemListeners, function (callback, event) {
  5507. me.itemsData.on(event, callback, id);
  5508. });
  5509. // add all new items
  5510. ids = this.itemsData.getIds();
  5511. this._onAdd(ids);
  5512. // update the group holding all ungrouped items
  5513. this._updateUngrouped();
  5514. }
  5515. };
  5516. /**
  5517. * Get the current items
  5518. * @returns {vis.DataSet | null}
  5519. */
  5520. ItemSet.prototype.getItems = function getItems() {
  5521. return this.itemsData;
  5522. };
  5523. /**
  5524. * Set groups
  5525. * @param {vis.DataSet} groups
  5526. */
  5527. ItemSet.prototype.setGroups = function setGroups(groups) {
  5528. var me = this,
  5529. ids;
  5530. // unsubscribe from current dataset
  5531. if (this.groupsData) {
  5532. util.forEach(this.groupListeners, function (callback, event) {
  5533. me.groupsData.unsubscribe(event, callback);
  5534. });
  5535. // remove all drawn groups
  5536. ids = this.groupsData.getIds();
  5537. this.groupsData = null;
  5538. this._onRemoveGroups(ids); // note: this will cause a repaint
  5539. }
  5540. // replace the dataset
  5541. if (!groups) {
  5542. this.groupsData = null;
  5543. }
  5544. else if (groups instanceof DataSet || groups instanceof DataView) {
  5545. this.groupsData = groups;
  5546. }
  5547. else {
  5548. throw new TypeError('Data must be an instance of DataSet or DataView');
  5549. }
  5550. if (this.groupsData) {
  5551. // subscribe to new dataset
  5552. var id = this.id;
  5553. util.forEach(this.groupListeners, function (callback, event) {
  5554. me.groupsData.on(event, callback, id);
  5555. });
  5556. // draw all ms
  5557. ids = this.groupsData.getIds();
  5558. this._onAddGroups(ids);
  5559. }
  5560. // update the group holding all ungrouped items
  5561. this._updateUngrouped();
  5562. // update the order of all items in each group
  5563. this._order();
  5564. this.emit('change');
  5565. };
  5566. /**
  5567. * Get the current groups
  5568. * @returns {vis.DataSet | null} groups
  5569. */
  5570. ItemSet.prototype.getGroups = function getGroups() {
  5571. return this.groupsData;
  5572. };
  5573. /**
  5574. * Remove an item by its id
  5575. * @param {String | Number} id
  5576. */
  5577. ItemSet.prototype.removeItem = function removeItem (id) {
  5578. var item = this.itemsData.get(id),
  5579. dataset = this._myDataSet();
  5580. if (item) {
  5581. // confirm deletion
  5582. this.options.onRemove(item, function (item) {
  5583. if (item) {
  5584. // remove by id here, it is possible that an item has no id defined
  5585. // itself, so better not delete by the item itself
  5586. dataset.remove(id);
  5587. }
  5588. });
  5589. }
  5590. };
  5591. /**
  5592. * Handle updated items
  5593. * @param {Number[]} ids
  5594. * @protected
  5595. */
  5596. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  5597. var me = this,
  5598. items = this.items,
  5599. itemOptions = this.itemOptions;
  5600. ids.forEach(function (id) {
  5601. var itemData = me.itemsData.get(id),
  5602. item = items[id],
  5603. type = itemData.type ||
  5604. (itemData.start && itemData.end && 'range') ||
  5605. me.options.type ||
  5606. 'box';
  5607. var constructor = ItemSet.types[type];
  5608. if (item) {
  5609. // update item
  5610. if (!constructor || !(item instanceof constructor)) {
  5611. // item type has changed, delete the item and recreate it
  5612. me._removeItem(item);
  5613. item = null;
  5614. }
  5615. else {
  5616. me._updateItem(item, itemData);
  5617. }
  5618. }
  5619. if (!item) {
  5620. // create item
  5621. if (constructor) {
  5622. item = new constructor(itemData, me.options, itemOptions);
  5623. item.id = id; // TODO: not so nice setting id afterwards
  5624. me._addItem(item);
  5625. }
  5626. else {
  5627. throw new TypeError('Unknown item type "' + type + '"');
  5628. }
  5629. }
  5630. });
  5631. this._order();
  5632. this.stackDirty = true; // force re-stacking of all items next repaint
  5633. this.emit('change');
  5634. };
  5635. /**
  5636. * Handle added items
  5637. * @param {Number[]} ids
  5638. * @protected
  5639. */
  5640. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  5641. /**
  5642. * Handle removed items
  5643. * @param {Number[]} ids
  5644. * @protected
  5645. */
  5646. ItemSet.prototype._onRemove = function _onRemove(ids) {
  5647. var count = 0;
  5648. var me = this;
  5649. ids.forEach(function (id) {
  5650. var item = me.items[id];
  5651. if (item) {
  5652. count++;
  5653. me._removeItem(item);
  5654. }
  5655. });
  5656. if (count) {
  5657. // update order
  5658. this._order();
  5659. this.stackDirty = true; // force re-stacking of all items next repaint
  5660. this.emit('change');
  5661. }
  5662. };
  5663. /**
  5664. * Update the order of item in all groups
  5665. * @private
  5666. */
  5667. ItemSet.prototype._order = function _order() {
  5668. // reorder the items in all groups
  5669. // TODO: optimization: only reorder groups affected by the changed items
  5670. util.forEach(this.groups, function (group) {
  5671. group.order();
  5672. });
  5673. };
  5674. /**
  5675. * Handle updated groups
  5676. * @param {Number[]} ids
  5677. * @private
  5678. */
  5679. ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
  5680. this._onAddGroups(ids);
  5681. };
  5682. /**
  5683. * Handle changed groups
  5684. * @param {Number[]} ids
  5685. * @private
  5686. */
  5687. ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
  5688. var me = this;
  5689. ids.forEach(function (id) {
  5690. var groupData = me.groupsData.get(id);
  5691. var group = me.groups[id];
  5692. if (!group) {
  5693. // check for reserved ids
  5694. if (id == UNGROUPED) {
  5695. throw new Error('Illegal group id. ' + id + ' is a reserved id.');
  5696. }
  5697. var groupOptions = Object.create(me.options);
  5698. util.extend(groupOptions, {
  5699. height: null
  5700. });
  5701. group = new Group(id, groupData, me);
  5702. me.groups[id] = group;
  5703. // add items with this groupId to the new group
  5704. for (var itemId in me.items) {
  5705. if (me.items.hasOwnProperty(itemId)) {
  5706. var item = me.items[itemId];
  5707. if (item.data.group == id) {
  5708. group.add(item);
  5709. }
  5710. }
  5711. }
  5712. group.order();
  5713. group.show();
  5714. }
  5715. else {
  5716. // update group
  5717. group.setData(groupData);
  5718. }
  5719. });
  5720. this.emit('change');
  5721. };
  5722. /**
  5723. * Handle removed groups
  5724. * @param {Number[]} ids
  5725. * @private
  5726. */
  5727. ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
  5728. var groups = this.groups;
  5729. ids.forEach(function (id) {
  5730. var group = groups[id];
  5731. if (group) {
  5732. group.hide();
  5733. delete groups[id];
  5734. }
  5735. });
  5736. this.markDirty();
  5737. this.emit('change');
  5738. };
  5739. /**
  5740. * Reorder the groups if needed
  5741. * @return {boolean} changed
  5742. * @private
  5743. */
  5744. ItemSet.prototype._orderGroups = function () {
  5745. if (this.groupsData) {
  5746. // reorder the groups
  5747. var groupIds = this.groupsData.getIds({
  5748. order: this.options.groupOrder
  5749. });
  5750. var changed = !util.equalArray(groupIds, this.groupIds);
  5751. if (changed) {
  5752. // hide all groups, removes them from the DOM
  5753. var groups = this.groups;
  5754. groupIds.forEach(function (groupId) {
  5755. groups[groupId].hide();
  5756. });
  5757. // show the groups again, attach them to the DOM in correct order
  5758. groupIds.forEach(function (groupId) {
  5759. groups[groupId].show();
  5760. });
  5761. this.groupIds = groupIds;
  5762. }
  5763. return changed;
  5764. }
  5765. else {
  5766. return false;
  5767. }
  5768. };
  5769. /**
  5770. * Add a new item
  5771. * @param {Item} item
  5772. * @private
  5773. */
  5774. ItemSet.prototype._addItem = function _addItem(item) {
  5775. this.items[item.id] = item;
  5776. // add to group
  5777. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  5778. var group = this.groups[groupId];
  5779. if (group) group.add(item);
  5780. };
  5781. /**
  5782. * Update an existing item
  5783. * @param {Item} item
  5784. * @param {Object} itemData
  5785. * @private
  5786. */
  5787. ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
  5788. var oldGroupId = item.data.group;
  5789. item.data = itemData;
  5790. if (item.displayed) {
  5791. item.repaint();
  5792. }
  5793. // update group
  5794. if (oldGroupId != item.data.group) {
  5795. var oldGroup = this.groups[oldGroupId];
  5796. if (oldGroup) oldGroup.remove(item);
  5797. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  5798. var group = this.groups[groupId];
  5799. if (group) group.add(item);
  5800. }
  5801. };
  5802. /**
  5803. * Delete an item from the ItemSet: remove it from the DOM, from the map
  5804. * with items, and from the map with visible items, and from the selection
  5805. * @param {Item} item
  5806. * @private
  5807. */
  5808. ItemSet.prototype._removeItem = function _removeItem(item) {
  5809. // remove from DOM
  5810. item.hide();
  5811. // remove from items
  5812. delete this.items[item.id];
  5813. // remove from selection
  5814. var index = this.selection.indexOf(item.id);
  5815. if (index != -1) this.selection.splice(index, 1);
  5816. // remove from group
  5817. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  5818. var group = this.groups[groupId];
  5819. if (group) group.remove(item);
  5820. };
  5821. /**
  5822. * Create an array containing all items being a range (having an end date)
  5823. * @param array
  5824. * @returns {Array}
  5825. * @private
  5826. */
  5827. ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
  5828. var endArray = [];
  5829. for (var i = 0; i < array.length; i++) {
  5830. if (array[i] instanceof ItemRange) {
  5831. endArray.push(array[i]);
  5832. }
  5833. }
  5834. return endArray;
  5835. };
  5836. /**
  5837. * Get the width of the group labels
  5838. * @return {Number} width
  5839. */
  5840. ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
  5841. var width = 0;
  5842. util.forEach(this.groups, function (group) {
  5843. width = Math.max(width, group.getLabelWidth());
  5844. });
  5845. return width;
  5846. };
  5847. /**
  5848. * Get the height of the itemsets background
  5849. * @return {Number} height
  5850. */
  5851. ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
  5852. return this.height;
  5853. };
  5854. /**
  5855. * Start dragging the selected events
  5856. * @param {Event} event
  5857. * @private
  5858. */
  5859. ItemSet.prototype._onDragStart = function (event) {
  5860. if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
  5861. return;
  5862. }
  5863. var item = ItemSet.itemFromTarget(event),
  5864. me = this,
  5865. props;
  5866. if (item && item.selected) {
  5867. var dragLeftItem = event.target.dragLeftItem;
  5868. var dragRightItem = event.target.dragRightItem;
  5869. if (dragLeftItem) {
  5870. props = {
  5871. item: dragLeftItem
  5872. };
  5873. if (me.options.editable.updateTime) {
  5874. props.start = item.data.start.valueOf();
  5875. }
  5876. if (me.options.editable.updateGroup) {
  5877. if ('group' in item.data) props.group = item.data.group;
  5878. }
  5879. this.touchParams.itemProps = [props];
  5880. }
  5881. else if (dragRightItem) {
  5882. props = {
  5883. item: dragRightItem
  5884. };
  5885. if (me.options.editable.updateTime) {
  5886. props.end = item.data.end.valueOf();
  5887. }
  5888. if (me.options.editable.updateGroup) {
  5889. if ('group' in item.data) props.group = item.data.group;
  5890. }
  5891. this.touchParams.itemProps = [props];
  5892. }
  5893. else {
  5894. this.touchParams.itemProps = this.getSelection().map(function (id) {
  5895. var item = me.items[id];
  5896. var props = {
  5897. item: item
  5898. };
  5899. if (me.options.editable.updateTime) {
  5900. if ('start' in item.data) props.start = item.data.start.valueOf();
  5901. if ('end' in item.data) props.end = item.data.end.valueOf();
  5902. }
  5903. if (me.options.editable.updateGroup) {
  5904. if ('group' in item.data) props.group = item.data.group;
  5905. }
  5906. return props;
  5907. });
  5908. }
  5909. event.stopPropagation();
  5910. }
  5911. };
  5912. /**
  5913. * Drag selected items
  5914. * @param {Event} event
  5915. * @private
  5916. */
  5917. ItemSet.prototype._onDrag = function (event) {
  5918. if (this.touchParams.itemProps) {
  5919. var snap = this.options.snap || null,
  5920. deltaX = event.gesture.deltaX,
  5921. scale = (this.width / (this.range.end - this.range.start)),
  5922. offset = deltaX / scale;
  5923. // move
  5924. this.touchParams.itemProps.forEach(function (props) {
  5925. if ('start' in props) {
  5926. var start = new Date(props.start + offset);
  5927. props.item.data.start = snap ? snap(start) : start;
  5928. }
  5929. if ('end' in props) {
  5930. var end = new Date(props.end + offset);
  5931. props.item.data.end = snap ? snap(end) : end;
  5932. }
  5933. if ('group' in props) {
  5934. // drag from one group to another
  5935. var group = ItemSet.groupFromTarget(event);
  5936. if (group && group.groupId != props.item.data.group) {
  5937. var oldGroup = props.item.parent;
  5938. oldGroup.remove(props.item);
  5939. oldGroup.order();
  5940. group.add(props.item);
  5941. group.order();
  5942. props.item.data.group = group.groupId;
  5943. }
  5944. }
  5945. });
  5946. // TODO: implement onMoving handler
  5947. this.stackDirty = true; // force re-stacking of all items next repaint
  5948. this.emit('change');
  5949. event.stopPropagation();
  5950. }
  5951. };
  5952. /**
  5953. * End of dragging selected items
  5954. * @param {Event} event
  5955. * @private
  5956. */
  5957. ItemSet.prototype._onDragEnd = function (event) {
  5958. if (this.touchParams.itemProps) {
  5959. // prepare a change set for the changed items
  5960. var changes = [],
  5961. me = this,
  5962. dataset = this._myDataSet();
  5963. this.touchParams.itemProps.forEach(function (props) {
  5964. var id = props.item.id,
  5965. itemData = me.itemsData.get(id);
  5966. var changed = false;
  5967. if ('start' in props.item.data) {
  5968. changed = (props.start != props.item.data.start.valueOf());
  5969. itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
  5970. }
  5971. if ('end' in props.item.data) {
  5972. changed = changed || (props.end != props.item.data.end.valueOf());
  5973. itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
  5974. }
  5975. if ('group' in props.item.data) {
  5976. changed = changed || (props.group != props.item.data.group);
  5977. itemData.group = props.item.data.group;
  5978. }
  5979. // only apply changes when start or end is actually changed
  5980. if (changed) {
  5981. me.options.onMove(itemData, function (itemData) {
  5982. if (itemData) {
  5983. // apply changes
  5984. itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
  5985. changes.push(itemData);
  5986. }
  5987. else {
  5988. // restore original values
  5989. if ('start' in props) props.item.data.start = props.start;
  5990. if ('end' in props) props.item.data.end = props.end;
  5991. me.stackDirty = true; // force re-stacking of all items next repaint
  5992. me.emit('change');
  5993. }
  5994. });
  5995. }
  5996. });
  5997. this.touchParams.itemProps = null;
  5998. // apply the changes to the data (if there are changes)
  5999. if (changes.length) {
  6000. dataset.update(changes);
  6001. }
  6002. event.stopPropagation();
  6003. }
  6004. };
  6005. /**
  6006. * Find an item from an event target:
  6007. * searches for the attribute 'timeline-item' in the event target's element tree
  6008. * @param {Event} event
  6009. * @return {Item | null} item
  6010. */
  6011. ItemSet.itemFromTarget = function itemFromTarget (event) {
  6012. var target = event.target;
  6013. while (target) {
  6014. if (target.hasOwnProperty('timeline-item')) {
  6015. return target['timeline-item'];
  6016. }
  6017. target = target.parentNode;
  6018. }
  6019. return null;
  6020. };
  6021. /**
  6022. * Find the Group from an event target:
  6023. * searches for the attribute 'timeline-group' in the event target's element tree
  6024. * @param {Event} event
  6025. * @return {Group | null} group
  6026. */
  6027. ItemSet.groupFromTarget = function groupFromTarget (event) {
  6028. var target = event.target;
  6029. while (target) {
  6030. if (target.hasOwnProperty('timeline-group')) {
  6031. return target['timeline-group'];
  6032. }
  6033. target = target.parentNode;
  6034. }
  6035. return null;
  6036. };
  6037. /**
  6038. * Find the ItemSet from an event target:
  6039. * searches for the attribute 'timeline-itemset' in the event target's element tree
  6040. * @param {Event} event
  6041. * @return {ItemSet | null} item
  6042. */
  6043. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  6044. var target = event.target;
  6045. while (target) {
  6046. if (target.hasOwnProperty('timeline-itemset')) {
  6047. return target['timeline-itemset'];
  6048. }
  6049. target = target.parentNode;
  6050. }
  6051. return null;
  6052. };
  6053. /**
  6054. * Find the DataSet to which this ItemSet is connected
  6055. * @returns {null | DataSet} dataset
  6056. * @private
  6057. */
  6058. ItemSet.prototype._myDataSet = function _myDataSet() {
  6059. // find the root DataSet
  6060. var dataset = this.itemsData;
  6061. while (dataset instanceof DataView) {
  6062. dataset = dataset.data;
  6063. }
  6064. return dataset;
  6065. };
  6066. /**
  6067. * @constructor Item
  6068. * @param {Object} data Object containing (optional) parameters type,
  6069. * start, end, content, group, className.
  6070. * @param {Object} [options] Options to set initial property values
  6071. * @param {Object} [defaultOptions] default options
  6072. * // TODO: describe available options
  6073. */
  6074. function Item (data, options, defaultOptions) {
  6075. this.id = null;
  6076. this.parent = null;
  6077. this.data = data;
  6078. this.dom = null;
  6079. this.options = options || {};
  6080. this.defaultOptions = defaultOptions || {};
  6081. this.selected = false;
  6082. this.displayed = false;
  6083. this.dirty = true;
  6084. this.top = null;
  6085. this.left = null;
  6086. this.width = null;
  6087. this.height = null;
  6088. }
  6089. /**
  6090. * Select current item
  6091. */
  6092. Item.prototype.select = function select() {
  6093. this.selected = true;
  6094. if (this.displayed) this.repaint();
  6095. };
  6096. /**
  6097. * Unselect current item
  6098. */
  6099. Item.prototype.unselect = function unselect() {
  6100. this.selected = false;
  6101. if (this.displayed) this.repaint();
  6102. };
  6103. /**
  6104. * Set a parent for the item
  6105. * @param {ItemSet | Group} parent
  6106. */
  6107. Item.prototype.setParent = function setParent(parent) {
  6108. if (this.displayed) {
  6109. this.hide();
  6110. this.parent = parent;
  6111. if (this.parent) {
  6112. this.show();
  6113. }
  6114. }
  6115. else {
  6116. this.parent = parent;
  6117. }
  6118. };
  6119. /**
  6120. * Check whether this item is visible inside given range
  6121. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  6122. * @returns {boolean} True if visible
  6123. */
  6124. Item.prototype.isVisible = function isVisible (range) {
  6125. // Should be implemented by Item implementations
  6126. return false;
  6127. };
  6128. /**
  6129. * Show the Item in the DOM (when not already visible)
  6130. * @return {Boolean} changed
  6131. */
  6132. Item.prototype.show = function show() {
  6133. return false;
  6134. };
  6135. /**
  6136. * Hide the Item from the DOM (when visible)
  6137. * @return {Boolean} changed
  6138. */
  6139. Item.prototype.hide = function hide() {
  6140. return false;
  6141. };
  6142. /**
  6143. * Repaint the item
  6144. */
  6145. Item.prototype.repaint = function repaint() {
  6146. // should be implemented by the item
  6147. };
  6148. /**
  6149. * Reposition the Item horizontally
  6150. */
  6151. Item.prototype.repositionX = function repositionX() {
  6152. // should be implemented by the item
  6153. };
  6154. /**
  6155. * Reposition the Item vertically
  6156. */
  6157. Item.prototype.repositionY = function repositionY() {
  6158. // should be implemented by the item
  6159. };
  6160. /**
  6161. * Repaint a delete button on the top right of the item when the item is selected
  6162. * @param {HTMLElement} anchor
  6163. * @protected
  6164. */
  6165. Item.prototype._repaintDeleteButton = function (anchor) {
  6166. if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
  6167. // create and show button
  6168. var me = this;
  6169. var deleteButton = document.createElement('div');
  6170. deleteButton.className = 'delete';
  6171. deleteButton.title = 'Delete this item';
  6172. Hammer(deleteButton, {
  6173. preventDefault: true
  6174. }).on('tap', function (event) {
  6175. me.parent.removeFromDataSet(me);
  6176. event.stopPropagation();
  6177. });
  6178. anchor.appendChild(deleteButton);
  6179. this.dom.deleteButton = deleteButton;
  6180. }
  6181. else if (!this.selected && this.dom.deleteButton) {
  6182. // remove button
  6183. if (this.dom.deleteButton.parentNode) {
  6184. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  6185. }
  6186. this.dom.deleteButton = null;
  6187. }
  6188. };
  6189. /**
  6190. * @constructor ItemBox
  6191. * @extends Item
  6192. * @param {Object} data Object containing parameters start
  6193. * content, className.
  6194. * @param {Object} [options] Options to set initial property values
  6195. * @param {Object} [defaultOptions] default options
  6196. * // TODO: describe available options
  6197. */
  6198. function ItemBox (data, options, defaultOptions) {
  6199. this.props = {
  6200. dot: {
  6201. width: 0,
  6202. height: 0
  6203. },
  6204. line: {
  6205. width: 0,
  6206. height: 0
  6207. }
  6208. };
  6209. // validate data
  6210. if (data) {
  6211. if (data.start == undefined) {
  6212. throw new Error('Property "start" missing in item ' + data);
  6213. }
  6214. }
  6215. Item.call(this, data, options, defaultOptions);
  6216. }
  6217. ItemBox.prototype = new Item (null);
  6218. /**
  6219. * Check whether this item is visible inside given range
  6220. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  6221. * @returns {boolean} True if visible
  6222. */
  6223. ItemBox.prototype.isVisible = function isVisible (range) {
  6224. // determine visibility
  6225. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  6226. var interval = (range.end - range.start) / 4;
  6227. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  6228. };
  6229. /**
  6230. * Repaint the item
  6231. */
  6232. ItemBox.prototype.repaint = function repaint() {
  6233. var dom = this.dom;
  6234. if (!dom) {
  6235. // create DOM
  6236. this.dom = {};
  6237. dom = this.dom;
  6238. // create main box
  6239. dom.box = document.createElement('DIV');
  6240. // contents box (inside the background box). used for making margins
  6241. dom.content = document.createElement('DIV');
  6242. dom.content.className = 'content';
  6243. dom.box.appendChild(dom.content);
  6244. // line to axis
  6245. dom.line = document.createElement('DIV');
  6246. dom.line.className = 'line';
  6247. // dot on axis
  6248. dom.dot = document.createElement('DIV');
  6249. dom.dot.className = 'dot';
  6250. // attach this item as attribute
  6251. dom.box['timeline-item'] = this;
  6252. }
  6253. // append DOM to parent DOM
  6254. if (!this.parent) {
  6255. throw new Error('Cannot repaint item: no parent attached');
  6256. }
  6257. if (!dom.box.parentNode) {
  6258. var foreground = this.parent.getForeground();
  6259. if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
  6260. foreground.appendChild(dom.box);
  6261. }
  6262. if (!dom.line.parentNode) {
  6263. var background = this.parent.getBackground();
  6264. if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
  6265. background.appendChild(dom.line);
  6266. }
  6267. if (!dom.dot.parentNode) {
  6268. var axis = this.parent.getAxis();
  6269. if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
  6270. axis.appendChild(dom.dot);
  6271. }
  6272. this.displayed = true;
  6273. // update contents
  6274. if (this.data.content != this.content) {
  6275. this.content = this.data.content;
  6276. if (this.content instanceof Element) {
  6277. dom.content.innerHTML = '';
  6278. dom.content.appendChild(this.content);
  6279. }
  6280. else if (this.data.content != undefined) {
  6281. dom.content.innerHTML = this.content;
  6282. }
  6283. else {
  6284. throw new Error('Property "content" missing in item ' + this.data.id);
  6285. }
  6286. this.dirty = true;
  6287. }
  6288. // update class
  6289. var className = (this.data.className? ' ' + this.data.className : '') +
  6290. (this.selected ? ' selected' : '');
  6291. if (this.className != className) {
  6292. this.className = className;
  6293. dom.box.className = 'item box' + className;
  6294. dom.line.className = 'item line' + className;
  6295. dom.dot.className = 'item dot' + className;
  6296. this.dirty = true;
  6297. }
  6298. // recalculate size
  6299. if (this.dirty) {
  6300. this.props.dot.height = dom.dot.offsetHeight;
  6301. this.props.dot.width = dom.dot.offsetWidth;
  6302. this.props.line.width = dom.line.offsetWidth;
  6303. this.width = dom.box.offsetWidth;
  6304. this.height = dom.box.offsetHeight;
  6305. this.dirty = false;
  6306. }
  6307. this._repaintDeleteButton(dom.box);
  6308. };
  6309. /**
  6310. * Show the item in the DOM (when not already displayed). The items DOM will
  6311. * be created when needed.
  6312. */
  6313. ItemBox.prototype.show = function show() {
  6314. if (!this.displayed) {
  6315. this.repaint();
  6316. }
  6317. };
  6318. /**
  6319. * Hide the item from the DOM (when visible)
  6320. */
  6321. ItemBox.prototype.hide = function hide() {
  6322. if (this.displayed) {
  6323. var dom = this.dom;
  6324. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  6325. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  6326. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  6327. this.top = null;
  6328. this.left = null;
  6329. this.displayed = false;
  6330. }
  6331. };
  6332. /**
  6333. * Reposition the item horizontally
  6334. * @Override
  6335. */
  6336. ItemBox.prototype.repositionX = function repositionX() {
  6337. var start = this.defaultOptions.toScreen(this.data.start),
  6338. align = this.options.align || this.defaultOptions.align,
  6339. left,
  6340. box = this.dom.box,
  6341. line = this.dom.line,
  6342. dot = this.dom.dot;
  6343. // calculate left position of the box
  6344. if (align == 'right') {
  6345. this.left = start - this.width;
  6346. }
  6347. else if (align == 'left') {
  6348. this.left = start;
  6349. }
  6350. else {
  6351. // default or 'center'
  6352. this.left = start - this.width / 2;
  6353. }
  6354. // reposition box
  6355. box.style.left = this.left + 'px';
  6356. // reposition line
  6357. line.style.left = (start - this.props.line.width / 2) + 'px';
  6358. // reposition dot
  6359. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  6360. };
  6361. /**
  6362. * Reposition the item vertically
  6363. * @Override
  6364. */
  6365. ItemBox.prototype.repositionY = function repositionY () {
  6366. var orientation = this.options.orientation || this.defaultOptions.orientation,
  6367. box = this.dom.box,
  6368. line = this.dom.line,
  6369. dot = this.dom.dot;
  6370. if (orientation == 'top') {
  6371. box.style.top = (this.top || 0) + 'px';
  6372. box.style.bottom = '';
  6373. line.style.top = '0';
  6374. line.style.bottom = '';
  6375. line.style.height = (this.parent.top + this.top + 1) + 'px';
  6376. }
  6377. else { // orientation 'bottom'
  6378. box.style.top = '';
  6379. box.style.bottom = (this.top || 0) + 'px';
  6380. line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
  6381. line.style.bottom = '0';
  6382. line.style.height = '';
  6383. }
  6384. dot.style.top = (-this.props.dot.height / 2) + 'px';
  6385. };
  6386. /**
  6387. * @constructor ItemPoint
  6388. * @extends Item
  6389. * @param {Object} data Object containing parameters start
  6390. * content, className.
  6391. * @param {Object} [options] Options to set initial property values
  6392. * @param {Object} [defaultOptions] default options
  6393. * // TODO: describe available options
  6394. */
  6395. function ItemPoint (data, options, defaultOptions) {
  6396. this.props = {
  6397. dot: {
  6398. top: 0,
  6399. width: 0,
  6400. height: 0
  6401. },
  6402. content: {
  6403. height: 0,
  6404. marginLeft: 0
  6405. }
  6406. };
  6407. // validate data
  6408. if (data) {
  6409. if (data.start == undefined) {
  6410. throw new Error('Property "start" missing in item ' + data);
  6411. }
  6412. }
  6413. Item.call(this, data, options, defaultOptions);
  6414. }
  6415. ItemPoint.prototype = new Item (null);
  6416. /**
  6417. * Check whether this item is visible inside given range
  6418. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  6419. * @returns {boolean} True if visible
  6420. */
  6421. ItemPoint.prototype.isVisible = function isVisible (range) {
  6422. // determine visibility
  6423. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  6424. var interval = (range.end - range.start) / 4;
  6425. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  6426. };
  6427. /**
  6428. * Repaint the item
  6429. */
  6430. ItemPoint.prototype.repaint = function repaint() {
  6431. var dom = this.dom;
  6432. if (!dom) {
  6433. // create DOM
  6434. this.dom = {};
  6435. dom = this.dom;
  6436. // background box
  6437. dom.point = document.createElement('div');
  6438. // className is updated in repaint()
  6439. // contents box, right from the dot
  6440. dom.content = document.createElement('div');
  6441. dom.content.className = 'content';
  6442. dom.point.appendChild(dom.content);
  6443. // dot at start
  6444. dom.dot = document.createElement('div');
  6445. dom.point.appendChild(dom.dot);
  6446. // attach this item as attribute
  6447. dom.point['timeline-item'] = this;
  6448. }
  6449. // append DOM to parent DOM
  6450. if (!this.parent) {
  6451. throw new Error('Cannot repaint item: no parent attached');
  6452. }
  6453. if (!dom.point.parentNode) {
  6454. var foreground = this.parent.getForeground();
  6455. if (!foreground) {
  6456. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  6457. }
  6458. foreground.appendChild(dom.point);
  6459. }
  6460. this.displayed = true;
  6461. // update contents
  6462. if (this.data.content != this.content) {
  6463. this.content = this.data.content;
  6464. if (this.content instanceof Element) {
  6465. dom.content.innerHTML = '';
  6466. dom.content.appendChild(this.content);
  6467. }
  6468. else if (this.data.content != undefined) {
  6469. dom.content.innerHTML = this.content;
  6470. }
  6471. else {
  6472. throw new Error('Property "content" missing in item ' + this.data.id);
  6473. }
  6474. this.dirty = true;
  6475. }
  6476. // update class
  6477. var className = (this.data.className? ' ' + this.data.className : '') +
  6478. (this.selected ? ' selected' : '');
  6479. if (this.className != className) {
  6480. this.className = className;
  6481. dom.point.className = 'item point' + className;
  6482. dom.dot.className = 'item dot' + className;
  6483. this.dirty = true;
  6484. }
  6485. // recalculate size
  6486. if (this.dirty) {
  6487. this.width = dom.point.offsetWidth;
  6488. this.height = dom.point.offsetHeight;
  6489. this.props.dot.width = dom.dot.offsetWidth;
  6490. this.props.dot.height = dom.dot.offsetHeight;
  6491. this.props.content.height = dom.content.offsetHeight;
  6492. // resize contents
  6493. dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
  6494. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  6495. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  6496. dom.dot.style.left = (this.props.dot.width / 2) + 'px';
  6497. this.dirty = false;
  6498. }
  6499. this._repaintDeleteButton(dom.point);
  6500. };
  6501. /**
  6502. * Show the item in the DOM (when not already visible). The items DOM will
  6503. * be created when needed.
  6504. */
  6505. ItemPoint.prototype.show = function show() {
  6506. if (!this.displayed) {
  6507. this.repaint();
  6508. }
  6509. };
  6510. /**
  6511. * Hide the item from the DOM (when visible)
  6512. */
  6513. ItemPoint.prototype.hide = function hide() {
  6514. if (this.displayed) {
  6515. if (this.dom.point.parentNode) {
  6516. this.dom.point.parentNode.removeChild(this.dom.point);
  6517. }
  6518. this.top = null;
  6519. this.left = null;
  6520. this.displayed = false;
  6521. }
  6522. };
  6523. /**
  6524. * Reposition the item horizontally
  6525. * @Override
  6526. */
  6527. ItemPoint.prototype.repositionX = function repositionX() {
  6528. var start = this.defaultOptions.toScreen(this.data.start);
  6529. this.left = start - this.props.dot.width;
  6530. // reposition point
  6531. this.dom.point.style.left = this.left + 'px';
  6532. };
  6533. /**
  6534. * Reposition the item vertically
  6535. * @Override
  6536. */
  6537. ItemPoint.prototype.repositionY = function repositionY () {
  6538. var orientation = this.options.orientation || this.defaultOptions.orientation,
  6539. point = this.dom.point;
  6540. if (orientation == 'top') {
  6541. point.style.top = this.top + 'px';
  6542. point.style.bottom = '';
  6543. }
  6544. else {
  6545. point.style.top = '';
  6546. point.style.bottom = this.top + 'px';
  6547. }
  6548. };
  6549. /**
  6550. * @constructor ItemRange
  6551. * @extends Item
  6552. * @param {Object} data Object containing parameters start, end
  6553. * content, className.
  6554. * @param {Object} [options] Options to set initial property values
  6555. * @param {Object} [defaultOptions] default options
  6556. * // TODO: describe available options
  6557. */
  6558. function ItemRange (data, options, defaultOptions) {
  6559. this.props = {
  6560. content: {
  6561. width: 0
  6562. }
  6563. };
  6564. // validate data
  6565. if (data) {
  6566. if (data.start == undefined) {
  6567. throw new Error('Property "start" missing in item ' + data.id);
  6568. }
  6569. if (data.end == undefined) {
  6570. throw new Error('Property "end" missing in item ' + data.id);
  6571. }
  6572. }
  6573. Item.call(this, data, options, defaultOptions);
  6574. }
  6575. ItemRange.prototype = new Item (null);
  6576. ItemRange.prototype.baseClassName = 'item range';
  6577. /**
  6578. * Check whether this item is visible inside given range
  6579. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  6580. * @returns {boolean} True if visible
  6581. */
  6582. ItemRange.prototype.isVisible = function isVisible (range) {
  6583. // determine visibility
  6584. return (this.data.start < range.end) && (this.data.end > range.start);
  6585. };
  6586. /**
  6587. * Repaint the item
  6588. */
  6589. ItemRange.prototype.repaint = function repaint() {
  6590. var dom = this.dom;
  6591. if (!dom) {
  6592. // create DOM
  6593. this.dom = {};
  6594. dom = this.dom;
  6595. // background box
  6596. dom.box = document.createElement('div');
  6597. // className is updated in repaint()
  6598. // contents box
  6599. dom.content = document.createElement('div');
  6600. dom.content.className = 'content';
  6601. dom.box.appendChild(dom.content);
  6602. // attach this item as attribute
  6603. dom.box['timeline-item'] = this;
  6604. }
  6605. // append DOM to parent DOM
  6606. if (!this.parent) {
  6607. throw new Error('Cannot repaint item: no parent attached');
  6608. }
  6609. if (!dom.box.parentNode) {
  6610. var foreground = this.parent.getForeground();
  6611. if (!foreground) {
  6612. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  6613. }
  6614. foreground.appendChild(dom.box);
  6615. }
  6616. this.displayed = true;
  6617. // update contents
  6618. if (this.data.content != this.content) {
  6619. this.content = this.data.content;
  6620. if (this.content instanceof Element) {
  6621. dom.content.innerHTML = '';
  6622. dom.content.appendChild(this.content);
  6623. }
  6624. else if (this.data.content != undefined) {
  6625. dom.content.innerHTML = this.content;
  6626. }
  6627. else {
  6628. throw new Error('Property "content" missing in item ' + this.data.id);
  6629. }
  6630. this.dirty = true;
  6631. }
  6632. // update class
  6633. var className = (this.data.className ? (' ' + this.data.className) : '') +
  6634. (this.selected ? ' selected' : '');
  6635. if (this.className != className) {
  6636. this.className = className;
  6637. dom.box.className = this.baseClassName + className;
  6638. this.dirty = true;
  6639. }
  6640. // recalculate size
  6641. if (this.dirty) {
  6642. this.props.content.width = this.dom.content.offsetWidth;
  6643. this.height = this.dom.box.offsetHeight;
  6644. this.dirty = false;
  6645. }
  6646. this._repaintDeleteButton(dom.box);
  6647. this._repaintDragLeft();
  6648. this._repaintDragRight();
  6649. };
  6650. /**
  6651. * Show the item in the DOM (when not already visible). The items DOM will
  6652. * be created when needed.
  6653. */
  6654. ItemRange.prototype.show = function show() {
  6655. if (!this.displayed) {
  6656. this.repaint();
  6657. }
  6658. };
  6659. /**
  6660. * Hide the item from the DOM (when visible)
  6661. * @return {Boolean} changed
  6662. */
  6663. ItemRange.prototype.hide = function hide() {
  6664. if (this.displayed) {
  6665. var box = this.dom.box;
  6666. if (box.parentNode) {
  6667. box.parentNode.removeChild(box);
  6668. }
  6669. this.top = null;
  6670. this.left = null;
  6671. this.displayed = false;
  6672. }
  6673. };
  6674. /**
  6675. * Reposition the item horizontally
  6676. * @Override
  6677. */
  6678. ItemRange.prototype.repositionX = function repositionX() {
  6679. var props = this.props,
  6680. parentWidth = this.parent.width,
  6681. start = this.defaultOptions.toScreen(this.data.start),
  6682. end = this.defaultOptions.toScreen(this.data.end),
  6683. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  6684. contentLeft;
  6685. // limit the width of the this, as browsers cannot draw very wide divs
  6686. if (start < -parentWidth) {
  6687. start = -parentWidth;
  6688. }
  6689. if (end > 2 * parentWidth) {
  6690. end = 2 * parentWidth;
  6691. }
  6692. // when range exceeds left of the window, position the contents at the left of the visible area
  6693. if (start < 0) {
  6694. contentLeft = Math.min(-start,
  6695. (end - start - props.content.width - 2 * padding));
  6696. // TODO: remove the need for options.padding. it's terrible.
  6697. }
  6698. else {
  6699. contentLeft = 0;
  6700. }
  6701. this.left = start;
  6702. this.width = Math.max(end - start, 1);
  6703. this.dom.box.style.left = this.left + 'px';
  6704. this.dom.box.style.width = this.width + 'px';
  6705. this.dom.content.style.left = contentLeft + 'px';
  6706. };
  6707. /**
  6708. * Reposition the item vertically
  6709. * @Override
  6710. */
  6711. ItemRange.prototype.repositionY = function repositionY() {
  6712. var orientation = this.options.orientation || this.defaultOptions.orientation,
  6713. box = this.dom.box;
  6714. if (orientation == 'top') {
  6715. box.style.top = this.top + 'px';
  6716. box.style.bottom = '';
  6717. }
  6718. else {
  6719. box.style.top = '';
  6720. box.style.bottom = this.top + 'px';
  6721. }
  6722. };
  6723. /**
  6724. * Repaint a drag area on the left side of the range when the range is selected
  6725. * @protected
  6726. */
  6727. ItemRange.prototype._repaintDragLeft = function () {
  6728. if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
  6729. // create and show drag area
  6730. var dragLeft = document.createElement('div');
  6731. dragLeft.className = 'drag-left';
  6732. dragLeft.dragLeftItem = this;
  6733. // TODO: this should be redundant?
  6734. Hammer(dragLeft, {
  6735. preventDefault: true
  6736. }).on('drag', function () {
  6737. //console.log('drag left')
  6738. });
  6739. this.dom.box.appendChild(dragLeft);
  6740. this.dom.dragLeft = dragLeft;
  6741. }
  6742. else if (!this.selected && this.dom.dragLeft) {
  6743. // delete drag area
  6744. if (this.dom.dragLeft.parentNode) {
  6745. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  6746. }
  6747. this.dom.dragLeft = null;
  6748. }
  6749. };
  6750. /**
  6751. * Repaint a drag area on the right side of the range when the range is selected
  6752. * @protected
  6753. */
  6754. ItemRange.prototype._repaintDragRight = function () {
  6755. if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
  6756. // create and show drag area
  6757. var dragRight = document.createElement('div');
  6758. dragRight.className = 'drag-right';
  6759. dragRight.dragRightItem = this;
  6760. // TODO: this should be redundant?
  6761. Hammer(dragRight, {
  6762. preventDefault: true
  6763. }).on('drag', function () {
  6764. //console.log('drag right')
  6765. });
  6766. this.dom.box.appendChild(dragRight);
  6767. this.dom.dragRight = dragRight;
  6768. }
  6769. else if (!this.selected && this.dom.dragRight) {
  6770. // delete drag area
  6771. if (this.dom.dragRight.parentNode) {
  6772. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  6773. }
  6774. this.dom.dragRight = null;
  6775. }
  6776. };
  6777. /**
  6778. * @constructor ItemRangeOverflow
  6779. * @extends ItemRange
  6780. * @param {Object} data Object containing parameters start, end
  6781. * content, className.
  6782. * @param {Object} [options] Options to set initial property values
  6783. * @param {Object} [defaultOptions] default options
  6784. * // TODO: describe available options
  6785. */
  6786. function ItemRangeOverflow (data, options, defaultOptions) {
  6787. this.props = {
  6788. content: {
  6789. left: 0,
  6790. width: 0
  6791. }
  6792. };
  6793. ItemRange.call(this, data, options, defaultOptions);
  6794. }
  6795. ItemRangeOverflow.prototype = new ItemRange (null);
  6796. ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
  6797. /**
  6798. * Reposition the item horizontally
  6799. * @Override
  6800. */
  6801. ItemRangeOverflow.prototype.repositionX = function repositionX() {
  6802. var parentWidth = this.parent.width,
  6803. start = this.defaultOptions.toScreen(this.data.start),
  6804. end = this.defaultOptions.toScreen(this.data.end),
  6805. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  6806. contentLeft;
  6807. // limit the width of the this, as browsers cannot draw very wide divs
  6808. if (start < -parentWidth) {
  6809. start = -parentWidth;
  6810. }
  6811. if (end > 2 * parentWidth) {
  6812. end = 2 * parentWidth;
  6813. }
  6814. // when range exceeds left of the window, position the contents at the left of the visible area
  6815. contentLeft = Math.max(-start, 0);
  6816. this.left = start;
  6817. var boxWidth = Math.max(end - start, 1);
  6818. this.width = boxWidth + this.props.content.width;
  6819. // Note: The calculation of width is an optimistic calculation, giving
  6820. // a width which will not change when moving the Timeline
  6821. // So no restacking needed, which is nicer for the eye
  6822. this.dom.box.style.left = this.left + 'px';
  6823. this.dom.box.style.width = boxWidth + 'px';
  6824. this.dom.content.style.left = contentLeft + 'px';
  6825. };
  6826. /**
  6827. * @constructor Group
  6828. * @param {Number | String} groupId
  6829. * @param {Object} data
  6830. * @param {ItemSet} itemSet
  6831. */
  6832. function Group (groupId, data, itemSet) {
  6833. this.groupId = groupId;
  6834. this.itemSet = itemSet;
  6835. this.dom = {};
  6836. this.props = {
  6837. label: {
  6838. width: 0,
  6839. height: 0
  6840. }
  6841. };
  6842. this.items = {}; // items filtered by groupId of this group
  6843. this.visibleItems = []; // items currently visible in window
  6844. this.orderedItems = { // items sorted by start and by end
  6845. byStart: [],
  6846. byEnd: []
  6847. };
  6848. this._create();
  6849. this.setData(data);
  6850. }
  6851. /**
  6852. * Create DOM elements for the group
  6853. * @private
  6854. */
  6855. Group.prototype._create = function() {
  6856. var label = document.createElement('div');
  6857. label.className = 'vlabel';
  6858. this.dom.label = label;
  6859. var inner = document.createElement('div');
  6860. inner.className = 'inner';
  6861. label.appendChild(inner);
  6862. this.dom.inner = inner;
  6863. var foreground = document.createElement('div');
  6864. foreground.className = 'group';
  6865. foreground['timeline-group'] = this;
  6866. this.dom.foreground = foreground;
  6867. this.dom.background = document.createElement('div');
  6868. this.dom.axis = document.createElement('div');
  6869. // create a hidden marker to detect when the Timelines container is attached
  6870. // to the DOM, or the style of a parent of the Timeline is changed from
  6871. // display:none is changed to visible.
  6872. this.dom.marker = document.createElement('div');
  6873. this.dom.marker.style.visibility = 'hidden';
  6874. this.dom.marker.innerHTML = '?';
  6875. this.dom.background.appendChild(this.dom.marker);
  6876. };
  6877. /**
  6878. * Set the group data for this group
  6879. * @param {Object} data Group data, can contain properties content and className
  6880. */
  6881. Group.prototype.setData = function setData(data) {
  6882. // update contents
  6883. var content = data && data.content;
  6884. if (content instanceof Element) {
  6885. this.dom.inner.appendChild(content);
  6886. }
  6887. else if (content != undefined) {
  6888. this.dom.inner.innerHTML = content;
  6889. }
  6890. else {
  6891. this.dom.inner.innerHTML = this.groupId;
  6892. }
  6893. // update className
  6894. var className = data && data.className;
  6895. if (className) {
  6896. util.addClassName(this.dom.label, className);
  6897. }
  6898. };
  6899. /**
  6900. * Get the foreground container element
  6901. * @return {HTMLElement} foreground
  6902. */
  6903. Group.prototype.getForeground = function getForeground() {
  6904. return this.dom.foreground;
  6905. };
  6906. /**
  6907. * Get the background container element
  6908. * @return {HTMLElement} background
  6909. */
  6910. Group.prototype.getBackground = function getBackground() {
  6911. return this.dom.background;
  6912. };
  6913. /**
  6914. * Get the axis container element
  6915. * @return {HTMLElement} axis
  6916. */
  6917. Group.prototype.getAxis = function getAxis() {
  6918. return this.dom.axis;
  6919. };
  6920. /**
  6921. * Get the width of the group label
  6922. * @return {number} width
  6923. */
  6924. Group.prototype.getLabelWidth = function getLabelWidth() {
  6925. return this.props.label.width;
  6926. };
  6927. /**
  6928. * Repaint this group
  6929. * @param {{start: number, end: number}} range
  6930. * @param {{item: number, axis: number}} margin
  6931. * @param {boolean} [restack=false] Force restacking of all items
  6932. * @return {boolean} Returns true if the group is resized
  6933. */
  6934. Group.prototype.repaint = function repaint(range, margin, restack) {
  6935. var resized = false;
  6936. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  6937. // force recalculation of the height of the items when the marker height changed
  6938. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  6939. var markerHeight = this.dom.marker.clientHeight;
  6940. if (markerHeight != this.lastMarkerHeight) {
  6941. this.lastMarkerHeight = markerHeight;
  6942. util.forEach(this.items, function (item) {
  6943. item.dirty = true;
  6944. if (item.displayed) item.repaint();
  6945. });
  6946. restack = true;
  6947. }
  6948. // reposition visible items vertically
  6949. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  6950. stack.stack(this.visibleItems, margin, restack);
  6951. }
  6952. else { // no stacking
  6953. stack.nostack(this.visibleItems, margin);
  6954. }
  6955. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  6956. var item = this.visibleItems[i];
  6957. item.repositionY();
  6958. }
  6959. // recalculate the height of the group
  6960. var height;
  6961. var visibleItems = this.visibleItems;
  6962. if (visibleItems.length) {
  6963. var min = visibleItems[0].top;
  6964. var max = visibleItems[0].top + visibleItems[0].height;
  6965. util.forEach(visibleItems, function (item) {
  6966. min = Math.min(min, item.top);
  6967. max = Math.max(max, (item.top + item.height));
  6968. });
  6969. height = (max - min) + margin.axis + margin.item;
  6970. }
  6971. else {
  6972. height = margin.axis + margin.item;
  6973. }
  6974. height = Math.max(height, this.props.label.height);
  6975. // calculate actual size and position
  6976. var foreground = this.dom.foreground;
  6977. this.top = foreground.offsetTop;
  6978. this.left = foreground.offsetLeft;
  6979. this.width = foreground.offsetWidth;
  6980. resized = util.updateProperty(this, 'height', height) || resized;
  6981. // recalculate size of label
  6982. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  6983. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  6984. // apply new height
  6985. foreground.style.height = height + 'px';
  6986. this.dom.label.style.height = height + 'px';
  6987. return resized;
  6988. };
  6989. /**
  6990. * Show this group: attach to the DOM
  6991. */
  6992. Group.prototype.show = function show() {
  6993. if (!this.dom.label.parentNode) {
  6994. this.itemSet.getLabelSet().appendChild(this.dom.label);
  6995. }
  6996. if (!this.dom.foreground.parentNode) {
  6997. this.itemSet.getForeground().appendChild(this.dom.foreground);
  6998. }
  6999. if (!this.dom.background.parentNode) {
  7000. this.itemSet.getBackground().appendChild(this.dom.background);
  7001. }
  7002. if (!this.dom.axis.parentNode) {
  7003. this.itemSet.getAxis().appendChild(this.dom.axis);
  7004. }
  7005. };
  7006. /**
  7007. * Hide this group: remove from the DOM
  7008. */
  7009. Group.prototype.hide = function hide() {
  7010. var label = this.dom.label;
  7011. if (label.parentNode) {
  7012. label.parentNode.removeChild(label);
  7013. }
  7014. var foreground = this.dom.foreground;
  7015. if (foreground.parentNode) {
  7016. foreground.parentNode.removeChild(foreground);
  7017. }
  7018. var background = this.dom.background;
  7019. if (background.parentNode) {
  7020. background.parentNode.removeChild(background);
  7021. }
  7022. var axis = this.dom.axis;
  7023. if (axis.parentNode) {
  7024. axis.parentNode.removeChild(axis);
  7025. }
  7026. };
  7027. /**
  7028. * Add an item to the group
  7029. * @param {Item} item
  7030. */
  7031. Group.prototype.add = function add(item) {
  7032. this.items[item.id] = item;
  7033. item.setParent(this);
  7034. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  7035. var range = this.itemSet.range; // TODO: not nice accessing the range like this
  7036. this._checkIfVisible(item, this.visibleItems, range);
  7037. }
  7038. };
  7039. /**
  7040. * Remove an item from the group
  7041. * @param {Item} item
  7042. */
  7043. Group.prototype.remove = function remove(item) {
  7044. delete this.items[item.id];
  7045. item.setParent(this.itemSet);
  7046. // remove from visible items
  7047. var index = this.visibleItems.indexOf(item);
  7048. if (index != -1) this.visibleItems.splice(index, 1);
  7049. // TODO: also remove from ordered items?
  7050. };
  7051. /**
  7052. * Remove an item from the corresponding DataSet
  7053. * @param {Item} item
  7054. */
  7055. Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
  7056. this.itemSet.removeItem(item.id);
  7057. };
  7058. /**
  7059. * Reorder the items
  7060. */
  7061. Group.prototype.order = function order() {
  7062. var array = util.toArray(this.items);
  7063. this.orderedItems.byStart = array;
  7064. this.orderedItems.byEnd = this._constructByEndArray(array);
  7065. stack.orderByStart(this.orderedItems.byStart);
  7066. stack.orderByEnd(this.orderedItems.byEnd);
  7067. };
  7068. /**
  7069. * Create an array containing all items being a range (having an end date)
  7070. * @param {Item[]} array
  7071. * @returns {ItemRange[]}
  7072. * @private
  7073. */
  7074. Group.prototype._constructByEndArray = function _constructByEndArray(array) {
  7075. var endArray = [];
  7076. for (var i = 0; i < array.length; i++) {
  7077. if (array[i] instanceof ItemRange) {
  7078. endArray.push(array[i]);
  7079. }
  7080. }
  7081. return endArray;
  7082. };
  7083. /**
  7084. * Update the visible items
  7085. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  7086. * @param {Item[]} visibleItems The previously visible items.
  7087. * @param {{start: number, end: number}} range Visible range
  7088. * @return {Item[]} visibleItems The new visible items.
  7089. * @private
  7090. */
  7091. Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
  7092. var initialPosByStart,
  7093. newVisibleItems = [],
  7094. i;
  7095. // first check if the items that were in view previously are still in view.
  7096. // this handles the case for the ItemRange that is both before and after the current one.
  7097. if (visibleItems.length > 0) {
  7098. for (i = 0; i < visibleItems.length; i++) {
  7099. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  7100. }
  7101. }
  7102. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  7103. if (newVisibleItems.length == 0) {
  7104. initialPosByStart = this._binarySearch(orderedItems, range, false);
  7105. }
  7106. else {
  7107. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  7108. }
  7109. // use visible search to find a visible ItemRange (only based on endTime)
  7110. var initialPosByEnd = this._binarySearch(orderedItems, range, true);
  7111. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  7112. if (initialPosByStart != -1) {
  7113. for (i = initialPosByStart; i >= 0; i--) {
  7114. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  7115. }
  7116. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  7117. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  7118. }
  7119. }
  7120. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  7121. if (initialPosByEnd != -1) {
  7122. for (i = initialPosByEnd; i >= 0; i--) {
  7123. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  7124. }
  7125. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  7126. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  7127. }
  7128. }
  7129. return newVisibleItems;
  7130. };
  7131. /**
  7132. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  7133. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  7134. * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
  7135. * if the time we selected (start or end) is within the current range).
  7136. *
  7137. * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
  7138. * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
  7139. * either the start OR end time has to be in the range.
  7140. *
  7141. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
  7142. * @param {{start: number, end: number}} range
  7143. * @param {Boolean} byEnd
  7144. * @returns {number}
  7145. * @private
  7146. */
  7147. Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
  7148. var array = [];
  7149. var byTime = byEnd ? 'end' : 'start';
  7150. if (byEnd == true) {array = orderedItems.byEnd; }
  7151. else {array = orderedItems.byStart;}
  7152. var interval = range.end - range.start;
  7153. var found = false;
  7154. var low = 0;
  7155. var high = array.length;
  7156. var guess = Math.floor(0.5*(high+low));
  7157. var newGuess;
  7158. if (high == 0) {guess = -1;}
  7159. else if (high == 1) {
  7160. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  7161. guess = 0;
  7162. }
  7163. else {
  7164. guess = -1;
  7165. }
  7166. }
  7167. else {
  7168. high -= 1;
  7169. while (found == false) {
  7170. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  7171. found = true;
  7172. }
  7173. else {
  7174. if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
  7175. low = Math.floor(0.5*(high+low));
  7176. }
  7177. else { // it is too big --> decrease high
  7178. high = Math.floor(0.5*(high+low));
  7179. }
  7180. newGuess = Math.floor(0.5*(high+low));
  7181. // not in list;
  7182. if (guess == newGuess) {
  7183. guess = -1;
  7184. found = true;
  7185. }
  7186. else {
  7187. guess = newGuess;
  7188. }
  7189. }
  7190. }
  7191. }
  7192. return guess;
  7193. };
  7194. /**
  7195. * this function checks if an item is invisible. If it is NOT we make it visible
  7196. * and add it to the global visible items. If it is, return true.
  7197. *
  7198. * @param {Item} item
  7199. * @param {Item[]} visibleItems
  7200. * @param {{start:number, end:number}} range
  7201. * @returns {boolean}
  7202. * @private
  7203. */
  7204. Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
  7205. if (item.isVisible(range)) {
  7206. if (!item.displayed) item.show();
  7207. item.repositionX();
  7208. if (visibleItems.indexOf(item) == -1) {
  7209. visibleItems.push(item);
  7210. }
  7211. return false;
  7212. }
  7213. else {
  7214. return true;
  7215. }
  7216. };
  7217. /**
  7218. * this function is very similar to the _checkIfInvisible() but it does not
  7219. * return booleans, hides the item if it should not be seen and always adds to
  7220. * the visibleItems.
  7221. * this one is for brute forcing and hiding.
  7222. *
  7223. * @param {Item} item
  7224. * @param {Array} visibleItems
  7225. * @param {{start:number, end:number}} range
  7226. * @private
  7227. */
  7228. Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
  7229. if (item.isVisible(range)) {
  7230. if (!item.displayed) item.show();
  7231. // reposition item horizontally
  7232. item.repositionX();
  7233. visibleItems.push(item);
  7234. }
  7235. else {
  7236. if (item.displayed) item.hide();
  7237. }
  7238. };
  7239. /**
  7240. * Create a timeline visualization
  7241. * @param {HTMLElement} container
  7242. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  7243. * @param {Object} [options] See Timeline.setOptions for the available options.
  7244. * @constructor
  7245. */
  7246. function Timeline (container, items, options) {
  7247. // validate arguments
  7248. if (!container) throw new Error('No container element provided');
  7249. var me = this;
  7250. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  7251. this.options = {
  7252. orientation: 'bottom',
  7253. direction: 'horizontal', // 'horizontal' or 'vertical'
  7254. autoResize: true,
  7255. stack: true,
  7256. editable: {
  7257. updateTime: false,
  7258. updateGroup: false,
  7259. add: false,
  7260. remove: false
  7261. },
  7262. selectable: true,
  7263. snap: null, // will be specified after timeaxis is created
  7264. min: null,
  7265. max: null,
  7266. zoomMin: 10, // milliseconds
  7267. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  7268. // moveable: true, // TODO: option moveable
  7269. // zoomable: true, // TODO: option zoomable
  7270. showMinorLabels: true,
  7271. showMajorLabels: true,
  7272. showCurrentTime: false,
  7273. showCustomTime: false,
  7274. type: 'box',
  7275. align: 'center',
  7276. margin: {
  7277. axis: 20,
  7278. item: 10
  7279. },
  7280. padding: 5,
  7281. onAdd: function (item, callback) {
  7282. callback(item);
  7283. },
  7284. onUpdate: function (item, callback) {
  7285. callback(item);
  7286. },
  7287. onMove: function (item, callback) {
  7288. callback(item);
  7289. },
  7290. onRemove: function (item, callback) {
  7291. callback(item);
  7292. },
  7293. toScreen: me._toScreen.bind(me),
  7294. toTime: me._toTime.bind(me)
  7295. };
  7296. // root panel
  7297. var rootOptions = util.extend(Object.create(this.options), {
  7298. height: function () {
  7299. if (me.options.height) {
  7300. // fixed height
  7301. return me.options.height;
  7302. }
  7303. else {
  7304. // auto height
  7305. // TODO: implement a css based solution to automatically have the right hight
  7306. return (me.timeAxis.height + me.contentPanel.height) + 'px';
  7307. }
  7308. }
  7309. });
  7310. this.rootPanel = new RootPanel(container, rootOptions);
  7311. // single select (or unselect) when tapping an item
  7312. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  7313. // multi select when holding mouse/touch, or on ctrl+click
  7314. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  7315. // add item on doubletap
  7316. this.rootPanel.on('doubletap', this._onAddItem.bind(this));
  7317. // side panel
  7318. var sideOptions = util.extend(Object.create(this.options), {
  7319. top: function () {
  7320. return (sideOptions.orientation == 'top') ? '0' : '';
  7321. },
  7322. bottom: function () {
  7323. return (sideOptions.orientation == 'top') ? '' : '0';
  7324. },
  7325. left: '0',
  7326. right: null,
  7327. height: '100%',
  7328. width: function () {
  7329. if (me.itemSet) {
  7330. // return me.itemSet.getLabelsWidth();
  7331. return "100px";
  7332. }
  7333. else {
  7334. return 0;
  7335. }
  7336. },
  7337. className: function () {
  7338. return 'side' + (me.groupsData ? '' : ' hidden');
  7339. }
  7340. });
  7341. this.sidePanel = new Panel(sideOptions);
  7342. this.rootPanel.appendChild(this.sidePanel);
  7343. // main panel (contains time axis and itemsets)
  7344. var mainOptions = util.extend(Object.create(this.options), {
  7345. left: function () {
  7346. // we align left to enable a smooth resizing of the window
  7347. return me.sidePanel.width;
  7348. },
  7349. right: null,
  7350. height: '100%',
  7351. width: function () {
  7352. return me.rootPanel.width - me.sidePanel.width;
  7353. },
  7354. className: 'main'
  7355. });
  7356. this.mainPanel = new Panel(mainOptions);
  7357. this.rootPanel.appendChild(this.mainPanel);
  7358. // range
  7359. // TODO: move range inside rootPanel?
  7360. var rangeOptions = Object.create(this.options);
  7361. this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
  7362. this.range.setRange(
  7363. now.clone().add('days', -3).valueOf(),
  7364. now.clone().add('days', 4).valueOf()
  7365. );
  7366. this.range.on('rangechange', function (properties) {
  7367. me.rootPanel.repaint();
  7368. me.emit('rangechange', properties);
  7369. });
  7370. this.range.on('rangechanged', function (properties) {
  7371. me.rootPanel.repaint();
  7372. me.emit('rangechanged', properties);
  7373. });
  7374. // panel with time axis
  7375. var timeAxisOptions = util.extend(Object.create(rootOptions), {
  7376. range: this.range,
  7377. left: null,
  7378. top: null,
  7379. width: null,
  7380. height: null
  7381. });
  7382. this.timeAxis = new TimeAxis(timeAxisOptions);
  7383. this.timeAxis.setRange(this.range);
  7384. this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
  7385. this.mainPanel.appendChild(this.timeAxis);
  7386. // content panel (contains itemset(s))
  7387. var contentOptions = util.extend(Object.create(this.options), {
  7388. top: function () {
  7389. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  7390. },
  7391. bottom: function () {
  7392. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  7393. },
  7394. left: null,
  7395. right: null,
  7396. height: null,
  7397. width: null,
  7398. className: 'content'
  7399. });
  7400. this.contentPanel = new Panel(contentOptions);
  7401. this.mainPanel.appendChild(this.contentPanel);
  7402. // content panel (contains the vertical lines of box items)
  7403. var backgroundOptions = util.extend(Object.create(this.options), {
  7404. top: function () {
  7405. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  7406. },
  7407. bottom: function () {
  7408. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  7409. },
  7410. left: null,
  7411. right: null,
  7412. height: function () {
  7413. return me.contentPanel.height;
  7414. },
  7415. width: null,
  7416. className: 'background'
  7417. });
  7418. this.backgroundPanel = new Panel(backgroundOptions);
  7419. this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
  7420. // panel with axis holding the dots of item boxes
  7421. var axisPanelOptions = util.extend(Object.create(rootOptions), {
  7422. left: 0,
  7423. top: function () {
  7424. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  7425. },
  7426. bottom: function () {
  7427. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  7428. },
  7429. width: '100%',
  7430. height: 0,
  7431. className: 'axis'
  7432. });
  7433. this.axisPanel = new Panel(axisPanelOptions);
  7434. this.mainPanel.appendChild(this.axisPanel);
  7435. // content panel (contains itemset(s))
  7436. var sideContentOptions = util.extend(Object.create(this.options), {
  7437. top: function () {
  7438. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  7439. },
  7440. bottom: function () {
  7441. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  7442. },
  7443. left: null,
  7444. right: null,
  7445. height: null,
  7446. width: null,
  7447. className: 'side-content'
  7448. });
  7449. this.sideContentPanel = new Panel(sideContentOptions);
  7450. this.sidePanel.appendChild(this.sideContentPanel);
  7451. // current time bar
  7452. // Note: time bar will be attached in this.setOptions when selected
  7453. this.currentTime = new CurrentTime(this.range, rootOptions);
  7454. // custom time bar
  7455. // Note: time bar will be attached in this.setOptions when selected
  7456. this.customTime = new CustomTime(rootOptions);
  7457. this.customTime.on('timechange', function (time) {
  7458. me.emit('timechange', time);
  7459. });
  7460. this.customTime.on('timechanged', function (time) {
  7461. me.emit('timechanged', time);
  7462. });
  7463. // itemset containing items and groups
  7464. var itemOptions = util.extend(Object.create(this.options), {
  7465. left: null,
  7466. right: null,
  7467. top: null,
  7468. bottom: null,
  7469. width: null,
  7470. height: null
  7471. });
  7472. this.linegraph = new Linegraph(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions, this, this.sidePanel);
  7473. this.linegraph.setRange(this.range);
  7474. this.linegraph.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  7475. this.contentPanel.appendChild(this.linegraph);
  7476. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
  7477. this.itemSet.setRange(this.range);
  7478. this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  7479. this.contentPanel.appendChild(this.itemSet);
  7480. this.itemsData = null; // DataSet
  7481. this.groupsData = null; // DataSet
  7482. // apply options
  7483. if (options) {
  7484. this.setOptions(options);
  7485. }
  7486. // create itemset
  7487. if (items) {
  7488. this.setItems(items);
  7489. }
  7490. }
  7491. // turn Timeline into an event emitter
  7492. Emitter(Timeline.prototype);
  7493. /**
  7494. * Set options
  7495. * @param {Object} options TODO: describe the available options
  7496. */
  7497. Timeline.prototype.setOptions = function (options) {
  7498. util.extend(this.options, options);
  7499. if ('editable' in options) {
  7500. var isBoolean = typeof options.editable === 'boolean';
  7501. this.options.editable = {
  7502. updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
  7503. updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
  7504. add: isBoolean ? options.editable : (options.editable.add || false),
  7505. remove: isBoolean ? options.editable : (options.editable.remove || false)
  7506. };
  7507. }
  7508. // force update of range (apply new min/max etc.)
  7509. // both start and end are optional
  7510. this.range.setRange(options.start, options.end);
  7511. if ('editable' in options || 'selectable' in options) {
  7512. if (this.options.selectable) {
  7513. // force update of selection
  7514. this.setSelection(this.getSelection());
  7515. }
  7516. else {
  7517. // remove selection
  7518. this.setSelection([]);
  7519. }
  7520. }
  7521. // force the itemSet to refresh: options like orientation and margins may be changed
  7522. this.itemSet.markDirty();
  7523. // validate the callback functions
  7524. var validateCallback = (function (fn) {
  7525. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  7526. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  7527. }
  7528. }).bind(this);
  7529. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  7530. // add/remove the current time bar
  7531. if (this.options.showCurrentTime) {
  7532. if (!this.mainPanel.hasChild(this.currentTime)) {
  7533. this.mainPanel.appendChild(this.currentTime);
  7534. this.currentTime.start();
  7535. }
  7536. }
  7537. else {
  7538. if (this.mainPanel.hasChild(this.currentTime)) {
  7539. this.currentTime.stop();
  7540. this.mainPanel.removeChild(this.currentTime);
  7541. }
  7542. }
  7543. // add/remove the custom time bar
  7544. if (this.options.showCustomTime) {
  7545. if (!this.mainPanel.hasChild(this.customTime)) {
  7546. this.mainPanel.appendChild(this.customTime);
  7547. }
  7548. }
  7549. else {
  7550. if (this.mainPanel.hasChild(this.customTime)) {
  7551. this.mainPanel.removeChild(this.customTime);
  7552. }
  7553. }
  7554. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  7555. if (options && options.order) {
  7556. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  7557. }
  7558. // repaint everything
  7559. this.rootPanel.repaint();
  7560. };
  7561. /**
  7562. * Set a custom time bar
  7563. * @param {Date} time
  7564. */
  7565. Timeline.prototype.setCustomTime = function (time) {
  7566. if (!this.customTime) {
  7567. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7568. }
  7569. this.customTime.setCustomTime(time);
  7570. };
  7571. /**
  7572. * Retrieve the current custom time.
  7573. * @return {Date} customTime
  7574. */
  7575. Timeline.prototype.getCustomTime = function() {
  7576. if (!this.customTime) {
  7577. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7578. }
  7579. return this.customTime.getCustomTime();
  7580. };
  7581. /**
  7582. * Set items
  7583. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  7584. */
  7585. Timeline.prototype.setItems = function(items) {
  7586. var initialLoad = (this.itemsData == null);
  7587. // convert to type DataSet when needed
  7588. var newDataSet;
  7589. if (!items) {
  7590. newDataSet = null;
  7591. }
  7592. else if (items instanceof DataSet || items instanceof DataView) {
  7593. newDataSet = items;
  7594. }
  7595. else {
  7596. // turn an array into a dataset
  7597. newDataSet = new DataSet(items, {
  7598. convert: {
  7599. start: 'Date',
  7600. end: 'Date'
  7601. }
  7602. });
  7603. }
  7604. // set items
  7605. this.itemsData = newDataSet;
  7606. this.itemSet.setItems(newDataSet);
  7607. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  7608. this.fit();
  7609. var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
  7610. var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
  7611. this.setWindow(start, end);
  7612. }
  7613. };
  7614. /**
  7615. * Set groups
  7616. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  7617. */
  7618. Timeline.prototype.setGroups = function setGroups(groups) {
  7619. // convert to type DataSet when needed
  7620. var newDataSet;
  7621. if (!groups) {
  7622. newDataSet = null;
  7623. }
  7624. else if (groups instanceof DataSet || groups instanceof DataView) {
  7625. newDataSet = groups;
  7626. }
  7627. else {
  7628. // turn an array into a dataset
  7629. newDataSet = new DataSet(groups);
  7630. }
  7631. this.groupsData = newDataSet;
  7632. this.itemSet.setGroups(newDataSet);
  7633. };
  7634. /**
  7635. * Set Timeline window such that it fits all items
  7636. */
  7637. Timeline.prototype.fit = function fit() {
  7638. // apply the data range as range
  7639. var dataRange = this.getItemRange();
  7640. // add 5% space on both sides
  7641. var start = dataRange.min;
  7642. var end = dataRange.max;
  7643. if (start != null && end != null) {
  7644. var interval = (end.valueOf() - start.valueOf());
  7645. if (interval <= 0) {
  7646. // prevent an empty interval
  7647. interval = 24 * 60 * 60 * 1000; // 1 day
  7648. }
  7649. start = new Date(start.valueOf() - interval * 0.05);
  7650. end = new Date(end.valueOf() + interval * 0.05);
  7651. }
  7652. // skip range set if there is no start and end date
  7653. if (start === null && end === null) {
  7654. return;
  7655. }
  7656. this.range.setRange(start, end);
  7657. };
  7658. /**
  7659. * Get the data range of the item set.
  7660. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  7661. * When no minimum is found, min==null
  7662. * When no maximum is found, max==null
  7663. */
  7664. Timeline.prototype.getItemRange = function getItemRange() {
  7665. // calculate min from start filed
  7666. var itemsData = this.itemsData,
  7667. min = null,
  7668. max = null;
  7669. if (itemsData) {
  7670. // calculate the minimum value of the field 'start'
  7671. var minItem = itemsData.min('start');
  7672. min = minItem ? minItem.start.valueOf() : null;
  7673. // calculate maximum value of fields 'start' and 'end'
  7674. var maxStartItem = itemsData.max('start');
  7675. if (maxStartItem) {
  7676. max = maxStartItem.start.valueOf();
  7677. }
  7678. var maxEndItem = itemsData.max('end');
  7679. if (maxEndItem) {
  7680. if (max == null) {
  7681. max = maxEndItem.end.valueOf();
  7682. }
  7683. else {
  7684. max = Math.max(max, maxEndItem.end.valueOf());
  7685. }
  7686. }
  7687. }
  7688. return {
  7689. min: (min != null) ? new Date(min) : null,
  7690. max: (max != null) ? new Date(max) : null
  7691. };
  7692. };
  7693. /**
  7694. * Set selected items by their id. Replaces the current selection
  7695. * Unknown id's are silently ignored.
  7696. * @param {Array} [ids] An array with zero or more id's of the items to be
  7697. * selected. If ids is an empty array, all items will be
  7698. * unselected.
  7699. */
  7700. Timeline.prototype.setSelection = function setSelection (ids) {
  7701. this.itemSet.setSelection(ids);
  7702. };
  7703. /**
  7704. * Get the selected items by their id
  7705. * @return {Array} ids The ids of the selected items
  7706. */
  7707. Timeline.prototype.getSelection = function getSelection() {
  7708. return this.itemSet.getSelection();
  7709. };
  7710. /**
  7711. * Set the visible window. Both parameters are optional, you can change only
  7712. * start or only end. Syntax:
  7713. *
  7714. * TimeLine.setWindow(start, end)
  7715. * TimeLine.setWindow(range)
  7716. *
  7717. * Where start and end can be a Date, number, or string, and range is an
  7718. * object with properties start and end.
  7719. *
  7720. * @param {Date | Number | String | Object} [start] Start date of visible window
  7721. * @param {Date | Number | String} [end] End date of visible window
  7722. */
  7723. Timeline.prototype.setWindow = function setWindow(start, end) {
  7724. if (arguments.length == 1) {
  7725. var range = arguments[0];
  7726. this.range.setRange(range.start, range.end);
  7727. }
  7728. else {
  7729. this.range.setRange(start, end);
  7730. }
  7731. };
  7732. /**
  7733. * Get the visible window
  7734. * @return {{start: Date, end: Date}} Visible range
  7735. */
  7736. Timeline.prototype.getWindow = function setWindow() {
  7737. var range = this.range.getRange();
  7738. return {
  7739. start: new Date(range.start),
  7740. end: new Date(range.end)
  7741. };
  7742. };
  7743. /**
  7744. * Force a repaint of the Timeline. Can be useful to manually repaint when
  7745. * option autoResize=false
  7746. */
  7747. Timeline.prototype.repaint = function repaint() {
  7748. this.rootPanel.repaint();
  7749. };
  7750. /**
  7751. * Handle selecting/deselecting an item when tapping it
  7752. * @param {Event} event
  7753. * @private
  7754. */
  7755. // TODO: move this function to ItemSet
  7756. Timeline.prototype._onSelectItem = function (event) {
  7757. if (!this.options.selectable) return;
  7758. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  7759. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  7760. if (ctrlKey || shiftKey) {
  7761. this._onMultiSelectItem(event);
  7762. return;
  7763. }
  7764. var oldSelection = this.getSelection();
  7765. var item = ItemSet.itemFromTarget(event);
  7766. var selection = item ? [item.id] : [];
  7767. this.setSelection(selection);
  7768. var newSelection = this.getSelection();
  7769. // if selection is changed, emit a select event
  7770. if (!util.equalArray(oldSelection, newSelection)) {
  7771. this.emit('select', {
  7772. items: this.getSelection()
  7773. });
  7774. }
  7775. event.stopPropagation();
  7776. };
  7777. /**
  7778. * Handle creation and updates of an item on double tap
  7779. * @param event
  7780. * @private
  7781. */
  7782. Timeline.prototype._onAddItem = function (event) {
  7783. if (!this.options.selectable) return;
  7784. if (!this.options.editable.add) return;
  7785. var me = this,
  7786. item = ItemSet.itemFromTarget(event);
  7787. if (item) {
  7788. // update item
  7789. // execute async handler to update the item (or cancel it)
  7790. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  7791. this.options.onUpdate(itemData, function (itemData) {
  7792. if (itemData) {
  7793. me.itemsData.update(itemData);
  7794. }
  7795. });
  7796. }
  7797. else {
  7798. // add item
  7799. var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
  7800. var x = event.gesture.center.pageX - xAbs;
  7801. var newItem = {
  7802. start: this.timeAxis.snap(this._toTime(x)),
  7803. content: 'new item'
  7804. };
  7805. // when default type is a range, add a default end date to the new item
  7806. if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
  7807. newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
  7808. }
  7809. var id = util.randomUUID();
  7810. newItem[this.itemsData.fieldId] = id;
  7811. var group = ItemSet.groupFromTarget(event);
  7812. if (group) {
  7813. newItem.group = group.groupId;
  7814. }
  7815. // execute async handler to customize (or cancel) adding an item
  7816. this.options.onAdd(newItem, function (item) {
  7817. if (item) {
  7818. me.itemsData.add(newItem);
  7819. // TODO: need to trigger a repaint?
  7820. }
  7821. });
  7822. }
  7823. };
  7824. /**
  7825. * Handle selecting/deselecting multiple items when holding an item
  7826. * @param {Event} event
  7827. * @private
  7828. */
  7829. // TODO: move this function to ItemSet
  7830. Timeline.prototype._onMultiSelectItem = function (event) {
  7831. if (!this.options.selectable) return;
  7832. var selection,
  7833. item = ItemSet.itemFromTarget(event);
  7834. if (item) {
  7835. // multi select items
  7836. selection = this.getSelection(); // current selection
  7837. var index = selection.indexOf(item.id);
  7838. if (index == -1) {
  7839. // item is not yet selected -> select it
  7840. selection.push(item.id);
  7841. }
  7842. else {
  7843. // item is already selected -> deselect it
  7844. selection.splice(index, 1);
  7845. }
  7846. this.setSelection(selection);
  7847. this.emit('select', {
  7848. items: this.getSelection()
  7849. });
  7850. event.stopPropagation();
  7851. }
  7852. };
  7853. /**
  7854. * Convert a position on screen (pixels) to a datetime
  7855. * @param {int} x Position on the screen in pixels
  7856. * @return {Date} time The datetime the corresponds with given position x
  7857. * @private
  7858. */
  7859. Timeline.prototype._toTime = function _toTime(x) {
  7860. var conversion = this.range.conversion(this.mainPanel.width);
  7861. return new Date(x / conversion.scale + conversion.offset);
  7862. };
  7863. /**
  7864. * Convert a datetime (Date object) into a position on the screen
  7865. * @param {Date} time A date
  7866. * @return {int} x The position on the screen in pixels which corresponds
  7867. * with the given date.
  7868. * @private
  7869. */
  7870. Timeline.prototype._toScreen = function _toScreen(time) {
  7871. var conversion = this.range.conversion(this.mainPanel.width);
  7872. return (time.valueOf() - conversion.offset) * conversion.scale;
  7873. };
  7874. (function(exports) {
  7875. /**
  7876. * Parse a text source containing data in DOT language into a JSON object.
  7877. * The object contains two lists: one with nodes and one with edges.
  7878. *
  7879. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  7880. *
  7881. * @param {String} data Text containing a graph in DOT-notation
  7882. * @return {Object} graph An object containing two parameters:
  7883. * {Object[]} nodes
  7884. * {Object[]} edges
  7885. */
  7886. function parseDOT (data) {
  7887. dot = data;
  7888. return parseGraph();
  7889. }
  7890. // token types enumeration
  7891. var TOKENTYPE = {
  7892. NULL : 0,
  7893. DELIMITER : 1,
  7894. IDENTIFIER: 2,
  7895. UNKNOWN : 3
  7896. };
  7897. // map with all delimiters
  7898. var DELIMITERS = {
  7899. '{': true,
  7900. '}': true,
  7901. '[': true,
  7902. ']': true,
  7903. ';': true,
  7904. '=': true,
  7905. ',': true,
  7906. '->': true,
  7907. '--': true
  7908. };
  7909. var dot = ''; // current dot file
  7910. var index = 0; // current index in dot file
  7911. var c = ''; // current token character in expr
  7912. var token = ''; // current token
  7913. var tokenType = TOKENTYPE.NULL; // type of the token
  7914. /**
  7915. * Get the first character from the dot file.
  7916. * The character is stored into the char c. If the end of the dot file is
  7917. * reached, the function puts an empty string in c.
  7918. */
  7919. function first() {
  7920. index = 0;
  7921. c = dot.charAt(0);
  7922. }
  7923. /**
  7924. * Get the next character from the dot file.
  7925. * The character is stored into the char c. If the end of the dot file is
  7926. * reached, the function puts an empty string in c.
  7927. */
  7928. function next() {
  7929. index++;
  7930. c = dot.charAt(index);
  7931. }
  7932. /**
  7933. * Preview the next character from the dot file.
  7934. * @return {String} cNext
  7935. */
  7936. function nextPreview() {
  7937. return dot.charAt(index + 1);
  7938. }
  7939. /**
  7940. * Test whether given character is alphabetic or numeric
  7941. * @param {String} c
  7942. * @return {Boolean} isAlphaNumeric
  7943. */
  7944. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  7945. function isAlphaNumeric(c) {
  7946. return regexAlphaNumeric.test(c);
  7947. }
  7948. /**
  7949. * Merge all properties of object b into object b
  7950. * @param {Object} a
  7951. * @param {Object} b
  7952. * @return {Object} a
  7953. */
  7954. function merge (a, b) {
  7955. if (!a) {
  7956. a = {};
  7957. }
  7958. if (b) {
  7959. for (var name in b) {
  7960. if (b.hasOwnProperty(name)) {
  7961. a[name] = b[name];
  7962. }
  7963. }
  7964. }
  7965. return a;
  7966. }
  7967. /**
  7968. * Set a value in an object, where the provided parameter name can be a
  7969. * path with nested parameters. For example:
  7970. *
  7971. * var obj = {a: 2};
  7972. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  7973. *
  7974. * @param {Object} obj
  7975. * @param {String} path A parameter name or dot-separated parameter path,
  7976. * like "color.highlight.border".
  7977. * @param {*} value
  7978. */
  7979. function setValue(obj, path, value) {
  7980. var keys = path.split('.');
  7981. var o = obj;
  7982. while (keys.length) {
  7983. var key = keys.shift();
  7984. if (keys.length) {
  7985. // this isn't the end point
  7986. if (!o[key]) {
  7987. o[key] = {};
  7988. }
  7989. o = o[key];
  7990. }
  7991. else {
  7992. // this is the end point
  7993. o[key] = value;
  7994. }
  7995. }
  7996. }
  7997. /**
  7998. * Add a node to a graph object. If there is already a node with
  7999. * the same id, their attributes will be merged.
  8000. * @param {Object} graph
  8001. * @param {Object} node
  8002. */
  8003. function addNode(graph, node) {
  8004. var i, len;
  8005. var current = null;
  8006. // find root graph (in case of subgraph)
  8007. var graphs = [graph]; // list with all graphs from current graph to root graph
  8008. var root = graph;
  8009. while (root.parent) {
  8010. graphs.push(root.parent);
  8011. root = root.parent;
  8012. }
  8013. // find existing node (at root level) by its id
  8014. if (root.nodes) {
  8015. for (i = 0, len = root.nodes.length; i < len; i++) {
  8016. if (node.id === root.nodes[i].id) {
  8017. current = root.nodes[i];
  8018. break;
  8019. }
  8020. }
  8021. }
  8022. if (!current) {
  8023. // this is a new node
  8024. current = {
  8025. id: node.id
  8026. };
  8027. if (graph.node) {
  8028. // clone default attributes
  8029. current.attr = merge(current.attr, graph.node);
  8030. }
  8031. }
  8032. // add node to this (sub)graph and all its parent graphs
  8033. for (i = graphs.length - 1; i >= 0; i--) {
  8034. var g = graphs[i];
  8035. if (!g.nodes) {
  8036. g.nodes = [];
  8037. }
  8038. if (g.nodes.indexOf(current) == -1) {
  8039. g.nodes.push(current);
  8040. }
  8041. }
  8042. // merge attributes
  8043. if (node.attr) {
  8044. current.attr = merge(current.attr, node.attr);
  8045. }
  8046. }
  8047. /**
  8048. * Add an edge to a graph object
  8049. * @param {Object} graph
  8050. * @param {Object} edge
  8051. */
  8052. function addEdge(graph, edge) {
  8053. if (!graph.edges) {
  8054. graph.edges = [];
  8055. }
  8056. graph.edges.push(edge);
  8057. if (graph.edge) {
  8058. var attr = merge({}, graph.edge); // clone default attributes
  8059. edge.attr = merge(attr, edge.attr); // merge attributes
  8060. }
  8061. }
  8062. /**
  8063. * Create an edge to a graph object
  8064. * @param {Object} graph
  8065. * @param {String | Number | Object} from
  8066. * @param {String | Number | Object} to
  8067. * @param {String} type
  8068. * @param {Object | null} attr
  8069. * @return {Object} edge
  8070. */
  8071. function createEdge(graph, from, to, type, attr) {
  8072. var edge = {
  8073. from: from,
  8074. to: to,
  8075. type: type
  8076. };
  8077. if (graph.edge) {
  8078. edge.attr = merge({}, graph.edge); // clone default attributes
  8079. }
  8080. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  8081. return edge;
  8082. }
  8083. /**
  8084. * Get next token in the current dot file.
  8085. * The token and token type are available as token and tokenType
  8086. */
  8087. function getToken() {
  8088. tokenType = TOKENTYPE.NULL;
  8089. token = '';
  8090. // skip over whitespaces
  8091. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  8092. next();
  8093. }
  8094. do {
  8095. var isComment = false;
  8096. // skip comment
  8097. if (c == '#') {
  8098. // find the previous non-space character
  8099. var i = index - 1;
  8100. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  8101. i--;
  8102. }
  8103. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  8104. // the # is at the start of a line, this is indeed a line comment
  8105. while (c != '' && c != '\n') {
  8106. next();
  8107. }
  8108. isComment = true;
  8109. }
  8110. }
  8111. if (c == '/' && nextPreview() == '/') {
  8112. // skip line comment
  8113. while (c != '' && c != '\n') {
  8114. next();
  8115. }
  8116. isComment = true;
  8117. }
  8118. if (c == '/' && nextPreview() == '*') {
  8119. // skip block comment
  8120. while (c != '') {
  8121. if (c == '*' && nextPreview() == '/') {
  8122. // end of block comment found. skip these last two characters
  8123. next();
  8124. next();
  8125. break;
  8126. }
  8127. else {
  8128. next();
  8129. }
  8130. }
  8131. isComment = true;
  8132. }
  8133. // skip over whitespaces
  8134. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  8135. next();
  8136. }
  8137. }
  8138. while (isComment);
  8139. // check for end of dot file
  8140. if (c == '') {
  8141. // token is still empty
  8142. tokenType = TOKENTYPE.DELIMITER;
  8143. return;
  8144. }
  8145. // check for delimiters consisting of 2 characters
  8146. var c2 = c + nextPreview();
  8147. if (DELIMITERS[c2]) {
  8148. tokenType = TOKENTYPE.DELIMITER;
  8149. token = c2;
  8150. next();
  8151. next();
  8152. return;
  8153. }
  8154. // check for delimiters consisting of 1 character
  8155. if (DELIMITERS[c]) {
  8156. tokenType = TOKENTYPE.DELIMITER;
  8157. token = c;
  8158. next();
  8159. return;
  8160. }
  8161. // check for an identifier (number or string)
  8162. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  8163. if (isAlphaNumeric(c) || c == '-') {
  8164. token += c;
  8165. next();
  8166. while (isAlphaNumeric(c)) {
  8167. token += c;
  8168. next();
  8169. }
  8170. if (token == 'false') {
  8171. token = false; // convert to boolean
  8172. }
  8173. else if (token == 'true') {
  8174. token = true; // convert to boolean
  8175. }
  8176. else if (!isNaN(Number(token))) {
  8177. token = Number(token); // convert to number
  8178. }
  8179. tokenType = TOKENTYPE.IDENTIFIER;
  8180. return;
  8181. }
  8182. // check for a string enclosed by double quotes
  8183. if (c == '"') {
  8184. next();
  8185. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  8186. token += c;
  8187. if (c == '"') { // skip the escape character
  8188. next();
  8189. }
  8190. next();
  8191. }
  8192. if (c != '"') {
  8193. throw newSyntaxError('End of string " expected');
  8194. }
  8195. next();
  8196. tokenType = TOKENTYPE.IDENTIFIER;
  8197. return;
  8198. }
  8199. // something unknown is found, wrong characters, a syntax error
  8200. tokenType = TOKENTYPE.UNKNOWN;
  8201. while (c != '') {
  8202. token += c;
  8203. next();
  8204. }
  8205. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  8206. }
  8207. /**
  8208. * Parse a graph.
  8209. * @returns {Object} graph
  8210. */
  8211. function parseGraph() {
  8212. var graph = {};
  8213. first();
  8214. getToken();
  8215. // optional strict keyword
  8216. if (token == 'strict') {
  8217. graph.strict = true;
  8218. getToken();
  8219. }
  8220. // graph or digraph keyword
  8221. if (token == 'graph' || token == 'digraph') {
  8222. graph.type = token;
  8223. getToken();
  8224. }
  8225. // optional graph id
  8226. if (tokenType == TOKENTYPE.IDENTIFIER) {
  8227. graph.id = token;
  8228. getToken();
  8229. }
  8230. // open angle bracket
  8231. if (token != '{') {
  8232. throw newSyntaxError('Angle bracket { expected');
  8233. }
  8234. getToken();
  8235. // statements
  8236. parseStatements(graph);
  8237. // close angle bracket
  8238. if (token != '}') {
  8239. throw newSyntaxError('Angle bracket } expected');
  8240. }
  8241. getToken();
  8242. // end of file
  8243. if (token !== '') {
  8244. throw newSyntaxError('End of file expected');
  8245. }
  8246. getToken();
  8247. // remove temporary default properties
  8248. delete graph.node;
  8249. delete graph.edge;
  8250. delete graph.graph;
  8251. return graph;
  8252. }
  8253. /**
  8254. * Parse a list with statements.
  8255. * @param {Object} graph
  8256. */
  8257. function parseStatements (graph) {
  8258. while (token !== '' && token != '}') {
  8259. parseStatement(graph);
  8260. if (token == ';') {
  8261. getToken();
  8262. }
  8263. }
  8264. }
  8265. /**
  8266. * Parse a single statement. Can be a an attribute statement, node
  8267. * statement, a series of node statements and edge statements, or a
  8268. * parameter.
  8269. * @param {Object} graph
  8270. */
  8271. function parseStatement(graph) {
  8272. // parse subgraph
  8273. var subgraph = parseSubgraph(graph);
  8274. if (subgraph) {
  8275. // edge statements
  8276. parseEdge(graph, subgraph);
  8277. return;
  8278. }
  8279. // parse an attribute statement
  8280. var attr = parseAttributeStatement(graph);
  8281. if (attr) {
  8282. return;
  8283. }
  8284. // parse node
  8285. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8286. throw newSyntaxError('Identifier expected');
  8287. }
  8288. var id = token; // id can be a string or a number
  8289. getToken();
  8290. if (token == '=') {
  8291. // id statement
  8292. getToken();
  8293. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8294. throw newSyntaxError('Identifier expected');
  8295. }
  8296. graph[id] = token;
  8297. getToken();
  8298. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  8299. }
  8300. else {
  8301. parseNodeStatement(graph, id);
  8302. }
  8303. }
  8304. /**
  8305. * Parse a subgraph
  8306. * @param {Object} graph parent graph object
  8307. * @return {Object | null} subgraph
  8308. */
  8309. function parseSubgraph (graph) {
  8310. var subgraph = null;
  8311. // optional subgraph keyword
  8312. if (token == 'subgraph') {
  8313. subgraph = {};
  8314. subgraph.type = 'subgraph';
  8315. getToken();
  8316. // optional graph id
  8317. if (tokenType == TOKENTYPE.IDENTIFIER) {
  8318. subgraph.id = token;
  8319. getToken();
  8320. }
  8321. }
  8322. // open angle bracket
  8323. if (token == '{') {
  8324. getToken();
  8325. if (!subgraph) {
  8326. subgraph = {};
  8327. }
  8328. subgraph.parent = graph;
  8329. subgraph.node = graph.node;
  8330. subgraph.edge = graph.edge;
  8331. subgraph.graph = graph.graph;
  8332. // statements
  8333. parseStatements(subgraph);
  8334. // close angle bracket
  8335. if (token != '}') {
  8336. throw newSyntaxError('Angle bracket } expected');
  8337. }
  8338. getToken();
  8339. // remove temporary default properties
  8340. delete subgraph.node;
  8341. delete subgraph.edge;
  8342. delete subgraph.graph;
  8343. delete subgraph.parent;
  8344. // register at the parent graph
  8345. if (!graph.subgraphs) {
  8346. graph.subgraphs = [];
  8347. }
  8348. graph.subgraphs.push(subgraph);
  8349. }
  8350. return subgraph;
  8351. }
  8352. /**
  8353. * parse an attribute statement like "node [shape=circle fontSize=16]".
  8354. * Available keywords are 'node', 'edge', 'graph'.
  8355. * The previous list with default attributes will be replaced
  8356. * @param {Object} graph
  8357. * @returns {String | null} keyword Returns the name of the parsed attribute
  8358. * (node, edge, graph), or null if nothing
  8359. * is parsed.
  8360. */
  8361. function parseAttributeStatement (graph) {
  8362. // attribute statements
  8363. if (token == 'node') {
  8364. getToken();
  8365. // node attributes
  8366. graph.node = parseAttributeList();
  8367. return 'node';
  8368. }
  8369. else if (token == 'edge') {
  8370. getToken();
  8371. // edge attributes
  8372. graph.edge = parseAttributeList();
  8373. return 'edge';
  8374. }
  8375. else if (token == 'graph') {
  8376. getToken();
  8377. // graph attributes
  8378. graph.graph = parseAttributeList();
  8379. return 'graph';
  8380. }
  8381. return null;
  8382. }
  8383. /**
  8384. * parse a node statement
  8385. * @param {Object} graph
  8386. * @param {String | Number} id
  8387. */
  8388. function parseNodeStatement(graph, id) {
  8389. // node statement
  8390. var node = {
  8391. id: id
  8392. };
  8393. var attr = parseAttributeList();
  8394. if (attr) {
  8395. node.attr = attr;
  8396. }
  8397. addNode(graph, node);
  8398. // edge statements
  8399. parseEdge(graph, id);
  8400. }
  8401. /**
  8402. * Parse an edge or a series of edges
  8403. * @param {Object} graph
  8404. * @param {String | Number} from Id of the from node
  8405. */
  8406. function parseEdge(graph, from) {
  8407. while (token == '->' || token == '--') {
  8408. var to;
  8409. var type = token;
  8410. getToken();
  8411. var subgraph = parseSubgraph(graph);
  8412. if (subgraph) {
  8413. to = subgraph;
  8414. }
  8415. else {
  8416. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8417. throw newSyntaxError('Identifier or subgraph expected');
  8418. }
  8419. to = token;
  8420. addNode(graph, {
  8421. id: to
  8422. });
  8423. getToken();
  8424. }
  8425. // parse edge attributes
  8426. var attr = parseAttributeList();
  8427. // create edge
  8428. var edge = createEdge(graph, from, to, type, attr);
  8429. addEdge(graph, edge);
  8430. from = to;
  8431. }
  8432. }
  8433. /**
  8434. * Parse a set with attributes,
  8435. * for example [label="1.000", shape=solid]
  8436. * @return {Object | null} attr
  8437. */
  8438. function parseAttributeList() {
  8439. var attr = null;
  8440. while (token == '[') {
  8441. getToken();
  8442. attr = {};
  8443. while (token !== '' && token != ']') {
  8444. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8445. throw newSyntaxError('Attribute name expected');
  8446. }
  8447. var name = token;
  8448. getToken();
  8449. if (token != '=') {
  8450. throw newSyntaxError('Equal sign = expected');
  8451. }
  8452. getToken();
  8453. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8454. throw newSyntaxError('Attribute value expected');
  8455. }
  8456. var value = token;
  8457. setValue(attr, name, value); // name can be a path
  8458. getToken();
  8459. if (token ==',') {
  8460. getToken();
  8461. }
  8462. }
  8463. if (token != ']') {
  8464. throw newSyntaxError('Bracket ] expected');
  8465. }
  8466. getToken();
  8467. }
  8468. return attr;
  8469. }
  8470. /**
  8471. * Create a syntax error with extra information on current token and index.
  8472. * @param {String} message
  8473. * @returns {SyntaxError} err
  8474. */
  8475. function newSyntaxError(message) {
  8476. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  8477. }
  8478. /**
  8479. * Chop off text after a maximum length
  8480. * @param {String} text
  8481. * @param {Number} maxLength
  8482. * @returns {String}
  8483. */
  8484. function chop (text, maxLength) {
  8485. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  8486. }
  8487. /**
  8488. * Execute a function fn for each pair of elements in two arrays
  8489. * @param {Array | *} array1
  8490. * @param {Array | *} array2
  8491. * @param {function} fn
  8492. */
  8493. function forEach2(array1, array2, fn) {
  8494. if (array1 instanceof Array) {
  8495. array1.forEach(function (elem1) {
  8496. if (array2 instanceof Array) {
  8497. array2.forEach(function (elem2) {
  8498. fn(elem1, elem2);
  8499. });
  8500. }
  8501. else {
  8502. fn(elem1, array2);
  8503. }
  8504. });
  8505. }
  8506. else {
  8507. if (array2 instanceof Array) {
  8508. array2.forEach(function (elem2) {
  8509. fn(array1, elem2);
  8510. });
  8511. }
  8512. else {
  8513. fn(array1, array2);
  8514. }
  8515. }
  8516. }
  8517. /**
  8518. * Convert a string containing a graph in DOT language into a map containing
  8519. * with nodes and edges in the format of graph.
  8520. * @param {String} data Text containing a graph in DOT-notation
  8521. * @return {Object} graphData
  8522. */
  8523. function DOTToGraph (data) {
  8524. // parse the DOT file
  8525. var dotData = parseDOT(data);
  8526. var graphData = {
  8527. nodes: [],
  8528. edges: [],
  8529. options: {}
  8530. };
  8531. // copy the nodes
  8532. if (dotData.nodes) {
  8533. dotData.nodes.forEach(function (dotNode) {
  8534. var graphNode = {
  8535. id: dotNode.id,
  8536. label: String(dotNode.label || dotNode.id)
  8537. };
  8538. merge(graphNode, dotNode.attr);
  8539. if (graphNode.image) {
  8540. graphNode.shape = 'image';
  8541. }
  8542. graphData.nodes.push(graphNode);
  8543. });
  8544. }
  8545. // copy the edges
  8546. if (dotData.edges) {
  8547. /**
  8548. * Convert an edge in DOT format to an edge with VisGraph format
  8549. * @param {Object} dotEdge
  8550. * @returns {Object} graphEdge
  8551. */
  8552. function convertEdge(dotEdge) {
  8553. var graphEdge = {
  8554. from: dotEdge.from,
  8555. to: dotEdge.to
  8556. };
  8557. merge(graphEdge, dotEdge.attr);
  8558. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  8559. return graphEdge;
  8560. }
  8561. dotData.edges.forEach(function (dotEdge) {
  8562. var from, to;
  8563. if (dotEdge.from instanceof Object) {
  8564. from = dotEdge.from.nodes;
  8565. }
  8566. else {
  8567. from = {
  8568. id: dotEdge.from
  8569. }
  8570. }
  8571. if (dotEdge.to instanceof Object) {
  8572. to = dotEdge.to.nodes;
  8573. }
  8574. else {
  8575. to = {
  8576. id: dotEdge.to
  8577. }
  8578. }
  8579. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  8580. dotEdge.from.edges.forEach(function (subEdge) {
  8581. var graphEdge = convertEdge(subEdge);
  8582. graphData.edges.push(graphEdge);
  8583. });
  8584. }
  8585. forEach2(from, to, function (from, to) {
  8586. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  8587. var graphEdge = convertEdge(subEdge);
  8588. graphData.edges.push(graphEdge);
  8589. });
  8590. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  8591. dotEdge.to.edges.forEach(function (subEdge) {
  8592. var graphEdge = convertEdge(subEdge);
  8593. graphData.edges.push(graphEdge);
  8594. });
  8595. }
  8596. });
  8597. }
  8598. // copy the options
  8599. if (dotData.attr) {
  8600. graphData.options = dotData.attr;
  8601. }
  8602. return graphData;
  8603. }
  8604. // exports
  8605. exports.parseDOT = parseDOT;
  8606. exports.DOTToGraph = DOTToGraph;
  8607. })(typeof util !== 'undefined' ? util : exports);
  8608. /**
  8609. * Canvas shapes used by the Graph
  8610. */
  8611. if (typeof CanvasRenderingContext2D !== 'undefined') {
  8612. /**
  8613. * Draw a circle shape
  8614. */
  8615. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  8616. this.beginPath();
  8617. this.arc(x, y, r, 0, 2*Math.PI, false);
  8618. };
  8619. /**
  8620. * Draw a square shape
  8621. * @param {Number} x horizontal center
  8622. * @param {Number} y vertical center
  8623. * @param {Number} r size, width and height of the square
  8624. */
  8625. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  8626. this.beginPath();
  8627. this.rect(x - r, y - r, r * 2, r * 2);
  8628. };
  8629. /**
  8630. * Draw a triangle shape
  8631. * @param {Number} x horizontal center
  8632. * @param {Number} y vertical center
  8633. * @param {Number} r radius, half the length of the sides of the triangle
  8634. */
  8635. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  8636. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8637. this.beginPath();
  8638. var s = r * 2;
  8639. var s2 = s / 2;
  8640. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8641. var h = Math.sqrt(s * s - s2 * s2); // height
  8642. this.moveTo(x, y - (h - ir));
  8643. this.lineTo(x + s2, y + ir);
  8644. this.lineTo(x - s2, y + ir);
  8645. this.lineTo(x, y - (h - ir));
  8646. this.closePath();
  8647. };
  8648. /**
  8649. * Draw a triangle shape in downward orientation
  8650. * @param {Number} x horizontal center
  8651. * @param {Number} y vertical center
  8652. * @param {Number} r radius
  8653. */
  8654. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  8655. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8656. this.beginPath();
  8657. var s = r * 2;
  8658. var s2 = s / 2;
  8659. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8660. var h = Math.sqrt(s * s - s2 * s2); // height
  8661. this.moveTo(x, y + (h - ir));
  8662. this.lineTo(x + s2, y - ir);
  8663. this.lineTo(x - s2, y - ir);
  8664. this.lineTo(x, y + (h - ir));
  8665. this.closePath();
  8666. };
  8667. /**
  8668. * Draw a star shape, a star with 5 points
  8669. * @param {Number} x horizontal center
  8670. * @param {Number} y vertical center
  8671. * @param {Number} r radius, half the length of the sides of the triangle
  8672. */
  8673. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  8674. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  8675. this.beginPath();
  8676. for (var n = 0; n < 10; n++) {
  8677. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  8678. this.lineTo(
  8679. x + radius * Math.sin(n * 2 * Math.PI / 10),
  8680. y - radius * Math.cos(n * 2 * Math.PI / 10)
  8681. );
  8682. }
  8683. this.closePath();
  8684. };
  8685. /**
  8686. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  8687. */
  8688. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  8689. var r2d = Math.PI/180;
  8690. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  8691. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  8692. this.beginPath();
  8693. this.moveTo(x+r,y);
  8694. this.lineTo(x+w-r,y);
  8695. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  8696. this.lineTo(x+w,y+h-r);
  8697. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  8698. this.lineTo(x+r,y+h);
  8699. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  8700. this.lineTo(x,y+r);
  8701. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  8702. };
  8703. /**
  8704. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8705. */
  8706. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  8707. var kappa = .5522848,
  8708. ox = (w / 2) * kappa, // control point offset horizontal
  8709. oy = (h / 2) * kappa, // control point offset vertical
  8710. xe = x + w, // x-end
  8711. ye = y + h, // y-end
  8712. xm = x + w / 2, // x-middle
  8713. ym = y + h / 2; // y-middle
  8714. this.beginPath();
  8715. this.moveTo(x, ym);
  8716. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8717. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8718. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8719. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8720. };
  8721. /**
  8722. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8723. */
  8724. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  8725. var f = 1/3;
  8726. var wEllipse = w;
  8727. var hEllipse = h * f;
  8728. var kappa = .5522848,
  8729. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  8730. oy = (hEllipse / 2) * kappa, // control point offset vertical
  8731. xe = x + wEllipse, // x-end
  8732. ye = y + hEllipse, // y-end
  8733. xm = x + wEllipse / 2, // x-middle
  8734. ym = y + hEllipse / 2, // y-middle
  8735. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  8736. yeb = y + h; // y-end, bottom ellipse
  8737. this.beginPath();
  8738. this.moveTo(xe, ym);
  8739. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8740. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8741. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8742. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8743. this.lineTo(xe, ymb);
  8744. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  8745. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  8746. this.lineTo(x, ym);
  8747. };
  8748. /**
  8749. * Draw an arrow point (no line)
  8750. */
  8751. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  8752. // tail
  8753. var xt = x - length * Math.cos(angle);
  8754. var yt = y - length * Math.sin(angle);
  8755. // inner tail
  8756. // TODO: allow to customize different shapes
  8757. var xi = x - length * 0.9 * Math.cos(angle);
  8758. var yi = y - length * 0.9 * Math.sin(angle);
  8759. // left
  8760. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  8761. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  8762. // right
  8763. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  8764. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  8765. this.beginPath();
  8766. this.moveTo(x, y);
  8767. this.lineTo(xl, yl);
  8768. this.lineTo(xi, yi);
  8769. this.lineTo(xr, yr);
  8770. this.closePath();
  8771. };
  8772. /**
  8773. * Sets up the dashedLine functionality for drawing
  8774. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  8775. * @author David Jordan
  8776. * @date 2012-08-08
  8777. */
  8778. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  8779. if (!dashArray) dashArray=[10,5];
  8780. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  8781. var dashCount = dashArray.length;
  8782. this.moveTo(x, y);
  8783. var dx = (x2-x), dy = (y2-y);
  8784. var slope = dy/dx;
  8785. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  8786. var dashIndex=0, draw=true;
  8787. while (distRemaining>=0.1){
  8788. var dashLength = dashArray[dashIndex++%dashCount];
  8789. if (dashLength > distRemaining) dashLength = distRemaining;
  8790. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  8791. if (dx<0) xStep = -xStep;
  8792. x += xStep;
  8793. y += slope*xStep;
  8794. this[draw ? 'lineTo' : 'moveTo'](x,y);
  8795. distRemaining -= dashLength;
  8796. draw = !draw;
  8797. }
  8798. };
  8799. // TODO: add diamond shape
  8800. }
  8801. /**
  8802. * @class Node
  8803. * A node. A node can be connected to other nodes via one or multiple edges.
  8804. * @param {object} properties An object containing properties for the node. All
  8805. * properties are optional, except for the id.
  8806. * {number} id Id of the node. Required
  8807. * {string} label Text label for the node
  8808. * {number} x Horizontal position of the node
  8809. * {number} y Vertical position of the node
  8810. * {string} shape Node shape, available:
  8811. * "database", "circle", "ellipse",
  8812. * "box", "image", "text", "dot",
  8813. * "star", "triangle", "triangleDown",
  8814. * "square"
  8815. * {string} image An image url
  8816. * {string} title An title text, can be HTML
  8817. * {anytype} group A group name or number
  8818. * @param {Graph.Images} imagelist A list with images. Only needed
  8819. * when the node has an image
  8820. * @param {Graph.Groups} grouplist A list with groups. Needed for
  8821. * retrieving group properties
  8822. * @param {Object} constants An object with default values for
  8823. * example for the color
  8824. *
  8825. */
  8826. function Node(properties, imagelist, grouplist, constants) {
  8827. this.selected = false;
  8828. this.edges = []; // all edges connected to this node
  8829. this.dynamicEdges = [];
  8830. this.reroutedEdges = {};
  8831. this.group = constants.nodes.group;
  8832. this.fontSize = constants.nodes.fontSize;
  8833. this.fontFace = constants.nodes.fontFace;
  8834. this.fontColor = constants.nodes.fontColor;
  8835. this.fontDrawThreshold = 3;
  8836. this.color = constants.nodes.color;
  8837. // set defaults for the properties
  8838. this.id = undefined;
  8839. this.shape = constants.nodes.shape;
  8840. this.image = constants.nodes.image;
  8841. this.x = null;
  8842. this.y = null;
  8843. this.xFixed = false;
  8844. this.yFixed = false;
  8845. this.horizontalAlignLeft = true; // these are for the navigation controls
  8846. this.verticalAlignTop = true; // these are for the navigation controls
  8847. this.radius = constants.nodes.radius;
  8848. this.baseRadiusValue = constants.nodes.radius;
  8849. this.radiusFixed = false;
  8850. this.radiusMin = constants.nodes.radiusMin;
  8851. this.radiusMax = constants.nodes.radiusMax;
  8852. this.level = -1;
  8853. this.preassignedLevel = false;
  8854. this.imagelist = imagelist;
  8855. this.grouplist = grouplist;
  8856. // physics properties
  8857. this.fx = 0.0; // external force x
  8858. this.fy = 0.0; // external force y
  8859. this.vx = 0.0; // velocity x
  8860. this.vy = 0.0; // velocity y
  8861. this.minForce = constants.minForce;
  8862. this.damping = constants.physics.damping;
  8863. this.mass = 1; // kg
  8864. this.fixedData = {x:null,y:null};
  8865. this.setProperties(properties, constants);
  8866. // creating the variables for clustering
  8867. this.resetCluster();
  8868. this.dynamicEdgesLength = 0;
  8869. this.clusterSession = 0;
  8870. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  8871. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  8872. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  8873. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  8874. this.growthIndicator = 0;
  8875. // variables to tell the node about the graph.
  8876. this.graphScaleInv = 1;
  8877. this.graphScale = 1;
  8878. this.canvasTopLeft = {"x": -300, "y": -300};
  8879. this.canvasBottomRight = {"x": 300, "y": 300};
  8880. this.parentEdgeId = null;
  8881. }
  8882. /**
  8883. * (re)setting the clustering variables and objects
  8884. */
  8885. Node.prototype.resetCluster = function() {
  8886. // clustering variables
  8887. this.formationScale = undefined; // this is used to determine when to open the cluster
  8888. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  8889. this.containedNodes = {};
  8890. this.containedEdges = {};
  8891. this.clusterSessions = [];
  8892. };
  8893. /**
  8894. * Attach a edge to the node
  8895. * @param {Edge} edge
  8896. */
  8897. Node.prototype.attachEdge = function(edge) {
  8898. if (this.edges.indexOf(edge) == -1) {
  8899. this.edges.push(edge);
  8900. }
  8901. if (this.dynamicEdges.indexOf(edge) == -1) {
  8902. this.dynamicEdges.push(edge);
  8903. }
  8904. this.dynamicEdgesLength = this.dynamicEdges.length;
  8905. };
  8906. /**
  8907. * Detach a edge from the node
  8908. * @param {Edge} edge
  8909. */
  8910. Node.prototype.detachEdge = function(edge) {
  8911. var index = this.edges.indexOf(edge);
  8912. if (index != -1) {
  8913. this.edges.splice(index, 1);
  8914. this.dynamicEdges.splice(index, 1);
  8915. }
  8916. this.dynamicEdgesLength = this.dynamicEdges.length;
  8917. };
  8918. /**
  8919. * Set or overwrite properties for the node
  8920. * @param {Object} properties an object with properties
  8921. * @param {Object} constants and object with default, global properties
  8922. */
  8923. Node.prototype.setProperties = function(properties, constants) {
  8924. if (!properties) {
  8925. return;
  8926. }
  8927. this.originalLabel = undefined;
  8928. // basic properties
  8929. if (properties.id !== undefined) {this.id = properties.id;}
  8930. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  8931. if (properties.title !== undefined) {this.title = properties.title;}
  8932. if (properties.group !== undefined) {this.group = properties.group;}
  8933. if (properties.x !== undefined) {this.x = properties.x;}
  8934. if (properties.y !== undefined) {this.y = properties.y;}
  8935. if (properties.value !== undefined) {this.value = properties.value;}
  8936. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  8937. // physics
  8938. if (properties.mass !== undefined) {this.mass = properties.mass;}
  8939. // navigation controls properties
  8940. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  8941. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  8942. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  8943. if (this.id === undefined) {
  8944. throw "Node must have an id";
  8945. }
  8946. // copy group properties
  8947. if (this.group) {
  8948. var groupObj = this.grouplist.get(this.group);
  8949. for (var prop in groupObj) {
  8950. if (groupObj.hasOwnProperty(prop)) {
  8951. this[prop] = groupObj[prop];
  8952. }
  8953. }
  8954. }
  8955. // individual shape properties
  8956. if (properties.shape !== undefined) {this.shape = properties.shape;}
  8957. if (properties.image !== undefined) {this.image = properties.image;}
  8958. if (properties.radius !== undefined) {this.radius = properties.radius;}
  8959. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  8960. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8961. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8962. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8963. if (this.image !== undefined && this.image != "") {
  8964. if (this.imagelist) {
  8965. this.imageObj = this.imagelist.load(this.image);
  8966. }
  8967. else {
  8968. throw "No imagelist provided";
  8969. }
  8970. }
  8971. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  8972. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  8973. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  8974. if (this.shape == 'image') {
  8975. this.radiusMin = constants.nodes.widthMin;
  8976. this.radiusMax = constants.nodes.widthMax;
  8977. }
  8978. // choose draw method depending on the shape
  8979. switch (this.shape) {
  8980. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8981. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8982. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8983. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8984. // TODO: add diamond shape
  8985. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8986. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8987. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8988. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8989. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8990. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8991. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8992. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8993. }
  8994. // reset the size of the node, this can be changed
  8995. this._reset();
  8996. };
  8997. /**
  8998. * select this node
  8999. */
  9000. Node.prototype.select = function() {
  9001. this.selected = true;
  9002. this._reset();
  9003. };
  9004. /**
  9005. * unselect this node
  9006. */
  9007. Node.prototype.unselect = function() {
  9008. this.selected = false;
  9009. this._reset();
  9010. };
  9011. /**
  9012. * Reset the calculated size of the node, forces it to recalculate its size
  9013. */
  9014. Node.prototype.clearSizeCache = function() {
  9015. this._reset();
  9016. };
  9017. /**
  9018. * Reset the calculated size of the node, forces it to recalculate its size
  9019. * @private
  9020. */
  9021. Node.prototype._reset = function() {
  9022. this.width = undefined;
  9023. this.height = undefined;
  9024. };
  9025. /**
  9026. * get the title of this node.
  9027. * @return {string} title The title of the node, or undefined when no title
  9028. * has been set.
  9029. */
  9030. Node.prototype.getTitle = function() {
  9031. return typeof this.title === "function" ? this.title() : this.title;
  9032. };
  9033. /**
  9034. * Calculate the distance to the border of the Node
  9035. * @param {CanvasRenderingContext2D} ctx
  9036. * @param {Number} angle Angle in radians
  9037. * @returns {number} distance Distance to the border in pixels
  9038. */
  9039. Node.prototype.distanceToBorder = function (ctx, angle) {
  9040. var borderWidth = 1;
  9041. if (!this.width) {
  9042. this.resize(ctx);
  9043. }
  9044. switch (this.shape) {
  9045. case 'circle':
  9046. case 'dot':
  9047. return this.radius + borderWidth;
  9048. case 'ellipse':
  9049. var a = this.width / 2;
  9050. var b = this.height / 2;
  9051. var w = (Math.sin(angle) * a);
  9052. var h = (Math.cos(angle) * b);
  9053. return a * b / Math.sqrt(w * w + h * h);
  9054. // TODO: implement distanceToBorder for database
  9055. // TODO: implement distanceToBorder for triangle
  9056. // TODO: implement distanceToBorder for triangleDown
  9057. case 'box':
  9058. case 'image':
  9059. case 'text':
  9060. default:
  9061. if (this.width) {
  9062. return Math.min(
  9063. Math.abs(this.width / 2 / Math.cos(angle)),
  9064. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  9065. // TODO: reckon with border radius too in case of box
  9066. }
  9067. else {
  9068. return 0;
  9069. }
  9070. }
  9071. // TODO: implement calculation of distance to border for all shapes
  9072. };
  9073. /**
  9074. * Set forces acting on the node
  9075. * @param {number} fx Force in horizontal direction
  9076. * @param {number} fy Force in vertical direction
  9077. */
  9078. Node.prototype._setForce = function(fx, fy) {
  9079. this.fx = fx;
  9080. this.fy = fy;
  9081. };
  9082. /**
  9083. * Add forces acting on the node
  9084. * @param {number} fx Force in horizontal direction
  9085. * @param {number} fy Force in vertical direction
  9086. * @private
  9087. */
  9088. Node.prototype._addForce = function(fx, fy) {
  9089. this.fx += fx;
  9090. this.fy += fy;
  9091. };
  9092. /**
  9093. * Perform one discrete step for the node
  9094. * @param {number} interval Time interval in seconds
  9095. */
  9096. Node.prototype.discreteStep = function(interval) {
  9097. if (!this.xFixed) {
  9098. var dx = this.damping * this.vx; // damping force
  9099. var ax = (this.fx - dx) / this.mass; // acceleration
  9100. this.vx += ax * interval; // velocity
  9101. this.x += this.vx * interval; // position
  9102. }
  9103. if (!this.yFixed) {
  9104. var dy = this.damping * this.vy; // damping force
  9105. var ay = (this.fy - dy) / this.mass; // acceleration
  9106. this.vy += ay * interval; // velocity
  9107. this.y += this.vy * interval; // position
  9108. }
  9109. };
  9110. /**
  9111. * Perform one discrete step for the node
  9112. * @param {number} interval Time interval in seconds
  9113. * @param {number} maxVelocity The speed limit imposed on the velocity
  9114. */
  9115. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  9116. if (!this.xFixed) {
  9117. var dx = this.damping * this.vx; // damping force
  9118. var ax = (this.fx - dx) / this.mass; // acceleration
  9119. this.vx += ax * interval; // velocity
  9120. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  9121. this.x += this.vx * interval; // position
  9122. }
  9123. else {
  9124. this.fx = 0;
  9125. }
  9126. if (!this.yFixed) {
  9127. var dy = this.damping * this.vy; // damping force
  9128. var ay = (this.fy - dy) / this.mass; // acceleration
  9129. this.vy += ay * interval; // velocity
  9130. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  9131. this.y += this.vy * interval; // position
  9132. }
  9133. else {
  9134. this.fy = 0;
  9135. }
  9136. };
  9137. /**
  9138. * Check if this node has a fixed x and y position
  9139. * @return {boolean} true if fixed, false if not
  9140. */
  9141. Node.prototype.isFixed = function() {
  9142. return (this.xFixed && this.yFixed);
  9143. };
  9144. /**
  9145. * Check if this node is moving
  9146. * @param {number} vmin the minimum velocity considered as "moving"
  9147. * @return {boolean} true if moving, false if it has no velocity
  9148. */
  9149. // TODO: replace this method with calculating the kinetic energy
  9150. Node.prototype.isMoving = function(vmin) {
  9151. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  9152. };
  9153. /**
  9154. * check if this node is selecte
  9155. * @return {boolean} selected True if node is selected, else false
  9156. */
  9157. Node.prototype.isSelected = function() {
  9158. return this.selected;
  9159. };
  9160. /**
  9161. * Retrieve the value of the node. Can be undefined
  9162. * @return {Number} value
  9163. */
  9164. Node.prototype.getValue = function() {
  9165. return this.value;
  9166. };
  9167. /**
  9168. * Calculate the distance from the nodes location to the given location (x,y)
  9169. * @param {Number} x
  9170. * @param {Number} y
  9171. * @return {Number} value
  9172. */
  9173. Node.prototype.getDistance = function(x, y) {
  9174. var dx = this.x - x,
  9175. dy = this.y - y;
  9176. return Math.sqrt(dx * dx + dy * dy);
  9177. };
  9178. /**
  9179. * Adjust the value range of the node. The node will adjust it's radius
  9180. * based on its value.
  9181. * @param {Number} min
  9182. * @param {Number} max
  9183. */
  9184. Node.prototype.setValueRange = function(min, max) {
  9185. if (!this.radiusFixed && this.value !== undefined) {
  9186. if (max == min) {
  9187. this.radius = (this.radiusMin + this.radiusMax) / 2;
  9188. }
  9189. else {
  9190. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  9191. this.radius = (this.value - min) * scale + this.radiusMin;
  9192. }
  9193. }
  9194. this.baseRadiusValue = this.radius;
  9195. };
  9196. /**
  9197. * Draw this node in the given canvas
  9198. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9199. * @param {CanvasRenderingContext2D} ctx
  9200. */
  9201. Node.prototype.draw = function(ctx) {
  9202. throw "Draw method not initialized for node";
  9203. };
  9204. /**
  9205. * Recalculate the size of this node in the given canvas
  9206. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9207. * @param {CanvasRenderingContext2D} ctx
  9208. */
  9209. Node.prototype.resize = function(ctx) {
  9210. throw "Resize method not initialized for node";
  9211. };
  9212. /**
  9213. * Check if this object is overlapping with the provided object
  9214. * @param {Object} obj an object with parameters left, top, right, bottom
  9215. * @return {boolean} True if location is located on node
  9216. */
  9217. Node.prototype.isOverlappingWith = function(obj) {
  9218. return (this.left < obj.right &&
  9219. this.left + this.width > obj.left &&
  9220. this.top < obj.bottom &&
  9221. this.top + this.height > obj.top);
  9222. };
  9223. Node.prototype._resizeImage = function (ctx) {
  9224. // TODO: pre calculate the image size
  9225. if (!this.width || !this.height) { // undefined or 0
  9226. var width, height;
  9227. if (this.value) {
  9228. this.radius = this.baseRadiusValue;
  9229. var scale = this.imageObj.height / this.imageObj.width;
  9230. if (scale !== undefined) {
  9231. width = this.radius || this.imageObj.width;
  9232. height = this.radius * scale || this.imageObj.height;
  9233. }
  9234. else {
  9235. width = 0;
  9236. height = 0;
  9237. }
  9238. }
  9239. else {
  9240. width = this.imageObj.width;
  9241. height = this.imageObj.height;
  9242. }
  9243. this.width = width;
  9244. this.height = height;
  9245. this.growthIndicator = 0;
  9246. if (this.width > 0 && this.height > 0) {
  9247. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9248. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9249. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9250. this.growthIndicator = this.width - width;
  9251. }
  9252. }
  9253. };
  9254. Node.prototype._drawImage = function (ctx) {
  9255. this._resizeImage(ctx);
  9256. this.left = this.x - this.width / 2;
  9257. this.top = this.y - this.height / 2;
  9258. var yLabel;
  9259. if (this.imageObj.width != 0 ) {
  9260. // draw the shade
  9261. if (this.clusterSize > 1) {
  9262. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  9263. lineWidth *= this.graphScaleInv;
  9264. lineWidth = Math.min(0.2 * this.width,lineWidth);
  9265. ctx.globalAlpha = 0.5;
  9266. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  9267. }
  9268. // draw the image
  9269. ctx.globalAlpha = 1.0;
  9270. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  9271. yLabel = this.y + this.height / 2;
  9272. }
  9273. else {
  9274. // image still loading... just draw the label for now
  9275. yLabel = this.y;
  9276. }
  9277. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  9278. };
  9279. Node.prototype._resizeBox = function (ctx) {
  9280. if (!this.width) {
  9281. var margin = 5;
  9282. var textSize = this.getTextSize(ctx);
  9283. this.width = textSize.width + 2 * margin;
  9284. this.height = textSize.height + 2 * margin;
  9285. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  9286. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  9287. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  9288. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9289. }
  9290. };
  9291. Node.prototype._drawBox = function (ctx) {
  9292. this._resizeBox(ctx);
  9293. this.left = this.x - this.width / 2;
  9294. this.top = this.y - this.height / 2;
  9295. var clusterLineWidth = 2.5;
  9296. var selectionLineWidth = 2;
  9297. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9298. // draw the outer border
  9299. if (this.clusterSize > 1) {
  9300. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9301. ctx.lineWidth *= this.graphScaleInv;
  9302. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9303. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  9304. ctx.stroke();
  9305. }
  9306. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9307. ctx.lineWidth *= this.graphScaleInv;
  9308. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9309. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9310. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  9311. ctx.fill();
  9312. ctx.stroke();
  9313. this._label(ctx, this.label, this.x, this.y);
  9314. };
  9315. Node.prototype._resizeDatabase = function (ctx) {
  9316. if (!this.width) {
  9317. var margin = 5;
  9318. var textSize = this.getTextSize(ctx);
  9319. var size = textSize.width + 2 * margin;
  9320. this.width = size;
  9321. this.height = size;
  9322. // scaling used for clustering
  9323. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9324. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9325. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9326. this.growthIndicator = this.width - size;
  9327. }
  9328. };
  9329. Node.prototype._drawDatabase = function (ctx) {
  9330. this._resizeDatabase(ctx);
  9331. this.left = this.x - this.width / 2;
  9332. this.top = this.y - this.height / 2;
  9333. var clusterLineWidth = 2.5;
  9334. var selectionLineWidth = 2;
  9335. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9336. // draw the outer border
  9337. if (this.clusterSize > 1) {
  9338. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9339. ctx.lineWidth *= this.graphScaleInv;
  9340. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9341. ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
  9342. ctx.stroke();
  9343. }
  9344. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9345. ctx.lineWidth *= this.graphScaleInv;
  9346. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9347. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9348. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  9349. ctx.fill();
  9350. ctx.stroke();
  9351. this._label(ctx, this.label, this.x, this.y);
  9352. };
  9353. Node.prototype._resizeCircle = function (ctx) {
  9354. if (!this.width) {
  9355. var margin = 5;
  9356. var textSize = this.getTextSize(ctx);
  9357. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  9358. this.radius = diameter / 2;
  9359. this.width = diameter;
  9360. this.height = diameter;
  9361. // scaling used for clustering
  9362. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  9363. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  9364. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9365. this.growthIndicator = this.radius - 0.5*diameter;
  9366. }
  9367. };
  9368. Node.prototype._drawCircle = function (ctx) {
  9369. this._resizeCircle(ctx);
  9370. this.left = this.x - this.width / 2;
  9371. this.top = this.y - this.height / 2;
  9372. var clusterLineWidth = 2.5;
  9373. var selectionLineWidth = 2;
  9374. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9375. // draw the outer border
  9376. if (this.clusterSize > 1) {
  9377. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9378. ctx.lineWidth *= this.graphScaleInv;
  9379. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9380. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  9381. ctx.stroke();
  9382. }
  9383. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9384. ctx.lineWidth *= this.graphScaleInv;
  9385. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9386. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9387. ctx.circle(this.x, this.y, this.radius);
  9388. ctx.fill();
  9389. ctx.stroke();
  9390. this._label(ctx, this.label, this.x, this.y);
  9391. };
  9392. Node.prototype._resizeEllipse = function (ctx) {
  9393. if (!this.width) {
  9394. var textSize = this.getTextSize(ctx);
  9395. this.width = textSize.width * 1.5;
  9396. this.height = textSize.height * 2;
  9397. if (this.width < this.height) {
  9398. this.width = this.height;
  9399. }
  9400. var defaultSize = this.width;
  9401. // scaling used for clustering
  9402. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9403. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9404. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9405. this.growthIndicator = this.width - defaultSize;
  9406. }
  9407. };
  9408. Node.prototype._drawEllipse = function (ctx) {
  9409. this._resizeEllipse(ctx);
  9410. this.left = this.x - this.width / 2;
  9411. this.top = this.y - this.height / 2;
  9412. var clusterLineWidth = 2.5;
  9413. var selectionLineWidth = 2;
  9414. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9415. // draw the outer border
  9416. if (this.clusterSize > 1) {
  9417. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9418. ctx.lineWidth *= this.graphScaleInv;
  9419. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9420. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  9421. ctx.stroke();
  9422. }
  9423. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9424. ctx.lineWidth *= this.graphScaleInv;
  9425. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9426. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9427. ctx.ellipse(this.left, this.top, this.width, this.height);
  9428. ctx.fill();
  9429. ctx.stroke();
  9430. this._label(ctx, this.label, this.x, this.y);
  9431. };
  9432. Node.prototype._drawDot = function (ctx) {
  9433. this._drawShape(ctx, 'circle');
  9434. };
  9435. Node.prototype._drawTriangle = function (ctx) {
  9436. this._drawShape(ctx, 'triangle');
  9437. };
  9438. Node.prototype._drawTriangleDown = function (ctx) {
  9439. this._drawShape(ctx, 'triangleDown');
  9440. };
  9441. Node.prototype._drawSquare = function (ctx) {
  9442. this._drawShape(ctx, 'square');
  9443. };
  9444. Node.prototype._drawStar = function (ctx) {
  9445. this._drawShape(ctx, 'star');
  9446. };
  9447. Node.prototype._resizeShape = function (ctx) {
  9448. if (!this.width) {
  9449. this.radius = this.baseRadiusValue;
  9450. var size = 2 * this.radius;
  9451. this.width = size;
  9452. this.height = size;
  9453. // scaling used for clustering
  9454. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9455. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9456. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9457. this.growthIndicator = this.width - size;
  9458. }
  9459. };
  9460. Node.prototype._drawShape = function (ctx, shape) {
  9461. this._resizeShape(ctx);
  9462. this.left = this.x - this.width / 2;
  9463. this.top = this.y - this.height / 2;
  9464. var clusterLineWidth = 2.5;
  9465. var selectionLineWidth = 2;
  9466. var radiusMultiplier = 2;
  9467. // choose draw method depending on the shape
  9468. switch (shape) {
  9469. case 'dot': radiusMultiplier = 2; break;
  9470. case 'square': radiusMultiplier = 2; break;
  9471. case 'triangle': radiusMultiplier = 3; break;
  9472. case 'triangleDown': radiusMultiplier = 3; break;
  9473. case 'star': radiusMultiplier = 4; break;
  9474. }
  9475. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9476. // draw the outer border
  9477. if (this.clusterSize > 1) {
  9478. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9479. ctx.lineWidth *= this.graphScaleInv;
  9480. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9481. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  9482. ctx.stroke();
  9483. }
  9484. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9485. ctx.lineWidth *= this.graphScaleInv;
  9486. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9487. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9488. ctx[shape](this.x, this.y, this.radius);
  9489. ctx.fill();
  9490. ctx.stroke();
  9491. if (this.label) {
  9492. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  9493. }
  9494. };
  9495. Node.prototype._resizeText = function (ctx) {
  9496. if (!this.width) {
  9497. var margin = 5;
  9498. var textSize = this.getTextSize(ctx);
  9499. this.width = textSize.width + 2 * margin;
  9500. this.height = textSize.height + 2 * margin;
  9501. // scaling used for clustering
  9502. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9503. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9504. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9505. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  9506. }
  9507. };
  9508. Node.prototype._drawText = function (ctx) {
  9509. this._resizeText(ctx);
  9510. this.left = this.x - this.width / 2;
  9511. this.top = this.y - this.height / 2;
  9512. this._label(ctx, this.label, this.x, this.y);
  9513. };
  9514. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  9515. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  9516. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9517. ctx.fillStyle = this.fontColor || "black";
  9518. ctx.textAlign = align || "center";
  9519. ctx.textBaseline = baseline || "middle";
  9520. var lines = text.split('\n'),
  9521. lineCount = lines.length,
  9522. fontSize = (this.fontSize + 4),
  9523. yLine = y + (1 - lineCount) / 2 * fontSize;
  9524. for (var i = 0; i < lineCount; i++) {
  9525. ctx.fillText(lines[i], x, yLine);
  9526. yLine += fontSize;
  9527. }
  9528. }
  9529. };
  9530. Node.prototype.getTextSize = function(ctx) {
  9531. if (this.label !== undefined) {
  9532. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9533. var lines = this.label.split('\n'),
  9534. height = (this.fontSize + 4) * lines.length,
  9535. width = 0;
  9536. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  9537. width = Math.max(width, ctx.measureText(lines[i]).width);
  9538. }
  9539. return {"width": width, "height": height};
  9540. }
  9541. else {
  9542. return {"width": 0, "height": 0};
  9543. }
  9544. };
  9545. /**
  9546. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  9547. * there is a safety margin of 0.3 * width;
  9548. *
  9549. * @returns {boolean}
  9550. */
  9551. Node.prototype.inArea = function() {
  9552. if (this.width !== undefined) {
  9553. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  9554. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  9555. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  9556. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  9557. }
  9558. else {
  9559. return true;
  9560. }
  9561. };
  9562. /**
  9563. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  9564. * @returns {boolean}
  9565. */
  9566. Node.prototype.inView = function() {
  9567. return (this.x >= this.canvasTopLeft.x &&
  9568. this.x < this.canvasBottomRight.x &&
  9569. this.y >= this.canvasTopLeft.y &&
  9570. this.y < this.canvasBottomRight.y);
  9571. };
  9572. /**
  9573. * This allows the zoom level of the graph to influence the rendering
  9574. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  9575. *
  9576. * @param scale
  9577. * @param canvasTopLeft
  9578. * @param canvasBottomRight
  9579. */
  9580. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  9581. this.graphScaleInv = 1.0/scale;
  9582. this.graphScale = scale;
  9583. this.canvasTopLeft = canvasTopLeft;
  9584. this.canvasBottomRight = canvasBottomRight;
  9585. };
  9586. /**
  9587. * This allows the zoom level of the graph to influence the rendering
  9588. *
  9589. * @param scale
  9590. */
  9591. Node.prototype.setScale = function(scale) {
  9592. this.graphScaleInv = 1.0/scale;
  9593. this.graphScale = scale;
  9594. };
  9595. /**
  9596. * set the velocity at 0. Is called when this node is contained in another during clustering
  9597. */
  9598. Node.prototype.clearVelocity = function() {
  9599. this.vx = 0;
  9600. this.vy = 0;
  9601. };
  9602. /**
  9603. * Basic preservation of (kinectic) energy
  9604. *
  9605. * @param massBeforeClustering
  9606. */
  9607. Node.prototype.updateVelocity = function(massBeforeClustering) {
  9608. var energyBefore = this.vx * this.vx * massBeforeClustering;
  9609. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9610. this.vx = Math.sqrt(energyBefore/this.mass);
  9611. energyBefore = this.vy * this.vy * massBeforeClustering;
  9612. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9613. this.vy = Math.sqrt(energyBefore/this.mass);
  9614. };
  9615. /**
  9616. * @class Edge
  9617. *
  9618. * A edge connects two nodes
  9619. * @param {Object} properties Object with properties. Must contain
  9620. * At least properties from and to.
  9621. * Available properties: from (number),
  9622. * to (number), label (string, color (string),
  9623. * width (number), style (string),
  9624. * length (number), title (string)
  9625. * @param {Graph} graph A graph object, used to find and edge to
  9626. * nodes.
  9627. * @param {Object} constants An object with default values for
  9628. * example for the color
  9629. */
  9630. function Edge (properties, graph, constants) {
  9631. if (!graph) {
  9632. throw "No graph provided";
  9633. }
  9634. this.graph = graph;
  9635. // initialize constants
  9636. this.widthMin = constants.edges.widthMin;
  9637. this.widthMax = constants.edges.widthMax;
  9638. // initialize variables
  9639. this.id = undefined;
  9640. this.fromId = undefined;
  9641. this.toId = undefined;
  9642. this.style = constants.edges.style;
  9643. this.title = undefined;
  9644. this.width = constants.edges.width;
  9645. this.value = undefined;
  9646. this.length = constants.physics.springLength;
  9647. this.customLength = false;
  9648. this.selected = false;
  9649. this.smooth = constants.smoothCurves;
  9650. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  9651. this.from = null; // a node
  9652. this.to = null; // a node
  9653. this.via = null; // a temp node
  9654. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  9655. // by storing the original information we can revert to the original connection when the cluser is opened.
  9656. this.originalFromId = [];
  9657. this.originalToId = [];
  9658. this.connected = false;
  9659. // Added to support dashed lines
  9660. // David Jordan
  9661. // 2012-08-08
  9662. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  9663. this.color = {color:constants.edges.color.color,
  9664. highlight:constants.edges.color.highlight};
  9665. this.widthFixed = false;
  9666. this.lengthFixed = false;
  9667. this.setProperties(properties, constants);
  9668. }
  9669. /**
  9670. * Set or overwrite properties for the edge
  9671. * @param {Object} properties an object with properties
  9672. * @param {Object} constants and object with default, global properties
  9673. */
  9674. Edge.prototype.setProperties = function(properties, constants) {
  9675. if (!properties) {
  9676. return;
  9677. }
  9678. if (properties.from !== undefined) {this.fromId = properties.from;}
  9679. if (properties.to !== undefined) {this.toId = properties.to;}
  9680. if (properties.id !== undefined) {this.id = properties.id;}
  9681. if (properties.style !== undefined) {this.style = properties.style;}
  9682. if (properties.label !== undefined) {this.label = properties.label;}
  9683. if (this.label) {
  9684. this.fontSize = constants.edges.fontSize;
  9685. this.fontFace = constants.edges.fontFace;
  9686. this.fontColor = constants.edges.fontColor;
  9687. this.fontFill = constants.edges.fontFill;
  9688. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  9689. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  9690. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  9691. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  9692. }
  9693. if (properties.title !== undefined) {this.title = properties.title;}
  9694. if (properties.width !== undefined) {this.width = properties.width;}
  9695. if (properties.value !== undefined) {this.value = properties.value;}
  9696. if (properties.length !== undefined) {this.length = properties.length;
  9697. this.customLength = true;}
  9698. // scale the arrow
  9699. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  9700. // Added to support dashed lines
  9701. // David Jordan
  9702. // 2012-08-08
  9703. if (properties.dash) {
  9704. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  9705. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  9706. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  9707. }
  9708. if (properties.color !== undefined) {
  9709. if (util.isString(properties.color)) {
  9710. this.color.color = properties.color;
  9711. this.color.highlight = properties.color;
  9712. }
  9713. else {
  9714. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  9715. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  9716. }
  9717. }
  9718. // A node is connected when it has a from and to node.
  9719. this.connect();
  9720. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  9721. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  9722. // set draw method based on style
  9723. switch (this.style) {
  9724. case 'line': this.draw = this._drawLine; break;
  9725. case 'arrow': this.draw = this._drawArrow; break;
  9726. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  9727. case 'dash-line': this.draw = this._drawDashLine; break;
  9728. default: this.draw = this._drawLine; break;
  9729. }
  9730. };
  9731. /**
  9732. * Connect an edge to its nodes
  9733. */
  9734. Edge.prototype.connect = function () {
  9735. this.disconnect();
  9736. this.from = this.graph.nodes[this.fromId] || null;
  9737. this.to = this.graph.nodes[this.toId] || null;
  9738. this.connected = (this.from && this.to);
  9739. if (this.connected) {
  9740. this.from.attachEdge(this);
  9741. this.to.attachEdge(this);
  9742. }
  9743. else {
  9744. if (this.from) {
  9745. this.from.detachEdge(this);
  9746. }
  9747. if (this.to) {
  9748. this.to.detachEdge(this);
  9749. }
  9750. }
  9751. };
  9752. /**
  9753. * Disconnect an edge from its nodes
  9754. */
  9755. Edge.prototype.disconnect = function () {
  9756. if (this.from) {
  9757. this.from.detachEdge(this);
  9758. this.from = null;
  9759. }
  9760. if (this.to) {
  9761. this.to.detachEdge(this);
  9762. this.to = null;
  9763. }
  9764. this.connected = false;
  9765. };
  9766. /**
  9767. * get the title of this edge.
  9768. * @return {string} title The title of the edge, or undefined when no title
  9769. * has been set.
  9770. */
  9771. Edge.prototype.getTitle = function() {
  9772. return typeof this.title === "function" ? this.title() : this.title;
  9773. };
  9774. /**
  9775. * Retrieve the value of the edge. Can be undefined
  9776. * @return {Number} value
  9777. */
  9778. Edge.prototype.getValue = function() {
  9779. return this.value;
  9780. };
  9781. /**
  9782. * Adjust the value range of the edge. The edge will adjust it's width
  9783. * based on its value.
  9784. * @param {Number} min
  9785. * @param {Number} max
  9786. */
  9787. Edge.prototype.setValueRange = function(min, max) {
  9788. if (!this.widthFixed && this.value !== undefined) {
  9789. var scale = (this.widthMax - this.widthMin) / (max - min);
  9790. this.width = (this.value - min) * scale + this.widthMin;
  9791. }
  9792. };
  9793. /**
  9794. * Redraw a edge
  9795. * Draw this edge in the given canvas
  9796. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9797. * @param {CanvasRenderingContext2D} ctx
  9798. */
  9799. Edge.prototype.draw = function(ctx) {
  9800. throw "Method draw not initialized in edge";
  9801. };
  9802. /**
  9803. * Check if this object is overlapping with the provided object
  9804. * @param {Object} obj an object with parameters left, top
  9805. * @return {boolean} True if location is located on the edge
  9806. */
  9807. Edge.prototype.isOverlappingWith = function(obj) {
  9808. if (this.connected) {
  9809. var distMax = 10;
  9810. var xFrom = this.from.x;
  9811. var yFrom = this.from.y;
  9812. var xTo = this.to.x;
  9813. var yTo = this.to.y;
  9814. var xObj = obj.left;
  9815. var yObj = obj.top;
  9816. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  9817. return (dist < distMax);
  9818. }
  9819. else {
  9820. return false
  9821. }
  9822. };
  9823. /**
  9824. * Redraw a edge as a line
  9825. * Draw this edge in the given canvas
  9826. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9827. * @param {CanvasRenderingContext2D} ctx
  9828. * @private
  9829. */
  9830. Edge.prototype._drawLine = function(ctx) {
  9831. // set style
  9832. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  9833. else {ctx.strokeStyle = this.color.color;}
  9834. ctx.lineWidth = this._getLineWidth();
  9835. if (this.from != this.to) {
  9836. // draw line
  9837. this._line(ctx);
  9838. // draw label
  9839. var point;
  9840. if (this.label) {
  9841. if (this.smooth == true) {
  9842. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9843. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9844. point = {x:midpointX, y:midpointY};
  9845. }
  9846. else {
  9847. point = this._pointOnLine(0.5);
  9848. }
  9849. this._label(ctx, this.label, point.x, point.y);
  9850. }
  9851. }
  9852. else {
  9853. var x, y;
  9854. var radius = this.length / 4;
  9855. var node = this.from;
  9856. if (!node.width) {
  9857. node.resize(ctx);
  9858. }
  9859. if (node.width > node.height) {
  9860. x = node.x + node.width / 2;
  9861. y = node.y - radius;
  9862. }
  9863. else {
  9864. x = node.x + radius;
  9865. y = node.y - node.height / 2;
  9866. }
  9867. this._circle(ctx, x, y, radius);
  9868. point = this._pointOnCircle(x, y, radius, 0.5);
  9869. this._label(ctx, this.label, point.x, point.y);
  9870. }
  9871. };
  9872. /**
  9873. * Get the line width of the edge. Depends on width and whether one of the
  9874. * connected nodes is selected.
  9875. * @return {Number} width
  9876. * @private
  9877. */
  9878. Edge.prototype._getLineWidth = function() {
  9879. if (this.selected == true) {
  9880. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  9881. }
  9882. else {
  9883. return this.width*this.graphScaleInv;
  9884. }
  9885. };
  9886. /**
  9887. * Draw a line between two nodes
  9888. * @param {CanvasRenderingContext2D} ctx
  9889. * @private
  9890. */
  9891. Edge.prototype._line = function (ctx) {
  9892. // draw a straight line
  9893. ctx.beginPath();
  9894. ctx.moveTo(this.from.x, this.from.y);
  9895. if (this.smooth == true) {
  9896. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9897. }
  9898. else {
  9899. ctx.lineTo(this.to.x, this.to.y);
  9900. }
  9901. ctx.stroke();
  9902. };
  9903. /**
  9904. * Draw a line from a node to itself, a circle
  9905. * @param {CanvasRenderingContext2D} ctx
  9906. * @param {Number} x
  9907. * @param {Number} y
  9908. * @param {Number} radius
  9909. * @private
  9910. */
  9911. Edge.prototype._circle = function (ctx, x, y, radius) {
  9912. // draw a circle
  9913. ctx.beginPath();
  9914. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9915. ctx.stroke();
  9916. };
  9917. /**
  9918. * Draw label with white background and with the middle at (x, y)
  9919. * @param {CanvasRenderingContext2D} ctx
  9920. * @param {String} text
  9921. * @param {Number} x
  9922. * @param {Number} y
  9923. * @private
  9924. */
  9925. Edge.prototype._label = function (ctx, text, x, y) {
  9926. if (text) {
  9927. // TODO: cache the calculated size
  9928. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  9929. this.fontSize + "px " + this.fontFace;
  9930. ctx.fillStyle = this.fontFill;
  9931. var width = ctx.measureText(text).width;
  9932. var height = this.fontSize;
  9933. var left = x - width / 2;
  9934. var top = y - height / 2;
  9935. ctx.fillRect(left, top, width, height);
  9936. // draw text
  9937. ctx.fillStyle = this.fontColor || "black";
  9938. ctx.textAlign = "left";
  9939. ctx.textBaseline = "top";
  9940. ctx.fillText(text, left, top);
  9941. }
  9942. };
  9943. /**
  9944. * Redraw a edge as a dashed line
  9945. * Draw this edge in the given canvas
  9946. * @author David Jordan
  9947. * @date 2012-08-08
  9948. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9949. * @param {CanvasRenderingContext2D} ctx
  9950. * @private
  9951. */
  9952. Edge.prototype._drawDashLine = function(ctx) {
  9953. // set style
  9954. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  9955. else {ctx.strokeStyle = this.color.color;}
  9956. ctx.lineWidth = this._getLineWidth();
  9957. // only firefox and chrome support this method, else we use the legacy one.
  9958. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  9959. ctx.beginPath();
  9960. ctx.moveTo(this.from.x, this.from.y);
  9961. // configure the dash pattern
  9962. var pattern = [0];
  9963. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  9964. pattern = [this.dash.length,this.dash.gap];
  9965. }
  9966. else {
  9967. pattern = [5,5];
  9968. }
  9969. // set dash settings for chrome or firefox
  9970. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9971. ctx.setLineDash(pattern);
  9972. ctx.lineDashOffset = 0;
  9973. } else { //Firefox
  9974. ctx.mozDash = pattern;
  9975. ctx.mozDashOffset = 0;
  9976. }
  9977. // draw the line
  9978. if (this.smooth == true) {
  9979. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9980. }
  9981. else {
  9982. ctx.lineTo(this.to.x, this.to.y);
  9983. }
  9984. ctx.stroke();
  9985. // restore the dash settings.
  9986. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9987. ctx.setLineDash([0]);
  9988. ctx.lineDashOffset = 0;
  9989. } else { //Firefox
  9990. ctx.mozDash = [0];
  9991. ctx.mozDashOffset = 0;
  9992. }
  9993. }
  9994. else { // unsupporting smooth lines
  9995. // draw dashed line
  9996. ctx.beginPath();
  9997. ctx.lineCap = 'round';
  9998. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9999. {
  10000. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  10001. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  10002. }
  10003. 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
  10004. {
  10005. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  10006. [this.dash.length,this.dash.gap]);
  10007. }
  10008. else //If all else fails draw a line
  10009. {
  10010. ctx.moveTo(this.from.x, this.from.y);
  10011. ctx.lineTo(this.to.x, this.to.y);
  10012. }
  10013. ctx.stroke();
  10014. }
  10015. // draw label
  10016. if (this.label) {
  10017. var point;
  10018. if (this.smooth == true) {
  10019. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  10020. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  10021. point = {x:midpointX, y:midpointY};
  10022. }
  10023. else {
  10024. point = this._pointOnLine(0.5);
  10025. }
  10026. this._label(ctx, this.label, point.x, point.y);
  10027. }
  10028. };
  10029. /**
  10030. * Get a point on a line
  10031. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  10032. * @return {Object} point
  10033. * @private
  10034. */
  10035. Edge.prototype._pointOnLine = function (percentage) {
  10036. return {
  10037. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  10038. y: (1 - percentage) * this.from.y + percentage * this.to.y
  10039. }
  10040. };
  10041. /**
  10042. * Get a point on a circle
  10043. * @param {Number} x
  10044. * @param {Number} y
  10045. * @param {Number} radius
  10046. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  10047. * @return {Object} point
  10048. * @private
  10049. */
  10050. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  10051. var angle = (percentage - 3/8) * 2 * Math.PI;
  10052. return {
  10053. x: x + radius * Math.cos(angle),
  10054. y: y - radius * Math.sin(angle)
  10055. }
  10056. };
  10057. /**
  10058. * Redraw a edge as a line with an arrow halfway the line
  10059. * Draw this edge in the given canvas
  10060. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10061. * @param {CanvasRenderingContext2D} ctx
  10062. * @private
  10063. */
  10064. Edge.prototype._drawArrowCenter = function(ctx) {
  10065. var point;
  10066. // set style
  10067. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  10068. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  10069. ctx.lineWidth = this._getLineWidth();
  10070. if (this.from != this.to) {
  10071. // draw line
  10072. this._line(ctx);
  10073. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  10074. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  10075. // draw an arrow halfway the line
  10076. if (this.smooth == true) {
  10077. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  10078. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  10079. point = {x:midpointX, y:midpointY};
  10080. }
  10081. else {
  10082. point = this._pointOnLine(0.5);
  10083. }
  10084. ctx.arrow(point.x, point.y, angle, length);
  10085. ctx.fill();
  10086. ctx.stroke();
  10087. // draw label
  10088. if (this.label) {
  10089. this._label(ctx, this.label, point.x, point.y);
  10090. }
  10091. }
  10092. else {
  10093. // draw circle
  10094. var x, y;
  10095. var radius = 0.25 * Math.max(100,this.length);
  10096. var node = this.from;
  10097. if (!node.width) {
  10098. node.resize(ctx);
  10099. }
  10100. if (node.width > node.height) {
  10101. x = node.x + node.width * 0.5;
  10102. y = node.y - radius;
  10103. }
  10104. else {
  10105. x = node.x + radius;
  10106. y = node.y - node.height * 0.5;
  10107. }
  10108. this._circle(ctx, x, y, radius);
  10109. // draw all arrows
  10110. var angle = 0.2 * Math.PI;
  10111. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  10112. point = this._pointOnCircle(x, y, radius, 0.5);
  10113. ctx.arrow(point.x, point.y, angle, length);
  10114. ctx.fill();
  10115. ctx.stroke();
  10116. // draw label
  10117. if (this.label) {
  10118. point = this._pointOnCircle(x, y, radius, 0.5);
  10119. this._label(ctx, this.label, point.x, point.y);
  10120. }
  10121. }
  10122. };
  10123. /**
  10124. * Redraw a edge as a line with an arrow
  10125. * Draw this edge in the given canvas
  10126. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10127. * @param {CanvasRenderingContext2D} ctx
  10128. * @private
  10129. */
  10130. Edge.prototype._drawArrow = function(ctx) {
  10131. // set style
  10132. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  10133. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  10134. ctx.lineWidth = this._getLineWidth();
  10135. var angle, length;
  10136. //draw a line
  10137. if (this.from != this.to) {
  10138. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  10139. var dx = (this.to.x - this.from.x);
  10140. var dy = (this.to.y - this.from.y);
  10141. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  10142. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  10143. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  10144. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  10145. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  10146. if (this.smooth == true) {
  10147. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  10148. dx = (this.to.x - this.via.x);
  10149. dy = (this.to.y - this.via.y);
  10150. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  10151. }
  10152. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  10153. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  10154. var xTo,yTo;
  10155. if (this.smooth == true) {
  10156. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  10157. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  10158. }
  10159. else {
  10160. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  10161. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  10162. }
  10163. ctx.beginPath();
  10164. ctx.moveTo(xFrom,yFrom);
  10165. if (this.smooth == true) {
  10166. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  10167. }
  10168. else {
  10169. ctx.lineTo(xTo, yTo);
  10170. }
  10171. ctx.stroke();
  10172. // draw arrow at the end of the line
  10173. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  10174. ctx.arrow(xTo, yTo, angle, length);
  10175. ctx.fill();
  10176. ctx.stroke();
  10177. // draw label
  10178. if (this.label) {
  10179. var point;
  10180. if (this.smooth == true) {
  10181. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  10182. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  10183. point = {x:midpointX, y:midpointY};
  10184. }
  10185. else {
  10186. point = this._pointOnLine(0.5);
  10187. }
  10188. this._label(ctx, this.label, point.x, point.y);
  10189. }
  10190. }
  10191. else {
  10192. // draw circle
  10193. var node = this.from;
  10194. var x, y, arrow;
  10195. var radius = 0.25 * Math.max(100,this.length);
  10196. if (!node.width) {
  10197. node.resize(ctx);
  10198. }
  10199. if (node.width > node.height) {
  10200. x = node.x + node.width * 0.5;
  10201. y = node.y - radius;
  10202. arrow = {
  10203. x: x,
  10204. y: node.y,
  10205. angle: 0.9 * Math.PI
  10206. };
  10207. }
  10208. else {
  10209. x = node.x + radius;
  10210. y = node.y - node.height * 0.5;
  10211. arrow = {
  10212. x: node.x,
  10213. y: y,
  10214. angle: 0.6 * Math.PI
  10215. };
  10216. }
  10217. ctx.beginPath();
  10218. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  10219. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  10220. ctx.stroke();
  10221. // draw all arrows
  10222. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  10223. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  10224. ctx.fill();
  10225. ctx.stroke();
  10226. // draw label
  10227. if (this.label) {
  10228. point = this._pointOnCircle(x, y, radius, 0.5);
  10229. this._label(ctx, this.label, point.x, point.y);
  10230. }
  10231. }
  10232. };
  10233. /**
  10234. * Calculate the distance between a point (x3,y3) and a line segment from
  10235. * (x1,y1) to (x2,y2).
  10236. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  10237. * @param {number} x1
  10238. * @param {number} y1
  10239. * @param {number} x2
  10240. * @param {number} y2
  10241. * @param {number} x3
  10242. * @param {number} y3
  10243. * @private
  10244. */
  10245. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  10246. if (this.smooth == true) {
  10247. var minDistance = 1e9;
  10248. var i,t,x,y,dx,dy;
  10249. for (i = 0; i < 10; i++) {
  10250. t = 0.1*i;
  10251. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  10252. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  10253. dx = Math.abs(x3-x);
  10254. dy = Math.abs(y3-y);
  10255. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  10256. }
  10257. return minDistance
  10258. }
  10259. else {
  10260. var px = x2-x1,
  10261. py = y2-y1,
  10262. something = px*px + py*py,
  10263. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  10264. if (u > 1) {
  10265. u = 1;
  10266. }
  10267. else if (u < 0) {
  10268. u = 0;
  10269. }
  10270. var x = x1 + u * px,
  10271. y = y1 + u * py,
  10272. dx = x - x3,
  10273. dy = y - y3;
  10274. //# Note: If the actual distance does not matter,
  10275. //# if you only want to compare what this function
  10276. //# returns to other results of this function, you
  10277. //# can just return the squared distance instead
  10278. //# (i.e. remove the sqrt) to gain a little performance
  10279. return Math.sqrt(dx*dx + dy*dy);
  10280. }
  10281. };
  10282. /**
  10283. * This allows the zoom level of the graph to influence the rendering
  10284. *
  10285. * @param scale
  10286. */
  10287. Edge.prototype.setScale = function(scale) {
  10288. this.graphScaleInv = 1.0/scale;
  10289. };
  10290. Edge.prototype.select = function() {
  10291. this.selected = true;
  10292. };
  10293. Edge.prototype.unselect = function() {
  10294. this.selected = false;
  10295. };
  10296. Edge.prototype.positionBezierNode = function() {
  10297. if (this.via !== null) {
  10298. this.via.x = 0.5 * (this.from.x + this.to.x);
  10299. this.via.y = 0.5 * (this.from.y + this.to.y);
  10300. }
  10301. };
  10302. /**
  10303. * Popup is a class to create a popup window with some text
  10304. * @param {Element} container The container object.
  10305. * @param {Number} [x]
  10306. * @param {Number} [y]
  10307. * @param {String} [text]
  10308. * @param {Object} [style] An object containing borderColor,
  10309. * backgroundColor, etc.
  10310. */
  10311. function Popup(container, x, y, text, style) {
  10312. if (container) {
  10313. this.container = container;
  10314. }
  10315. else {
  10316. this.container = document.body;
  10317. }
  10318. // x, y and text are optional, see if a style object was passed in their place
  10319. if (style === undefined) {
  10320. if (typeof x === "object") {
  10321. style = x;
  10322. x = undefined;
  10323. } else if (typeof text === "object") {
  10324. style = text;
  10325. text = undefined;
  10326. } else {
  10327. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  10328. style = {
  10329. fontColor: 'black',
  10330. fontSize: 14, // px
  10331. fontFace: 'verdana',
  10332. color: {
  10333. border: '#666',
  10334. background: '#FFFFC6'
  10335. }
  10336. }
  10337. }
  10338. }
  10339. this.x = 0;
  10340. this.y = 0;
  10341. this.padding = 5;
  10342. if (x !== undefined && y !== undefined ) {
  10343. this.setPosition(x, y);
  10344. }
  10345. if (text !== undefined) {
  10346. this.setText(text);
  10347. }
  10348. // create the frame
  10349. this.frame = document.createElement("div");
  10350. var styleAttr = this.frame.style;
  10351. styleAttr.position = "absolute";
  10352. styleAttr.visibility = "hidden";
  10353. styleAttr.border = "1px solid " + style.color.border;
  10354. styleAttr.color = style.fontColor;
  10355. styleAttr.fontSize = style.fontSize + "px";
  10356. styleAttr.fontFamily = style.fontFace;
  10357. styleAttr.padding = this.padding + "px";
  10358. styleAttr.backgroundColor = style.color.background;
  10359. styleAttr.borderRadius = "3px";
  10360. styleAttr.MozBorderRadius = "3px";
  10361. styleAttr.WebkitBorderRadius = "3px";
  10362. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  10363. styleAttr.whiteSpace = "nowrap";
  10364. this.container.appendChild(this.frame);
  10365. }
  10366. /**
  10367. * @param {number} x Horizontal position of the popup window
  10368. * @param {number} y Vertical position of the popup window
  10369. */
  10370. Popup.prototype.setPosition = function(x, y) {
  10371. this.x = parseInt(x);
  10372. this.y = parseInt(y);
  10373. };
  10374. /**
  10375. * Set the text for the popup window. This can be HTML code
  10376. * @param {string} text
  10377. */
  10378. Popup.prototype.setText = function(text) {
  10379. this.frame.innerHTML = text;
  10380. };
  10381. /**
  10382. * Show the popup window
  10383. * @param {boolean} show Optional. Show or hide the window
  10384. */
  10385. Popup.prototype.show = function (show) {
  10386. if (show === undefined) {
  10387. show = true;
  10388. }
  10389. if (show) {
  10390. var height = this.frame.clientHeight;
  10391. var width = this.frame.clientWidth;
  10392. var maxHeight = this.frame.parentNode.clientHeight;
  10393. var maxWidth = this.frame.parentNode.clientWidth;
  10394. var top = (this.y - height);
  10395. if (top + height + this.padding > maxHeight) {
  10396. top = maxHeight - height - this.padding;
  10397. }
  10398. if (top < this.padding) {
  10399. top = this.padding;
  10400. }
  10401. var left = this.x;
  10402. if (left + width + this.padding > maxWidth) {
  10403. left = maxWidth - width - this.padding;
  10404. }
  10405. if (left < this.padding) {
  10406. left = this.padding;
  10407. }
  10408. this.frame.style.left = left + "px";
  10409. this.frame.style.top = top + "px";
  10410. this.frame.style.visibility = "visible";
  10411. }
  10412. else {
  10413. this.hide();
  10414. }
  10415. };
  10416. /**
  10417. * Hide the popup window
  10418. */
  10419. Popup.prototype.hide = function () {
  10420. this.frame.style.visibility = "hidden";
  10421. };
  10422. /**
  10423. * @class Groups
  10424. * This class can store groups and properties specific for groups.
  10425. */
  10426. function Groups() {
  10427. this.clear();
  10428. this.defaultIndex = 0;
  10429. }
  10430. /**
  10431. * default constants for group colors
  10432. */
  10433. Groups.DEFAULT = [
  10434. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  10435. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  10436. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  10437. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  10438. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  10439. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  10440. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  10441. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  10442. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  10443. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  10444. ];
  10445. /**
  10446. * Clear all groups
  10447. */
  10448. Groups.prototype.clear = function () {
  10449. this.groups = {};
  10450. this.groups.length = function()
  10451. {
  10452. var i = 0;
  10453. for ( var p in this ) {
  10454. if (this.hasOwnProperty(p)) {
  10455. i++;
  10456. }
  10457. }
  10458. return i;
  10459. }
  10460. };
  10461. /**
  10462. * get group properties of a groupname. If groupname is not found, a new group
  10463. * is added.
  10464. * @param {*} groupname Can be a number, string, Date, etc.
  10465. * @return {Object} group The created group, containing all group properties
  10466. */
  10467. Groups.prototype.get = function (groupname) {
  10468. var group = this.groups[groupname];
  10469. if (group == undefined) {
  10470. // create new group
  10471. var index = this.defaultIndex % Groups.DEFAULT.length;
  10472. this.defaultIndex++;
  10473. group = {};
  10474. group.color = Groups.DEFAULT[index];
  10475. this.groups[groupname] = group;
  10476. }
  10477. return group;
  10478. };
  10479. /**
  10480. * Add a custom group style
  10481. * @param {String} groupname
  10482. * @param {Object} style An object containing borderColor,
  10483. * backgroundColor, etc.
  10484. * @return {Object} group The created group object
  10485. */
  10486. Groups.prototype.add = function (groupname, style) {
  10487. this.groups[groupname] = style;
  10488. if (style.color) {
  10489. style.color = util.parseColor(style.color);
  10490. }
  10491. return style;
  10492. };
  10493. /**
  10494. * @class Images
  10495. * This class loads images and keeps them stored.
  10496. */
  10497. function Images() {
  10498. this.images = {};
  10499. this.callback = undefined;
  10500. }
  10501. /**
  10502. * Set an onload callback function. This will be called each time an image
  10503. * is loaded
  10504. * @param {function} callback
  10505. */
  10506. Images.prototype.setOnloadCallback = function(callback) {
  10507. this.callback = callback;
  10508. };
  10509. /**
  10510. *
  10511. * @param {string} url Url of the image
  10512. * @return {Image} img The image object
  10513. */
  10514. Images.prototype.load = function(url) {
  10515. var img = this.images[url];
  10516. if (img == undefined) {
  10517. // create the image
  10518. var images = this;
  10519. img = new Image();
  10520. this.images[url] = img;
  10521. img.onload = function() {
  10522. if (images.callback) {
  10523. images.callback(this);
  10524. }
  10525. };
  10526. img.src = url;
  10527. }
  10528. return img;
  10529. };
  10530. /**
  10531. * Created by Alex on 2/6/14.
  10532. */
  10533. var physicsMixin = {
  10534. /**
  10535. * Toggling barnes Hut calculation on and off.
  10536. *
  10537. * @private
  10538. */
  10539. _toggleBarnesHut: function () {
  10540. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  10541. this._loadSelectedForceSolver();
  10542. this.moving = true;
  10543. this.start();
  10544. },
  10545. /**
  10546. * This loads the node force solver based on the barnes hut or repulsion algorithm
  10547. *
  10548. * @private
  10549. */
  10550. _loadSelectedForceSolver: function () {
  10551. // this overloads the this._calculateNodeForces
  10552. if (this.constants.physics.barnesHut.enabled == true) {
  10553. this._clearMixin(repulsionMixin);
  10554. this._clearMixin(hierarchalRepulsionMixin);
  10555. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  10556. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  10557. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  10558. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  10559. this._loadMixin(barnesHutMixin);
  10560. }
  10561. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  10562. this._clearMixin(barnesHutMixin);
  10563. this._clearMixin(repulsionMixin);
  10564. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  10565. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  10566. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  10567. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  10568. this._loadMixin(hierarchalRepulsionMixin);
  10569. }
  10570. else {
  10571. this._clearMixin(barnesHutMixin);
  10572. this._clearMixin(hierarchalRepulsionMixin);
  10573. this.barnesHutTree = undefined;
  10574. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  10575. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  10576. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  10577. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  10578. this._loadMixin(repulsionMixin);
  10579. }
  10580. },
  10581. /**
  10582. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  10583. * if there is more than one node. If it is just one node, we dont calculate anything.
  10584. *
  10585. * @private
  10586. */
  10587. _initializeForceCalculation: function () {
  10588. // stop calculation if there is only one node
  10589. if (this.nodeIndices.length == 1) {
  10590. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  10591. }
  10592. else {
  10593. // if there are too many nodes on screen, we cluster without repositioning
  10594. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  10595. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  10596. }
  10597. // we now start the force calculation
  10598. this._calculateForces();
  10599. }
  10600. },
  10601. /**
  10602. * Calculate the external forces acting on the nodes
  10603. * Forces are caused by: edges, repulsing forces between nodes, gravity
  10604. * @private
  10605. */
  10606. _calculateForces: function () {
  10607. // Gravity is required to keep separated groups from floating off
  10608. // the forces are reset to zero in this loop by using _setForce instead
  10609. // of _addForce
  10610. this._calculateGravitationalForces();
  10611. this._calculateNodeForces();
  10612. if (this.constants.smoothCurves == true) {
  10613. this._calculateSpringForcesWithSupport();
  10614. }
  10615. else {
  10616. this._calculateSpringForces();
  10617. }
  10618. },
  10619. /**
  10620. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  10621. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  10622. * This function joins the datanodes and invisible (called support) nodes into one object.
  10623. * We do this so we do not contaminate this.nodes with the support nodes.
  10624. *
  10625. * @private
  10626. */
  10627. _updateCalculationNodes: function () {
  10628. if (this.constants.smoothCurves == true) {
  10629. this.calculationNodes = {};
  10630. this.calculationNodeIndices = [];
  10631. for (var nodeId in this.nodes) {
  10632. if (this.nodes.hasOwnProperty(nodeId)) {
  10633. this.calculationNodes[nodeId] = this.nodes[nodeId];
  10634. }
  10635. }
  10636. var supportNodes = this.sectors['support']['nodes'];
  10637. for (var supportNodeId in supportNodes) {
  10638. if (supportNodes.hasOwnProperty(supportNodeId)) {
  10639. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  10640. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  10641. }
  10642. else {
  10643. supportNodes[supportNodeId]._setForce(0, 0);
  10644. }
  10645. }
  10646. }
  10647. for (var idx in this.calculationNodes) {
  10648. if (this.calculationNodes.hasOwnProperty(idx)) {
  10649. this.calculationNodeIndices.push(idx);
  10650. }
  10651. }
  10652. }
  10653. else {
  10654. this.calculationNodes = this.nodes;
  10655. this.calculationNodeIndices = this.nodeIndices;
  10656. }
  10657. },
  10658. /**
  10659. * this function applies the central gravity effect to keep groups from floating off
  10660. *
  10661. * @private
  10662. */
  10663. _calculateGravitationalForces: function () {
  10664. var dx, dy, distance, node, i;
  10665. var nodes = this.calculationNodes;
  10666. var gravity = this.constants.physics.centralGravity;
  10667. var gravityForce = 0;
  10668. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  10669. node = nodes[this.calculationNodeIndices[i]];
  10670. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  10671. // gravity does not apply when we are in a pocket sector
  10672. if (this._sector() == "default" && gravity != 0) {
  10673. dx = -node.x;
  10674. dy = -node.y;
  10675. distance = Math.sqrt(dx * dx + dy * dy);
  10676. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  10677. node.fx = dx * gravityForce;
  10678. node.fy = dy * gravityForce;
  10679. }
  10680. else {
  10681. node.fx = 0;
  10682. node.fy = 0;
  10683. }
  10684. }
  10685. },
  10686. /**
  10687. * this function calculates the effects of the springs in the case of unsmooth curves.
  10688. *
  10689. * @private
  10690. */
  10691. _calculateSpringForces: function () {
  10692. var edgeLength, edge, edgeId;
  10693. var dx, dy, fx, fy, springForce, distance;
  10694. var edges = this.edges;
  10695. // forces caused by the edges, modelled as springs
  10696. for (edgeId in edges) {
  10697. if (edges.hasOwnProperty(edgeId)) {
  10698. edge = edges[edgeId];
  10699. if (edge.connected) {
  10700. // only calculate forces if nodes are in the same sector
  10701. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10702. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10703. // this implies that the edges between big clusters are longer
  10704. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  10705. dx = (edge.from.x - edge.to.x);
  10706. dy = (edge.from.y - edge.to.y);
  10707. distance = Math.sqrt(dx * dx + dy * dy);
  10708. if (distance == 0) {
  10709. distance = 0.01;
  10710. }
  10711. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  10712. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  10713. fx = dx * springForce;
  10714. fy = dy * springForce;
  10715. edge.from.fx += fx;
  10716. edge.from.fy += fy;
  10717. edge.to.fx -= fx;
  10718. edge.to.fy -= fy;
  10719. }
  10720. }
  10721. }
  10722. }
  10723. },
  10724. /**
  10725. * This function calculates the springforces on the nodes, accounting for the support nodes.
  10726. *
  10727. * @private
  10728. */
  10729. _calculateSpringForcesWithSupport: function () {
  10730. var edgeLength, edge, edgeId, combinedClusterSize;
  10731. var edges = this.edges;
  10732. // forces caused by the edges, modelled as springs
  10733. for (edgeId in edges) {
  10734. if (edges.hasOwnProperty(edgeId)) {
  10735. edge = edges[edgeId];
  10736. if (edge.connected) {
  10737. // only calculate forces if nodes are in the same sector
  10738. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10739. if (edge.via != null) {
  10740. var node1 = edge.to;
  10741. var node2 = edge.via;
  10742. var node3 = edge.from;
  10743. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10744. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  10745. // this implies that the edges between big clusters are longer
  10746. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  10747. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  10748. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  10749. }
  10750. }
  10751. }
  10752. }
  10753. }
  10754. },
  10755. /**
  10756. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  10757. *
  10758. * @param node1
  10759. * @param node2
  10760. * @param edgeLength
  10761. * @private
  10762. */
  10763. _calculateSpringForce: function (node1, node2, edgeLength) {
  10764. var dx, dy, fx, fy, springForce, distance;
  10765. dx = (node1.x - node2.x);
  10766. dy = (node1.y - node2.y);
  10767. distance = Math.sqrt(dx * dx + dy * dy);
  10768. if (distance == 0) {
  10769. distance = 0.01;
  10770. }
  10771. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  10772. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  10773. fx = dx * springForce;
  10774. fy = dy * springForce;
  10775. node1.fx += fx;
  10776. node1.fy += fy;
  10777. node2.fx -= fx;
  10778. node2.fy -= fy;
  10779. },
  10780. /**
  10781. * Load the HTML for the physics config and bind it
  10782. * @private
  10783. */
  10784. _loadPhysicsConfiguration: function () {
  10785. if (this.physicsConfiguration === undefined) {
  10786. this.backupConstants = {};
  10787. util.copyObject(this.constants, this.backupConstants);
  10788. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  10789. this.physicsConfiguration = document.createElement('div');
  10790. this.physicsConfiguration.className = "PhysicsConfiguration";
  10791. this.physicsConfiguration.innerHTML = '' +
  10792. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  10793. '<tr>' +
  10794. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  10795. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  10796. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  10797. '</tr>' +
  10798. '</table>' +
  10799. '<table id="graph_BH_table" style="display:none">' +
  10800. '<tr><td><b>Barnes Hut</b></td></tr>' +
  10801. '<tr>' +
  10802. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
  10803. '</tr>' +
  10804. '<tr>' +
  10805. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
  10806. '</tr>' +
  10807. '<tr>' +
  10808. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
  10809. '</tr>' +
  10810. '<tr>' +
  10811. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
  10812. '</tr>' +
  10813. '<tr>' +
  10814. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
  10815. '</tr>' +
  10816. '</table>' +
  10817. '<table id="graph_R_table" style="display:none">' +
  10818. '<tr><td><b>Repulsion</b></td></tr>' +
  10819. '<tr>' +
  10820. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
  10821. '</tr>' +
  10822. '<tr>' +
  10823. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
  10824. '</tr>' +
  10825. '<tr>' +
  10826. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
  10827. '</tr>' +
  10828. '<tr>' +
  10829. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
  10830. '</tr>' +
  10831. '<tr>' +
  10832. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
  10833. '</tr>' +
  10834. '</table>' +
  10835. '<table id="graph_H_table" style="display:none">' +
  10836. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  10837. '<tr>' +
  10838. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
  10839. '</tr>' +
  10840. '<tr>' +
  10841. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
  10842. '</tr>' +
  10843. '<tr>' +
  10844. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
  10845. '</tr>' +
  10846. '<tr>' +
  10847. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
  10848. '</tr>' +
  10849. '<tr>' +
  10850. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
  10851. '</tr>' +
  10852. '<tr>' +
  10853. '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
  10854. '</tr>' +
  10855. '<tr>' +
  10856. '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
  10857. '</tr>' +
  10858. '<tr>' +
  10859. '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
  10860. '</tr>' +
  10861. '</table>' +
  10862. '<table><tr><td><b>Options:</b></td></tr>' +
  10863. '<tr>' +
  10864. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  10865. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  10866. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  10867. '</tr>' +
  10868. '</table>'
  10869. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  10870. this.optionsDiv = document.createElement("div");
  10871. this.optionsDiv.style.fontSize = "14px";
  10872. this.optionsDiv.style.fontFamily = "verdana";
  10873. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  10874. var rangeElement;
  10875. rangeElement = document.getElementById('graph_BH_gc');
  10876. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  10877. rangeElement = document.getElementById('graph_BH_cg');
  10878. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  10879. rangeElement = document.getElementById('graph_BH_sc');
  10880. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  10881. rangeElement = document.getElementById('graph_BH_sl');
  10882. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  10883. rangeElement = document.getElementById('graph_BH_damp');
  10884. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  10885. rangeElement = document.getElementById('graph_R_nd');
  10886. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  10887. rangeElement = document.getElementById('graph_R_cg');
  10888. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  10889. rangeElement = document.getElementById('graph_R_sc');
  10890. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  10891. rangeElement = document.getElementById('graph_R_sl');
  10892. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  10893. rangeElement = document.getElementById('graph_R_damp');
  10894. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  10895. rangeElement = document.getElementById('graph_H_nd');
  10896. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  10897. rangeElement = document.getElementById('graph_H_cg');
  10898. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  10899. rangeElement = document.getElementById('graph_H_sc');
  10900. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  10901. rangeElement = document.getElementById('graph_H_sl');
  10902. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  10903. rangeElement = document.getElementById('graph_H_damp');
  10904. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  10905. rangeElement = document.getElementById('graph_H_direction');
  10906. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  10907. rangeElement = document.getElementById('graph_H_levsep');
  10908. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  10909. rangeElement = document.getElementById('graph_H_nspac');
  10910. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  10911. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10912. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10913. var radioButton3 = document.getElementById("graph_physicsMethod3");
  10914. radioButton2.checked = true;
  10915. if (this.constants.physics.barnesHut.enabled) {
  10916. radioButton1.checked = true;
  10917. }
  10918. if (this.constants.hierarchicalLayout.enabled) {
  10919. radioButton3.checked = true;
  10920. }
  10921. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10922. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  10923. var graph_generateOptions = document.getElementById("graph_generateOptions");
  10924. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  10925. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  10926. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  10927. if (this.constants.smoothCurves == true) {
  10928. graph_toggleSmooth.style.background = "#A4FF56";
  10929. }
  10930. else {
  10931. graph_toggleSmooth.style.background = "#FF8532";
  10932. }
  10933. switchConfigurations.apply(this);
  10934. radioButton1.onchange = switchConfigurations.bind(this);
  10935. radioButton2.onchange = switchConfigurations.bind(this);
  10936. radioButton3.onchange = switchConfigurations.bind(this);
  10937. }
  10938. },
  10939. /**
  10940. * This overwrites the this.constants.
  10941. *
  10942. * @param constantsVariableName
  10943. * @param value
  10944. * @private
  10945. */
  10946. _overWriteGraphConstants: function (constantsVariableName, value) {
  10947. var nameArray = constantsVariableName.split("_");
  10948. if (nameArray.length == 1) {
  10949. this.constants[nameArray[0]] = value;
  10950. }
  10951. else if (nameArray.length == 2) {
  10952. this.constants[nameArray[0]][nameArray[1]] = value;
  10953. }
  10954. else if (nameArray.length == 3) {
  10955. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  10956. }
  10957. }
  10958. };
  10959. /**
  10960. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  10961. */
  10962. function graphToggleSmoothCurves () {
  10963. this.constants.smoothCurves = !this.constants.smoothCurves;
  10964. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10965. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10966. else {graph_toggleSmooth.style.background = "#FF8532";}
  10967. this._configureSmoothCurves(false);
  10968. };
  10969. /**
  10970. * this function is used to scramble the nodes
  10971. *
  10972. */
  10973. function graphRepositionNodes () {
  10974. for (var nodeId in this.calculationNodes) {
  10975. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  10976. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  10977. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  10978. }
  10979. }
  10980. if (this.constants.hierarchicalLayout.enabled == true) {
  10981. this._setupHierarchicalLayout();
  10982. }
  10983. else {
  10984. this.repositionNodes();
  10985. }
  10986. this.moving = true;
  10987. this.start();
  10988. };
  10989. /**
  10990. * this is used to generate an options file from the playing with physics system.
  10991. */
  10992. function graphGenerateOptions () {
  10993. var options = "No options are required, default values used.";
  10994. var optionsSpecific = [];
  10995. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10996. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10997. if (radioButton1.checked == true) {
  10998. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  10999. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  11000. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  11001. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  11002. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  11003. if (optionsSpecific.length != 0) {
  11004. options = "var options = {";
  11005. options += "physics: {barnesHut: {";
  11006. for (var i = 0; i < optionsSpecific.length; i++) {
  11007. options += optionsSpecific[i];
  11008. if (i < optionsSpecific.length - 1) {
  11009. options += ", "
  11010. }
  11011. }
  11012. options += '}}'
  11013. }
  11014. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  11015. if (optionsSpecific.length == 0) {options = "var options = {";}
  11016. else {options += ", "}
  11017. options += "smoothCurves: " + this.constants.smoothCurves;
  11018. }
  11019. if (options != "No options are required, default values used.") {
  11020. options += '};'
  11021. }
  11022. }
  11023. else if (radioButton2.checked == true) {
  11024. options = "var options = {";
  11025. options += "physics: {barnesHut: {enabled: false}";
  11026. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  11027. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  11028. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  11029. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  11030. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  11031. if (optionsSpecific.length != 0) {
  11032. options += ", repulsion: {";
  11033. for (var i = 0; i < optionsSpecific.length; i++) {
  11034. options += optionsSpecific[i];
  11035. if (i < optionsSpecific.length - 1) {
  11036. options += ", "
  11037. }
  11038. }
  11039. options += '}}'
  11040. }
  11041. if (optionsSpecific.length == 0) {options += "}"}
  11042. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  11043. options += ", smoothCurves: " + this.constants.smoothCurves;
  11044. }
  11045. options += '};'
  11046. }
  11047. else {
  11048. options = "var options = {";
  11049. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  11050. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  11051. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  11052. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  11053. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  11054. if (optionsSpecific.length != 0) {
  11055. options += "physics: {hierarchicalRepulsion: {";
  11056. for (var i = 0; i < optionsSpecific.length; i++) {
  11057. options += optionsSpecific[i];
  11058. if (i < optionsSpecific.length - 1) {
  11059. options += ", ";
  11060. }
  11061. }
  11062. options += '}},';
  11063. }
  11064. options += 'hierarchicalLayout: {';
  11065. optionsSpecific = [];
  11066. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  11067. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  11068. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  11069. if (optionsSpecific.length != 0) {
  11070. for (var i = 0; i < optionsSpecific.length; i++) {
  11071. options += optionsSpecific[i];
  11072. if (i < optionsSpecific.length - 1) {
  11073. options += ", "
  11074. }
  11075. }
  11076. options += '}'
  11077. }
  11078. else {
  11079. options += "enabled:true}";
  11080. }
  11081. options += '};'
  11082. }
  11083. this.optionsDiv.innerHTML = options;
  11084. };
  11085. /**
  11086. * this is used to switch between barnesHut, repulsion and hierarchical.
  11087. *
  11088. */
  11089. function switchConfigurations () {
  11090. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  11091. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  11092. var tableId = "graph_" + radioButton + "_table";
  11093. var table = document.getElementById(tableId);
  11094. table.style.display = "block";
  11095. for (var i = 0; i < ids.length; i++) {
  11096. if (ids[i] != tableId) {
  11097. table = document.getElementById(ids[i]);
  11098. table.style.display = "none";
  11099. }
  11100. }
  11101. this._restoreNodes();
  11102. if (radioButton == "R") {
  11103. this.constants.hierarchicalLayout.enabled = false;
  11104. this.constants.physics.hierarchicalRepulsion.enabled = false;
  11105. this.constants.physics.barnesHut.enabled = false;
  11106. }
  11107. else if (radioButton == "H") {
  11108. if (this.constants.hierarchicalLayout.enabled == false) {
  11109. this.constants.hierarchicalLayout.enabled = true;
  11110. this.constants.physics.hierarchicalRepulsion.enabled = true;
  11111. this.constants.physics.barnesHut.enabled = false;
  11112. this._setupHierarchicalLayout();
  11113. }
  11114. }
  11115. else {
  11116. this.constants.hierarchicalLayout.enabled = false;
  11117. this.constants.physics.hierarchicalRepulsion.enabled = false;
  11118. this.constants.physics.barnesHut.enabled = true;
  11119. }
  11120. this._loadSelectedForceSolver();
  11121. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  11122. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  11123. else {graph_toggleSmooth.style.background = "#FF8532";}
  11124. this.moving = true;
  11125. this.start();
  11126. }
  11127. /**
  11128. * this generates the ranges depending on the iniital values.
  11129. *
  11130. * @param id
  11131. * @param map
  11132. * @param constantsVariableName
  11133. */
  11134. function showValueOfRange (id,map,constantsVariableName) {
  11135. var valueId = id + "_value";
  11136. var rangeValue = document.getElementById(id).value;
  11137. if (map instanceof Array) {
  11138. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  11139. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  11140. }
  11141. else {
  11142. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  11143. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  11144. }
  11145. if (constantsVariableName == "hierarchicalLayout_direction" ||
  11146. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  11147. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  11148. this._setupHierarchicalLayout();
  11149. }
  11150. this.moving = true;
  11151. this.start();
  11152. };
  11153. /**
  11154. * Created by Alex on 2/10/14.
  11155. */
  11156. var hierarchalRepulsionMixin = {
  11157. /**
  11158. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  11159. * This field is linearly approximated.
  11160. *
  11161. * @private
  11162. */
  11163. _calculateNodeForces: function () {
  11164. var dx, dy, distance, fx, fy, combinedClusterSize,
  11165. repulsingForce, node1, node2, i, j;
  11166. var nodes = this.calculationNodes;
  11167. var nodeIndices = this.calculationNodeIndices;
  11168. // approximation constants
  11169. var b = 5;
  11170. var a_base = 0.5 * -b;
  11171. // repulsing forces between nodes
  11172. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  11173. var minimumDistance = nodeDistance;
  11174. // we loop from i over all but the last entree in the array
  11175. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  11176. for (i = 0; i < nodeIndices.length - 1; i++) {
  11177. node1 = nodes[nodeIndices[i]];
  11178. for (j = i + 1; j < nodeIndices.length; j++) {
  11179. node2 = nodes[nodeIndices[j]];
  11180. dx = node2.x - node1.x;
  11181. dy = node2.y - node1.y;
  11182. distance = Math.sqrt(dx * dx + dy * dy);
  11183. var a = a_base / minimumDistance;
  11184. if (distance < 2 * minimumDistance) {
  11185. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  11186. // normalize force with
  11187. if (distance == 0) {
  11188. distance = 0.01;
  11189. }
  11190. else {
  11191. repulsingForce = repulsingForce / distance;
  11192. }
  11193. fx = dx * repulsingForce;
  11194. fy = dy * repulsingForce;
  11195. node1.fx -= fx;
  11196. node1.fy -= fy;
  11197. node2.fx += fx;
  11198. node2.fy += fy;
  11199. }
  11200. }
  11201. }
  11202. }
  11203. };
  11204. /**
  11205. * Created by Alex on 2/10/14.
  11206. */
  11207. var barnesHutMixin = {
  11208. /**
  11209. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  11210. * The Barnes Hut method is used to speed up this N-body simulation.
  11211. *
  11212. * @private
  11213. */
  11214. _calculateNodeForces : function() {
  11215. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  11216. var node;
  11217. var nodes = this.calculationNodes;
  11218. var nodeIndices = this.calculationNodeIndices;
  11219. var nodeCount = nodeIndices.length;
  11220. this._formBarnesHutTree(nodes,nodeIndices);
  11221. var barnesHutTree = this.barnesHutTree;
  11222. // place the nodes one by one recursively
  11223. for (var i = 0; i < nodeCount; i++) {
  11224. node = nodes[nodeIndices[i]];
  11225. // starting with root is irrelevant, it never passes the BarnesHut condition
  11226. this._getForceContribution(barnesHutTree.root.children.NW,node);
  11227. this._getForceContribution(barnesHutTree.root.children.NE,node);
  11228. this._getForceContribution(barnesHutTree.root.children.SW,node);
  11229. this._getForceContribution(barnesHutTree.root.children.SE,node);
  11230. }
  11231. }
  11232. },
  11233. /**
  11234. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  11235. * If a region contains a single node, we check if it is not itself, then we apply the force.
  11236. *
  11237. * @param parentBranch
  11238. * @param node
  11239. * @private
  11240. */
  11241. _getForceContribution : function(parentBranch,node) {
  11242. // we get no force contribution from an empty region
  11243. if (parentBranch.childrenCount > 0) {
  11244. var dx,dy,distance;
  11245. // get the distance from the center of mass to the node.
  11246. dx = parentBranch.centerOfMass.x - node.x;
  11247. dy = parentBranch.centerOfMass.y - node.y;
  11248. distance = Math.sqrt(dx * dx + dy * dy);
  11249. // BarnesHut condition
  11250. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  11251. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  11252. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  11253. // duplicate code to reduce function calls to speed up program
  11254. if (distance == 0) {
  11255. distance = 0.1*Math.random();
  11256. dx = distance;
  11257. }
  11258. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  11259. var fx = dx * gravityForce;
  11260. var fy = dy * gravityForce;
  11261. node.fx += fx;
  11262. node.fy += fy;
  11263. }
  11264. else {
  11265. // Did not pass the condition, go into children if available
  11266. if (parentBranch.childrenCount == 4) {
  11267. this._getForceContribution(parentBranch.children.NW,node);
  11268. this._getForceContribution(parentBranch.children.NE,node);
  11269. this._getForceContribution(parentBranch.children.SW,node);
  11270. this._getForceContribution(parentBranch.children.SE,node);
  11271. }
  11272. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  11273. if (parentBranch.children.data.id != node.id) { // if it is not self
  11274. // duplicate code to reduce function calls to speed up program
  11275. if (distance == 0) {
  11276. distance = 0.5*Math.random();
  11277. dx = distance;
  11278. }
  11279. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  11280. var fx = dx * gravityForce;
  11281. var fy = dy * gravityForce;
  11282. node.fx += fx;
  11283. node.fy += fy;
  11284. }
  11285. }
  11286. }
  11287. }
  11288. },
  11289. /**
  11290. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  11291. *
  11292. * @param nodes
  11293. * @param nodeIndices
  11294. * @private
  11295. */
  11296. _formBarnesHutTree : function(nodes,nodeIndices) {
  11297. var node;
  11298. var nodeCount = nodeIndices.length;
  11299. var minX = Number.MAX_VALUE,
  11300. minY = Number.MAX_VALUE,
  11301. maxX =-Number.MAX_VALUE,
  11302. maxY =-Number.MAX_VALUE;
  11303. // get the range of the nodes
  11304. for (var i = 0; i < nodeCount; i++) {
  11305. var x = nodes[nodeIndices[i]].x;
  11306. var y = nodes[nodeIndices[i]].y;
  11307. if (x < minX) { minX = x; }
  11308. if (x > maxX) { maxX = x; }
  11309. if (y < minY) { minY = y; }
  11310. if (y > maxY) { maxY = y; }
  11311. }
  11312. // make the range a square
  11313. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  11314. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  11315. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  11316. var minimumTreeSize = 1e-5;
  11317. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  11318. var halfRootSize = 0.5 * rootSize;
  11319. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  11320. // construct the barnesHutTree
  11321. var barnesHutTree = {root:{
  11322. centerOfMass:{x:0,y:0}, // Center of Mass
  11323. mass:0,
  11324. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  11325. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  11326. size: rootSize,
  11327. calcSize: 1 / rootSize,
  11328. children: {data:null},
  11329. maxWidth: 0,
  11330. level: 0,
  11331. childrenCount: 4
  11332. }};
  11333. this._splitBranch(barnesHutTree.root);
  11334. // place the nodes one by one recursively
  11335. for (i = 0; i < nodeCount; i++) {
  11336. node = nodes[nodeIndices[i]];
  11337. this._placeInTree(barnesHutTree.root,node);
  11338. }
  11339. // make global
  11340. this.barnesHutTree = barnesHutTree
  11341. },
  11342. /**
  11343. * this updates the mass of a branch. this is increased by adding a node.
  11344. *
  11345. * @param parentBranch
  11346. * @param node
  11347. * @private
  11348. */
  11349. _updateBranchMass : function(parentBranch, node) {
  11350. var totalMass = parentBranch.mass + node.mass;
  11351. var totalMassInv = 1/totalMass;
  11352. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  11353. parentBranch.centerOfMass.x *= totalMassInv;
  11354. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  11355. parentBranch.centerOfMass.y *= totalMassInv;
  11356. parentBranch.mass = totalMass;
  11357. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  11358. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  11359. },
  11360. /**
  11361. * determine in which branch the node will be placed.
  11362. *
  11363. * @param parentBranch
  11364. * @param node
  11365. * @param skipMassUpdate
  11366. * @private
  11367. */
  11368. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  11369. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  11370. // update the mass of the branch.
  11371. this._updateBranchMass(parentBranch,node);
  11372. }
  11373. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  11374. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  11375. this._placeInRegion(parentBranch,node,"NW");
  11376. }
  11377. else { // in SW
  11378. this._placeInRegion(parentBranch,node,"SW");
  11379. }
  11380. }
  11381. else { // in NE or SE
  11382. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  11383. this._placeInRegion(parentBranch,node,"NE");
  11384. }
  11385. else { // in SE
  11386. this._placeInRegion(parentBranch,node,"SE");
  11387. }
  11388. }
  11389. },
  11390. /**
  11391. * actually place the node in a region (or branch)
  11392. *
  11393. * @param parentBranch
  11394. * @param node
  11395. * @param region
  11396. * @private
  11397. */
  11398. _placeInRegion : function(parentBranch,node,region) {
  11399. switch (parentBranch.children[region].childrenCount) {
  11400. case 0: // place node here
  11401. parentBranch.children[region].children.data = node;
  11402. parentBranch.children[region].childrenCount = 1;
  11403. this._updateBranchMass(parentBranch.children[region],node);
  11404. break;
  11405. case 1: // convert into children
  11406. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  11407. // we move one node a pixel and we do not put it in the tree.
  11408. if (parentBranch.children[region].children.data.x == node.x &&
  11409. parentBranch.children[region].children.data.y == node.y) {
  11410. node.x += Math.random();
  11411. node.y += Math.random();
  11412. }
  11413. else {
  11414. this._splitBranch(parentBranch.children[region]);
  11415. this._placeInTree(parentBranch.children[region],node);
  11416. }
  11417. break;
  11418. case 4: // place in branch
  11419. this._placeInTree(parentBranch.children[region],node);
  11420. break;
  11421. }
  11422. },
  11423. /**
  11424. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  11425. * after the split is complete.
  11426. *
  11427. * @param parentBranch
  11428. * @private
  11429. */
  11430. _splitBranch : function(parentBranch) {
  11431. // if the branch is filled with a node, replace the node in the new subset.
  11432. var containedNode = null;
  11433. if (parentBranch.childrenCount == 1) {
  11434. containedNode = parentBranch.children.data;
  11435. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  11436. }
  11437. parentBranch.childrenCount = 4;
  11438. parentBranch.children.data = null;
  11439. this._insertRegion(parentBranch,"NW");
  11440. this._insertRegion(parentBranch,"NE");
  11441. this._insertRegion(parentBranch,"SW");
  11442. this._insertRegion(parentBranch,"SE");
  11443. if (containedNode != null) {
  11444. this._placeInTree(parentBranch,containedNode);
  11445. }
  11446. },
  11447. /**
  11448. * This function subdivides the region into four new segments.
  11449. * Specifically, this inserts a single new segment.
  11450. * It fills the children section of the parentBranch
  11451. *
  11452. * @param parentBranch
  11453. * @param region
  11454. * @param parentRange
  11455. * @private
  11456. */
  11457. _insertRegion : function(parentBranch, region) {
  11458. var minX,maxX,minY,maxY;
  11459. var childSize = 0.5 * parentBranch.size;
  11460. switch (region) {
  11461. case "NW":
  11462. minX = parentBranch.range.minX;
  11463. maxX = parentBranch.range.minX + childSize;
  11464. minY = parentBranch.range.minY;
  11465. maxY = parentBranch.range.minY + childSize;
  11466. break;
  11467. case "NE":
  11468. minX = parentBranch.range.minX + childSize;
  11469. maxX = parentBranch.range.maxX;
  11470. minY = parentBranch.range.minY;
  11471. maxY = parentBranch.range.minY + childSize;
  11472. break;
  11473. case "SW":
  11474. minX = parentBranch.range.minX;
  11475. maxX = parentBranch.range.minX + childSize;
  11476. minY = parentBranch.range.minY + childSize;
  11477. maxY = parentBranch.range.maxY;
  11478. break;
  11479. case "SE":
  11480. minX = parentBranch.range.minX + childSize;
  11481. maxX = parentBranch.range.maxX;
  11482. minY = parentBranch.range.minY + childSize;
  11483. maxY = parentBranch.range.maxY;
  11484. break;
  11485. }
  11486. parentBranch.children[region] = {
  11487. centerOfMass:{x:0,y:0},
  11488. mass:0,
  11489. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  11490. size: 0.5 * parentBranch.size,
  11491. calcSize: 2 * parentBranch.calcSize,
  11492. children: {data:null},
  11493. maxWidth: 0,
  11494. level: parentBranch.level+1,
  11495. childrenCount: 0
  11496. };
  11497. },
  11498. /**
  11499. * This function is for debugging purposed, it draws the tree.
  11500. *
  11501. * @param ctx
  11502. * @param color
  11503. * @private
  11504. */
  11505. _drawTree : function(ctx,color) {
  11506. if (this.barnesHutTree !== undefined) {
  11507. ctx.lineWidth = 1;
  11508. this._drawBranch(this.barnesHutTree.root,ctx,color);
  11509. }
  11510. },
  11511. /**
  11512. * This function is for debugging purposes. It draws the branches recursively.
  11513. *
  11514. * @param branch
  11515. * @param ctx
  11516. * @param color
  11517. * @private
  11518. */
  11519. _drawBranch : function(branch,ctx,color) {
  11520. if (color === undefined) {
  11521. color = "#FF0000";
  11522. }
  11523. if (branch.childrenCount == 4) {
  11524. this._drawBranch(branch.children.NW,ctx);
  11525. this._drawBranch(branch.children.NE,ctx);
  11526. this._drawBranch(branch.children.SE,ctx);
  11527. this._drawBranch(branch.children.SW,ctx);
  11528. }
  11529. ctx.strokeStyle = color;
  11530. ctx.beginPath();
  11531. ctx.moveTo(branch.range.minX,branch.range.minY);
  11532. ctx.lineTo(branch.range.maxX,branch.range.minY);
  11533. ctx.stroke();
  11534. ctx.beginPath();
  11535. ctx.moveTo(branch.range.maxX,branch.range.minY);
  11536. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  11537. ctx.stroke();
  11538. ctx.beginPath();
  11539. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  11540. ctx.lineTo(branch.range.minX,branch.range.maxY);
  11541. ctx.stroke();
  11542. ctx.beginPath();
  11543. ctx.moveTo(branch.range.minX,branch.range.maxY);
  11544. ctx.lineTo(branch.range.minX,branch.range.minY);
  11545. ctx.stroke();
  11546. /*
  11547. if (branch.mass > 0) {
  11548. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  11549. ctx.stroke();
  11550. }
  11551. */
  11552. }
  11553. };
  11554. /**
  11555. * Created by Alex on 2/10/14.
  11556. */
  11557. var repulsionMixin = {
  11558. /**
  11559. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  11560. * This field is linearly approximated.
  11561. *
  11562. * @private
  11563. */
  11564. _calculateNodeForces: function () {
  11565. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  11566. repulsingForce, node1, node2, i, j;
  11567. var nodes = this.calculationNodes;
  11568. var nodeIndices = this.calculationNodeIndices;
  11569. // approximation constants
  11570. var a_base = -2 / 3;
  11571. var b = 4 / 3;
  11572. // repulsing forces between nodes
  11573. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  11574. var minimumDistance = nodeDistance;
  11575. // we loop from i over all but the last entree in the array
  11576. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  11577. for (i = 0; i < nodeIndices.length - 1; i++) {
  11578. node1 = nodes[nodeIndices[i]];
  11579. for (j = i + 1; j < nodeIndices.length; j++) {
  11580. node2 = nodes[nodeIndices[j]];
  11581. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  11582. dx = node2.x - node1.x;
  11583. dy = node2.y - node1.y;
  11584. distance = Math.sqrt(dx * dx + dy * dy);
  11585. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  11586. var a = a_base / minimumDistance;
  11587. if (distance < 2 * minimumDistance) {
  11588. if (distance < 0.5 * minimumDistance) {
  11589. repulsingForce = 1.0;
  11590. }
  11591. else {
  11592. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  11593. }
  11594. // amplify the repulsion for clusters.
  11595. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  11596. repulsingForce = repulsingForce / distance;
  11597. fx = dx * repulsingForce;
  11598. fy = dy * repulsingForce;
  11599. node1.fx -= fx;
  11600. node1.fy -= fy;
  11601. node2.fx += fx;
  11602. node2.fy += fy;
  11603. }
  11604. }
  11605. }
  11606. }
  11607. };
  11608. var HierarchicalLayoutMixin = {
  11609. _resetLevels : function() {
  11610. for (var nodeId in this.nodes) {
  11611. if (this.nodes.hasOwnProperty(nodeId)) {
  11612. var node = this.nodes[nodeId];
  11613. if (node.preassignedLevel == false) {
  11614. node.level = -1;
  11615. }
  11616. }
  11617. }
  11618. },
  11619. /**
  11620. * This is the main function to layout the nodes in a hierarchical way.
  11621. * It checks if the node details are supplied correctly
  11622. *
  11623. * @private
  11624. */
  11625. _setupHierarchicalLayout : function() {
  11626. if (this.constants.hierarchicalLayout.enabled == true) {
  11627. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  11628. this.constants.hierarchicalLayout.levelSeparation *= -1;
  11629. }
  11630. else {
  11631. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  11632. }
  11633. // get the size of the largest hubs and check if the user has defined a level for a node.
  11634. var hubsize = 0;
  11635. var node, nodeId;
  11636. var definedLevel = false;
  11637. var undefinedLevel = false;
  11638. for (nodeId in this.nodes) {
  11639. if (this.nodes.hasOwnProperty(nodeId)) {
  11640. node = this.nodes[nodeId];
  11641. if (node.level != -1) {
  11642. definedLevel = true;
  11643. }
  11644. else {
  11645. undefinedLevel = true;
  11646. }
  11647. if (hubsize < node.edges.length) {
  11648. hubsize = node.edges.length;
  11649. }
  11650. }
  11651. }
  11652. // if the user defined some levels but not all, alert and run without hierarchical layout
  11653. if (undefinedLevel == true && definedLevel == true) {
  11654. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  11655. this.zoomExtent(true,this.constants.clustering.enabled);
  11656. if (!this.constants.clustering.enabled) {
  11657. this.start();
  11658. }
  11659. }
  11660. else {
  11661. // setup the system to use hierarchical method.
  11662. this._changeConstants();
  11663. // define levels if undefined by the users. Based on hubsize
  11664. if (undefinedLevel == true) {
  11665. this._determineLevels(hubsize);
  11666. }
  11667. // check the distribution of the nodes per level.
  11668. var distribution = this._getDistribution();
  11669. // place the nodes on the canvas. This also stablilizes the system.
  11670. this._placeNodesByHierarchy(distribution);
  11671. // start the simulation.
  11672. this.start();
  11673. }
  11674. }
  11675. },
  11676. /**
  11677. * This function places the nodes on the canvas based on the hierarchial distribution.
  11678. *
  11679. * @param {Object} distribution | obtained by the function this._getDistribution()
  11680. * @private
  11681. */
  11682. _placeNodesByHierarchy : function(distribution) {
  11683. var nodeId, node;
  11684. // start placing all the level 0 nodes first. Then recursively position their branches.
  11685. for (nodeId in distribution[0].nodes) {
  11686. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  11687. node = distribution[0].nodes[nodeId];
  11688. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11689. if (node.xFixed) {
  11690. node.x = distribution[0].minPos;
  11691. node.xFixed = false;
  11692. distribution[0].minPos += distribution[0].nodeSpacing;
  11693. }
  11694. }
  11695. else {
  11696. if (node.yFixed) {
  11697. node.y = distribution[0].minPos;
  11698. node.yFixed = false;
  11699. distribution[0].minPos += distribution[0].nodeSpacing;
  11700. }
  11701. }
  11702. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  11703. }
  11704. }
  11705. // stabilize the system after positioning. This function calls zoomExtent.
  11706. this._stabilize();
  11707. },
  11708. /**
  11709. * This function get the distribution of levels based on hubsize
  11710. *
  11711. * @returns {Object}
  11712. * @private
  11713. */
  11714. _getDistribution : function() {
  11715. var distribution = {};
  11716. var nodeId, node, level;
  11717. // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
  11718. // the fix of X is removed after the x value has been set.
  11719. for (nodeId in this.nodes) {
  11720. if (this.nodes.hasOwnProperty(nodeId)) {
  11721. node = this.nodes[nodeId];
  11722. node.xFixed = true;
  11723. node.yFixed = true;
  11724. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11725. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11726. }
  11727. else {
  11728. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11729. }
  11730. if (!distribution.hasOwnProperty(node.level)) {
  11731. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  11732. }
  11733. distribution[node.level].amount += 1;
  11734. distribution[node.level].nodes[node.id] = node;
  11735. }
  11736. }
  11737. // determine the largest amount of nodes of all levels
  11738. var maxCount = 0;
  11739. for (level in distribution) {
  11740. if (distribution.hasOwnProperty(level)) {
  11741. if (maxCount < distribution[level].amount) {
  11742. maxCount = distribution[level].amount;
  11743. }
  11744. }
  11745. }
  11746. // set the initial position and spacing of each nodes accordingly
  11747. for (level in distribution) {
  11748. if (distribution.hasOwnProperty(level)) {
  11749. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  11750. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  11751. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  11752. }
  11753. }
  11754. return distribution;
  11755. },
  11756. /**
  11757. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  11758. *
  11759. * @param hubsize
  11760. * @private
  11761. */
  11762. _determineLevels : function(hubsize) {
  11763. var nodeId, node;
  11764. // determine hubs
  11765. for (nodeId in this.nodes) {
  11766. if (this.nodes.hasOwnProperty(nodeId)) {
  11767. node = this.nodes[nodeId];
  11768. if (node.edges.length == hubsize) {
  11769. node.level = 0;
  11770. }
  11771. }
  11772. }
  11773. // branch from hubs
  11774. for (nodeId in this.nodes) {
  11775. if (this.nodes.hasOwnProperty(nodeId)) {
  11776. node = this.nodes[nodeId];
  11777. if (node.level == 0) {
  11778. this._setLevel(1,node.edges,node.id);
  11779. }
  11780. }
  11781. }
  11782. },
  11783. /**
  11784. * Since hierarchical layout does not support:
  11785. * - smooth curves (based on the physics),
  11786. * - clustering (based on dynamic node counts)
  11787. *
  11788. * We disable both features so there will be no problems.
  11789. *
  11790. * @private
  11791. */
  11792. _changeConstants : function() {
  11793. this.constants.clustering.enabled = false;
  11794. this.constants.physics.barnesHut.enabled = false;
  11795. this.constants.physics.hierarchicalRepulsion.enabled = true;
  11796. this._loadSelectedForceSolver();
  11797. this.constants.smoothCurves = false;
  11798. this._configureSmoothCurves();
  11799. },
  11800. /**
  11801. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  11802. * on a X position that ensures there will be no overlap.
  11803. *
  11804. * @param edges
  11805. * @param parentId
  11806. * @param distribution
  11807. * @param parentLevel
  11808. * @private
  11809. */
  11810. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  11811. for (var i = 0; i < edges.length; i++) {
  11812. var childNode = null;
  11813. if (edges[i].toId == parentId) {
  11814. childNode = edges[i].from;
  11815. }
  11816. else {
  11817. childNode = edges[i].to;
  11818. }
  11819. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  11820. var nodeMoved = false;
  11821. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11822. if (childNode.xFixed && childNode.level > parentLevel) {
  11823. childNode.xFixed = false;
  11824. childNode.x = distribution[childNode.level].minPos;
  11825. nodeMoved = true;
  11826. }
  11827. }
  11828. else {
  11829. if (childNode.yFixed && childNode.level > parentLevel) {
  11830. childNode.yFixed = false;
  11831. childNode.y = distribution[childNode.level].minPos;
  11832. nodeMoved = true;
  11833. }
  11834. }
  11835. if (nodeMoved == true) {
  11836. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  11837. if (childNode.edges.length > 1) {
  11838. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  11839. }
  11840. }
  11841. }
  11842. },
  11843. /**
  11844. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  11845. *
  11846. * @param level
  11847. * @param edges
  11848. * @param parentId
  11849. * @private
  11850. */
  11851. _setLevel : function(level, edges, parentId) {
  11852. for (var i = 0; i < edges.length; i++) {
  11853. var childNode = null;
  11854. if (edges[i].toId == parentId) {
  11855. childNode = edges[i].from;
  11856. }
  11857. else {
  11858. childNode = edges[i].to;
  11859. }
  11860. if (childNode.level == -1 || childNode.level > level) {
  11861. childNode.level = level;
  11862. if (edges.length > 1) {
  11863. this._setLevel(level+1, childNode.edges, childNode.id);
  11864. }
  11865. }
  11866. }
  11867. },
  11868. /**
  11869. * Unfix nodes
  11870. *
  11871. * @private
  11872. */
  11873. _restoreNodes : function() {
  11874. for (nodeId in this.nodes) {
  11875. if (this.nodes.hasOwnProperty(nodeId)) {
  11876. this.nodes[nodeId].xFixed = false;
  11877. this.nodes[nodeId].yFixed = false;
  11878. }
  11879. }
  11880. }
  11881. };
  11882. /**
  11883. * Created by Alex on 2/4/14.
  11884. */
  11885. var manipulationMixin = {
  11886. /**
  11887. * clears the toolbar div element of children
  11888. *
  11889. * @private
  11890. */
  11891. _clearManipulatorBar : function() {
  11892. while (this.manipulationDiv.hasChildNodes()) {
  11893. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11894. }
  11895. },
  11896. /**
  11897. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  11898. * these functions to their original functionality, we saved them in this.cachedFunctions.
  11899. * This function restores these functions to their original function.
  11900. *
  11901. * @private
  11902. */
  11903. _restoreOverloadedFunctions : function() {
  11904. for (var functionName in this.cachedFunctions) {
  11905. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  11906. this[functionName] = this.cachedFunctions[functionName];
  11907. }
  11908. }
  11909. },
  11910. /**
  11911. * Enable or disable edit-mode.
  11912. *
  11913. * @private
  11914. */
  11915. _toggleEditMode : function() {
  11916. this.editMode = !this.editMode;
  11917. var toolbar = document.getElementById("graph-manipulationDiv");
  11918. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11919. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  11920. if (this.editMode == true) {
  11921. toolbar.style.display="block";
  11922. closeDiv.style.display="block";
  11923. editModeDiv.style.display="none";
  11924. closeDiv.onclick = this._toggleEditMode.bind(this);
  11925. }
  11926. else {
  11927. toolbar.style.display="none";
  11928. closeDiv.style.display="none";
  11929. editModeDiv.style.display="block";
  11930. closeDiv.onclick = null;
  11931. }
  11932. this._createManipulatorBar()
  11933. },
  11934. /**
  11935. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  11936. *
  11937. * @private
  11938. */
  11939. _createManipulatorBar : function() {
  11940. // remove bound functions
  11941. if (this.boundFunction) {
  11942. this.off('select', this.boundFunction);
  11943. }
  11944. // restore overloaded functions
  11945. this._restoreOverloadedFunctions();
  11946. // resume calculation
  11947. this.freezeSimulation = false;
  11948. // reset global variables
  11949. this.blockConnectingEdgeSelection = false;
  11950. this.forceAppendSelection = false;
  11951. if (this.editMode == true) {
  11952. while (this.manipulationDiv.hasChildNodes()) {
  11953. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11954. }
  11955. // add the icons to the manipulator div
  11956. this.manipulationDiv.innerHTML = "" +
  11957. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  11958. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  11959. "<div class='graph-seperatorLine'></div>" +
  11960. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  11961. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  11962. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11963. this.manipulationDiv.innerHTML += "" +
  11964. "<div class='graph-seperatorLine'></div>" +
  11965. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  11966. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  11967. }
  11968. if (this._selectionIsEmpty() == false) {
  11969. this.manipulationDiv.innerHTML += "" +
  11970. "<div class='graph-seperatorLine'></div>" +
  11971. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  11972. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  11973. }
  11974. // bind the icons
  11975. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  11976. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  11977. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  11978. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  11979. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11980. var editButton = document.getElementById("graph-manipulate-editNode");
  11981. editButton.onclick = this._editNode.bind(this);
  11982. }
  11983. if (this._selectionIsEmpty() == false) {
  11984. var deleteButton = document.getElementById("graph-manipulate-delete");
  11985. deleteButton.onclick = this._deleteSelected.bind(this);
  11986. }
  11987. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11988. closeDiv.onclick = this._toggleEditMode.bind(this);
  11989. this.boundFunction = this._createManipulatorBar.bind(this);
  11990. this.on('select', this.boundFunction);
  11991. }
  11992. else {
  11993. this.editModeDiv.innerHTML = "" +
  11994. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  11995. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  11996. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  11997. editModeButton.onclick = this._toggleEditMode.bind(this);
  11998. }
  11999. },
  12000. /**
  12001. * Create the toolbar for adding Nodes
  12002. *
  12003. * @private
  12004. */
  12005. _createAddNodeToolbar : function() {
  12006. // clear the toolbar
  12007. this._clearManipulatorBar();
  12008. if (this.boundFunction) {
  12009. this.off('select', this.boundFunction);
  12010. }
  12011. // create the toolbar contents
  12012. this.manipulationDiv.innerHTML = "" +
  12013. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  12014. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  12015. "<div class='graph-seperatorLine'></div>" +
  12016. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  12017. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  12018. // bind the icon
  12019. var backButton = document.getElementById("graph-manipulate-back");
  12020. backButton.onclick = this._createManipulatorBar.bind(this);
  12021. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  12022. this.boundFunction = this._addNode.bind(this);
  12023. this.on('select', this.boundFunction);
  12024. },
  12025. /**
  12026. * create the toolbar to connect nodes
  12027. *
  12028. * @private
  12029. */
  12030. _createAddEdgeToolbar : function() {
  12031. // clear the toolbar
  12032. this._clearManipulatorBar();
  12033. this._unselectAll(true);
  12034. this.freezeSimulation = true;
  12035. if (this.boundFunction) {
  12036. this.off('select', this.boundFunction);
  12037. }
  12038. this._unselectAll();
  12039. this.forceAppendSelection = false;
  12040. this.blockConnectingEdgeSelection = true;
  12041. this.manipulationDiv.innerHTML = "" +
  12042. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  12043. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  12044. "<div class='graph-seperatorLine'></div>" +
  12045. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  12046. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  12047. // bind the icon
  12048. var backButton = document.getElementById("graph-manipulate-back");
  12049. backButton.onclick = this._createManipulatorBar.bind(this);
  12050. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  12051. this.boundFunction = this._handleConnect.bind(this);
  12052. this.on('select', this.boundFunction);
  12053. // temporarily overload functions
  12054. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  12055. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  12056. this._handleTouch = this._handleConnect;
  12057. this._handleOnRelease = this._finishConnect;
  12058. // redraw to show the unselect
  12059. this._redraw();
  12060. },
  12061. /**
  12062. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  12063. * to walk the user through the process.
  12064. *
  12065. * @private
  12066. */
  12067. _handleConnect : function(pointer) {
  12068. if (this._getSelectedNodeCount() == 0) {
  12069. var node = this._getNodeAt(pointer);
  12070. if (node != null) {
  12071. if (node.clusterSize > 1) {
  12072. alert("Cannot create edges to a cluster.")
  12073. }
  12074. else {
  12075. this._selectObject(node,false);
  12076. // create a node the temporary line can look at
  12077. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  12078. this.sectors['support']['nodes']['targetNode'].x = node.x;
  12079. this.sectors['support']['nodes']['targetNode'].y = node.y;
  12080. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  12081. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  12082. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  12083. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  12084. // create a temporary edge
  12085. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  12086. this.edges['connectionEdge'].from = node;
  12087. this.edges['connectionEdge'].connected = true;
  12088. this.edges['connectionEdge'].smooth = true;
  12089. this.edges['connectionEdge'].selected = true;
  12090. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  12091. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  12092. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  12093. this._handleOnDrag = function(event) {
  12094. var pointer = this._getPointer(event.gesture.center);
  12095. this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  12096. this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  12097. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  12098. this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  12099. };
  12100. this.moving = true;
  12101. this.start();
  12102. }
  12103. }
  12104. }
  12105. },
  12106. _finishConnect : function(pointer) {
  12107. if (this._getSelectedNodeCount() == 1) {
  12108. // restore the drag function
  12109. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  12110. delete this.cachedFunctions["_handleOnDrag"];
  12111. // remember the edge id
  12112. var connectFromId = this.edges['connectionEdge'].fromId;
  12113. // remove the temporary nodes and edge
  12114. delete this.edges['connectionEdge'];
  12115. delete this.sectors['support']['nodes']['targetNode'];
  12116. delete this.sectors['support']['nodes']['targetViaNode'];
  12117. var node = this._getNodeAt(pointer);
  12118. if (node != null) {
  12119. if (node.clusterSize > 1) {
  12120. alert("Cannot create edges to a cluster.")
  12121. }
  12122. else {
  12123. this._createEdge(connectFromId,node.id);
  12124. this._createManipulatorBar();
  12125. }
  12126. }
  12127. this._unselectAll();
  12128. }
  12129. },
  12130. /**
  12131. * Adds a node on the specified location
  12132. */
  12133. _addNode : function() {
  12134. if (this._selectionIsEmpty() && this.editMode == true) {
  12135. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  12136. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  12137. if (this.triggerFunctions.add) {
  12138. if (this.triggerFunctions.add.length == 2) {
  12139. var me = this;
  12140. this.triggerFunctions.add(defaultData, function(finalizedData) {
  12141. me.nodesData.add(finalizedData);
  12142. me._createManipulatorBar();
  12143. me.moving = true;
  12144. me.start();
  12145. });
  12146. }
  12147. else {
  12148. alert(this.constants.labels['addError']);
  12149. this._createManipulatorBar();
  12150. this.moving = true;
  12151. this.start();
  12152. }
  12153. }
  12154. else {
  12155. this.nodesData.add(defaultData);
  12156. this._createManipulatorBar();
  12157. this.moving = true;
  12158. this.start();
  12159. }
  12160. }
  12161. },
  12162. /**
  12163. * connect two nodes with a new edge.
  12164. *
  12165. * @private
  12166. */
  12167. _createEdge : function(sourceNodeId,targetNodeId) {
  12168. if (this.editMode == true) {
  12169. var defaultData = {from:sourceNodeId, to:targetNodeId};
  12170. if (this.triggerFunctions.connect) {
  12171. if (this.triggerFunctions.connect.length == 2) {
  12172. var me = this;
  12173. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  12174. me.edgesData.add(finalizedData);
  12175. me.moving = true;
  12176. me.start();
  12177. });
  12178. }
  12179. else {
  12180. alert(this.constants.labels["linkError"]);
  12181. this.moving = true;
  12182. this.start();
  12183. }
  12184. }
  12185. else {
  12186. this.edgesData.add(defaultData);
  12187. this.moving = true;
  12188. this.start();
  12189. }
  12190. }
  12191. },
  12192. /**
  12193. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  12194. *
  12195. * @private
  12196. */
  12197. _editNode : function() {
  12198. if (this.triggerFunctions.edit && this.editMode == true) {
  12199. var node = this._getSelectedNode();
  12200. var data = {id:node.id,
  12201. label: node.label,
  12202. group: node.group,
  12203. shape: node.shape,
  12204. color: {
  12205. background:node.color.background,
  12206. border:node.color.border,
  12207. highlight: {
  12208. background:node.color.highlight.background,
  12209. border:node.color.highlight.border
  12210. }
  12211. }};
  12212. if (this.triggerFunctions.edit.length == 2) {
  12213. var me = this;
  12214. this.triggerFunctions.edit(data, function (finalizedData) {
  12215. me.nodesData.update(finalizedData);
  12216. me._createManipulatorBar();
  12217. me.moving = true;
  12218. me.start();
  12219. });
  12220. }
  12221. else {
  12222. alert(this.constants.labels["editError"]);
  12223. }
  12224. }
  12225. else {
  12226. alert(this.constants.labels["editBoundError"]);
  12227. }
  12228. },
  12229. /**
  12230. * delete everything in the selection
  12231. *
  12232. * @private
  12233. */
  12234. _deleteSelected : function() {
  12235. if (!this._selectionIsEmpty() && this.editMode == true) {
  12236. if (!this._clusterInSelection()) {
  12237. var selectedNodes = this.getSelectedNodes();
  12238. var selectedEdges = this.getSelectedEdges();
  12239. if (this.triggerFunctions.del) {
  12240. var me = this;
  12241. var data = {nodes: selectedNodes, edges: selectedEdges};
  12242. if (this.triggerFunctions.del.length = 2) {
  12243. this.triggerFunctions.del(data, function (finalizedData) {
  12244. me.edgesData.remove(finalizedData.edges);
  12245. me.nodesData.remove(finalizedData.nodes);
  12246. me._unselectAll();
  12247. me.moving = true;
  12248. me.start();
  12249. });
  12250. }
  12251. else {
  12252. alert(this.constants.labels["deleteError"])
  12253. }
  12254. }
  12255. else {
  12256. this.edgesData.remove(selectedEdges);
  12257. this.nodesData.remove(selectedNodes);
  12258. this._unselectAll();
  12259. this.moving = true;
  12260. this.start();
  12261. }
  12262. }
  12263. else {
  12264. alert(this.constants.labels["deleteClusterError"]);
  12265. }
  12266. }
  12267. }
  12268. };
  12269. /**
  12270. * Creation of the SectorMixin var.
  12271. *
  12272. * This contains all the functions the Graph object can use to employ the sector system.
  12273. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  12274. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  12275. *
  12276. * Alex de Mulder
  12277. * 21-01-2013
  12278. */
  12279. var SectorMixin = {
  12280. /**
  12281. * This function is only called by the setData function of the Graph object.
  12282. * This loads the global references into the active sector. This initializes the sector.
  12283. *
  12284. * @private
  12285. */
  12286. _putDataInSector : function() {
  12287. this.sectors["active"][this._sector()].nodes = this.nodes;
  12288. this.sectors["active"][this._sector()].edges = this.edges;
  12289. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  12290. },
  12291. /**
  12292. * /**
  12293. * This function sets the global references to nodes, edges and nodeIndices back to
  12294. * those of the supplied (active) sector. If a type is defined, do the specific type
  12295. *
  12296. * @param {String} sectorId
  12297. * @param {String} [sectorType] | "active" or "frozen"
  12298. * @private
  12299. */
  12300. _switchToSector : function(sectorId, sectorType) {
  12301. if (sectorType === undefined || sectorType == "active") {
  12302. this._switchToActiveSector(sectorId);
  12303. }
  12304. else {
  12305. this._switchToFrozenSector(sectorId);
  12306. }
  12307. },
  12308. /**
  12309. * This function sets the global references to nodes, edges and nodeIndices back to
  12310. * those of the supplied active sector.
  12311. *
  12312. * @param sectorId
  12313. * @private
  12314. */
  12315. _switchToActiveSector : function(sectorId) {
  12316. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  12317. this.nodes = this.sectors["active"][sectorId]["nodes"];
  12318. this.edges = this.sectors["active"][sectorId]["edges"];
  12319. },
  12320. /**
  12321. * This function sets the global references to nodes, edges and nodeIndices back to
  12322. * those of the supplied active sector.
  12323. *
  12324. * @param sectorId
  12325. * @private
  12326. */
  12327. _switchToSupportSector : function() {
  12328. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  12329. this.nodes = this.sectors["support"]["nodes"];
  12330. this.edges = this.sectors["support"]["edges"];
  12331. },
  12332. /**
  12333. * This function sets the global references to nodes, edges and nodeIndices back to
  12334. * those of the supplied frozen sector.
  12335. *
  12336. * @param sectorId
  12337. * @private
  12338. */
  12339. _switchToFrozenSector : function(sectorId) {
  12340. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  12341. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  12342. this.edges = this.sectors["frozen"][sectorId]["edges"];
  12343. },
  12344. /**
  12345. * This function sets the global references to nodes, edges and nodeIndices back to
  12346. * those of the currently active sector.
  12347. *
  12348. * @private
  12349. */
  12350. _loadLatestSector : function() {
  12351. this._switchToSector(this._sector());
  12352. },
  12353. /**
  12354. * This function returns the currently active sector Id
  12355. *
  12356. * @returns {String}
  12357. * @private
  12358. */
  12359. _sector : function() {
  12360. return this.activeSector[this.activeSector.length-1];
  12361. },
  12362. /**
  12363. * This function returns the previously active sector Id
  12364. *
  12365. * @returns {String}
  12366. * @private
  12367. */
  12368. _previousSector : function() {
  12369. if (this.activeSector.length > 1) {
  12370. return this.activeSector[this.activeSector.length-2];
  12371. }
  12372. else {
  12373. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  12374. }
  12375. },
  12376. /**
  12377. * We add the active sector at the end of the this.activeSector array
  12378. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  12379. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  12380. *
  12381. * @param newId
  12382. * @private
  12383. */
  12384. _setActiveSector : function(newId) {
  12385. this.activeSector.push(newId);
  12386. },
  12387. /**
  12388. * We remove the currently active sector id from the active sector stack. This happens when
  12389. * we reactivate the previously active sector
  12390. *
  12391. * @private
  12392. */
  12393. _forgetLastSector : function() {
  12394. this.activeSector.pop();
  12395. },
  12396. /**
  12397. * This function creates a new active sector with the supplied newId. This newId
  12398. * is the expanding node id.
  12399. *
  12400. * @param {String} newId | Id of the new active sector
  12401. * @private
  12402. */
  12403. _createNewSector : function(newId) {
  12404. // create the new sector
  12405. this.sectors["active"][newId] = {"nodes":{},
  12406. "edges":{},
  12407. "nodeIndices":[],
  12408. "formationScale": this.scale,
  12409. "drawingNode": undefined};
  12410. // create the new sector render node. This gives visual feedback that you are in a new sector.
  12411. this.sectors["active"][newId]['drawingNode'] = new Node(
  12412. {id:newId,
  12413. color: {
  12414. background: "#eaefef",
  12415. border: "495c5e"
  12416. }
  12417. },{},{},this.constants);
  12418. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  12419. },
  12420. /**
  12421. * This function removes the currently active sector. This is called when we create a new
  12422. * active sector.
  12423. *
  12424. * @param {String} sectorId | Id of the active sector that will be removed
  12425. * @private
  12426. */
  12427. _deleteActiveSector : function(sectorId) {
  12428. delete this.sectors["active"][sectorId];
  12429. },
  12430. /**
  12431. * This function removes the currently active sector. This is called when we reactivate
  12432. * the previously active sector.
  12433. *
  12434. * @param {String} sectorId | Id of the active sector that will be removed
  12435. * @private
  12436. */
  12437. _deleteFrozenSector : function(sectorId) {
  12438. delete this.sectors["frozen"][sectorId];
  12439. },
  12440. /**
  12441. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  12442. * We copy the references, then delete the active entree.
  12443. *
  12444. * @param sectorId
  12445. * @private
  12446. */
  12447. _freezeSector : function(sectorId) {
  12448. // we move the set references from the active to the frozen stack.
  12449. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  12450. // we have moved the sector data into the frozen set, we now remove it from the active set
  12451. this._deleteActiveSector(sectorId);
  12452. },
  12453. /**
  12454. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  12455. * object to the "active" object.
  12456. *
  12457. * @param sectorId
  12458. * @private
  12459. */
  12460. _activateSector : function(sectorId) {
  12461. // we move the set references from the frozen to the active stack.
  12462. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  12463. // we have moved the sector data into the active set, we now remove it from the frozen stack
  12464. this._deleteFrozenSector(sectorId);
  12465. },
  12466. /**
  12467. * This function merges the data from the currently active sector with a frozen sector. This is used
  12468. * in the process of reverting back to the previously active sector.
  12469. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  12470. * upon the creation of a new active sector.
  12471. *
  12472. * @param sectorId
  12473. * @private
  12474. */
  12475. _mergeThisWithFrozen : function(sectorId) {
  12476. // copy all nodes
  12477. for (var nodeId in this.nodes) {
  12478. if (this.nodes.hasOwnProperty(nodeId)) {
  12479. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  12480. }
  12481. }
  12482. // copy all edges (if not fully clustered, else there are no edges)
  12483. for (var edgeId in this.edges) {
  12484. if (this.edges.hasOwnProperty(edgeId)) {
  12485. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  12486. }
  12487. }
  12488. // merge the nodeIndices
  12489. for (var i = 0; i < this.nodeIndices.length; i++) {
  12490. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  12491. }
  12492. },
  12493. /**
  12494. * This clusters the sector to one cluster. It was a single cluster before this process started so
  12495. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  12496. *
  12497. * @private
  12498. */
  12499. _collapseThisToSingleCluster : function() {
  12500. this.clusterToFit(1,false);
  12501. },
  12502. /**
  12503. * We create a new active sector from the node that we want to open.
  12504. *
  12505. * @param node
  12506. * @private
  12507. */
  12508. _addSector : function(node) {
  12509. // this is the currently active sector
  12510. var sector = this._sector();
  12511. // // this should allow me to select nodes from a frozen set.
  12512. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  12513. // console.log("the node is part of the active sector");
  12514. // }
  12515. // else {
  12516. // console.log("I dont know what the fuck happened!!");
  12517. // }
  12518. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  12519. delete this.nodes[node.id];
  12520. var unqiueIdentifier = util.randomUUID();
  12521. // we fully freeze the currently active sector
  12522. this._freezeSector(sector);
  12523. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  12524. this._createNewSector(unqiueIdentifier);
  12525. // we add the active sector to the sectors array to be able to revert these steps later on
  12526. this._setActiveSector(unqiueIdentifier);
  12527. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  12528. this._switchToSector(this._sector());
  12529. // finally we add the node we removed from our previous active sector to the new active sector
  12530. this.nodes[node.id] = node;
  12531. },
  12532. /**
  12533. * We close the sector that is currently open and revert back to the one before.
  12534. * If the active sector is the "default" sector, nothing happens.
  12535. *
  12536. * @private
  12537. */
  12538. _collapseSector : function() {
  12539. // the currently active sector
  12540. var sector = this._sector();
  12541. // we cannot collapse the default sector
  12542. if (sector != "default") {
  12543. if ((this.nodeIndices.length == 1) ||
  12544. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12545. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12546. var previousSector = this._previousSector();
  12547. // we collapse the sector back to a single cluster
  12548. this._collapseThisToSingleCluster();
  12549. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  12550. // This previous sector is the one we will reactivate
  12551. this._mergeThisWithFrozen(previousSector);
  12552. // the previously active (frozen) sector now has all the data from the currently active sector.
  12553. // we can now delete the active sector.
  12554. this._deleteActiveSector(sector);
  12555. // we activate the previously active (and currently frozen) sector.
  12556. this._activateSector(previousSector);
  12557. // we load the references from the newly active sector into the global references
  12558. this._switchToSector(previousSector);
  12559. // we forget the previously active sector because we reverted to the one before
  12560. this._forgetLastSector();
  12561. // finally, we update the node index list.
  12562. this._updateNodeIndexList();
  12563. // we refresh the list with calulation nodes and calculation node indices.
  12564. this._updateCalculationNodes();
  12565. }
  12566. }
  12567. },
  12568. /**
  12569. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12570. *
  12571. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12572. * | we dont pass the function itself because then the "this" is the window object
  12573. * | instead of the Graph object
  12574. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12575. * @private
  12576. */
  12577. _doInAllActiveSectors : function(runFunction,argument) {
  12578. if (argument === undefined) {
  12579. for (var sector in this.sectors["active"]) {
  12580. if (this.sectors["active"].hasOwnProperty(sector)) {
  12581. // switch the global references to those of this sector
  12582. this._switchToActiveSector(sector);
  12583. this[runFunction]();
  12584. }
  12585. }
  12586. }
  12587. else {
  12588. for (var sector in this.sectors["active"]) {
  12589. if (this.sectors["active"].hasOwnProperty(sector)) {
  12590. // switch the global references to those of this sector
  12591. this._switchToActiveSector(sector);
  12592. var args = Array.prototype.splice.call(arguments, 1);
  12593. if (args.length > 1) {
  12594. this[runFunction](args[0],args[1]);
  12595. }
  12596. else {
  12597. this[runFunction](argument);
  12598. }
  12599. }
  12600. }
  12601. }
  12602. // we revert the global references back to our active sector
  12603. this._loadLatestSector();
  12604. },
  12605. /**
  12606. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12607. *
  12608. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12609. * | we dont pass the function itself because then the "this" is the window object
  12610. * | instead of the Graph object
  12611. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12612. * @private
  12613. */
  12614. _doInSupportSector : function(runFunction,argument) {
  12615. if (argument === undefined) {
  12616. this._switchToSupportSector();
  12617. this[runFunction]();
  12618. }
  12619. else {
  12620. this._switchToSupportSector();
  12621. var args = Array.prototype.splice.call(arguments, 1);
  12622. if (args.length > 1) {
  12623. this[runFunction](args[0],args[1]);
  12624. }
  12625. else {
  12626. this[runFunction](argument);
  12627. }
  12628. }
  12629. // we revert the global references back to our active sector
  12630. this._loadLatestSector();
  12631. },
  12632. /**
  12633. * This runs a function in all frozen sectors. This is used in the _redraw().
  12634. *
  12635. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12636. * | we don't pass the function itself because then the "this" is the window object
  12637. * | instead of the Graph object
  12638. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12639. * @private
  12640. */
  12641. _doInAllFrozenSectors : function(runFunction,argument) {
  12642. if (argument === undefined) {
  12643. for (var sector in this.sectors["frozen"]) {
  12644. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12645. // switch the global references to those of this sector
  12646. this._switchToFrozenSector(sector);
  12647. this[runFunction]();
  12648. }
  12649. }
  12650. }
  12651. else {
  12652. for (var sector in this.sectors["frozen"]) {
  12653. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12654. // switch the global references to those of this sector
  12655. this._switchToFrozenSector(sector);
  12656. var args = Array.prototype.splice.call(arguments, 1);
  12657. if (args.length > 1) {
  12658. this[runFunction](args[0],args[1]);
  12659. }
  12660. else {
  12661. this[runFunction](argument);
  12662. }
  12663. }
  12664. }
  12665. }
  12666. this._loadLatestSector();
  12667. },
  12668. /**
  12669. * This runs a function in all sectors. This is used in the _redraw().
  12670. *
  12671. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12672. * | we don't pass the function itself because then the "this" is the window object
  12673. * | instead of the Graph object
  12674. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12675. * @private
  12676. */
  12677. _doInAllSectors : function(runFunction,argument) {
  12678. var args = Array.prototype.splice.call(arguments, 1);
  12679. if (argument === undefined) {
  12680. this._doInAllActiveSectors(runFunction);
  12681. this._doInAllFrozenSectors(runFunction);
  12682. }
  12683. else {
  12684. if (args.length > 1) {
  12685. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  12686. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  12687. }
  12688. else {
  12689. this._doInAllActiveSectors(runFunction,argument);
  12690. this._doInAllFrozenSectors(runFunction,argument);
  12691. }
  12692. }
  12693. },
  12694. /**
  12695. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  12696. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  12697. *
  12698. * @private
  12699. */
  12700. _clearNodeIndexList : function() {
  12701. var sector = this._sector();
  12702. this.sectors["active"][sector]["nodeIndices"] = [];
  12703. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  12704. },
  12705. /**
  12706. * Draw the encompassing sector node
  12707. *
  12708. * @param ctx
  12709. * @param sectorType
  12710. * @private
  12711. */
  12712. _drawSectorNodes : function(ctx,sectorType) {
  12713. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  12714. for (var sector in this.sectors[sectorType]) {
  12715. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  12716. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  12717. this._switchToSector(sector,sectorType);
  12718. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  12719. for (var nodeId in this.nodes) {
  12720. if (this.nodes.hasOwnProperty(nodeId)) {
  12721. node = this.nodes[nodeId];
  12722. node.resize(ctx);
  12723. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  12724. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  12725. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  12726. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  12727. }
  12728. }
  12729. node = this.sectors[sectorType][sector]["drawingNode"];
  12730. node.x = 0.5 * (maxX + minX);
  12731. node.y = 0.5 * (maxY + minY);
  12732. node.width = 2 * (node.x - minX);
  12733. node.height = 2 * (node.y - minY);
  12734. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  12735. node.setScale(this.scale);
  12736. node._drawCircle(ctx);
  12737. }
  12738. }
  12739. }
  12740. },
  12741. _drawAllSectorNodes : function(ctx) {
  12742. this._drawSectorNodes(ctx,"frozen");
  12743. this._drawSectorNodes(ctx,"active");
  12744. this._loadLatestSector();
  12745. }
  12746. };
  12747. /**
  12748. * Creation of the ClusterMixin var.
  12749. *
  12750. * This contains all the functions the Graph object can use to employ clustering
  12751. *
  12752. * Alex de Mulder
  12753. * 21-01-2013
  12754. */
  12755. var ClusterMixin = {
  12756. /**
  12757. * This is only called in the constructor of the graph object
  12758. *
  12759. */
  12760. startWithClustering : function() {
  12761. // cluster if the data set is big
  12762. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  12763. // updates the lables after clustering
  12764. this.updateLabels();
  12765. // this is called here because if clusterin is disabled, the start and stabilize are called in
  12766. // the setData function.
  12767. if (this.stabilize) {
  12768. this._stabilize();
  12769. }
  12770. this.start();
  12771. },
  12772. /**
  12773. * This function clusters until the initialMaxNodes has been reached
  12774. *
  12775. * @param {Number} maxNumberOfNodes
  12776. * @param {Boolean} reposition
  12777. */
  12778. clusterToFit : function(maxNumberOfNodes, reposition) {
  12779. var numberOfNodes = this.nodeIndices.length;
  12780. var maxLevels = 50;
  12781. var level = 0;
  12782. // we first cluster the hubs, then we pull in the outliers, repeat
  12783. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  12784. if (level % 3 == 0) {
  12785. this.forceAggregateHubs(true);
  12786. this.normalizeClusterLevels();
  12787. }
  12788. else {
  12789. this.increaseClusterLevel(); // this also includes a cluster normalization
  12790. }
  12791. numberOfNodes = this.nodeIndices.length;
  12792. level += 1;
  12793. }
  12794. // after the clustering we reposition the nodes to reduce the initial chaos
  12795. if (level > 0 && reposition == true) {
  12796. this.repositionNodes();
  12797. }
  12798. this._updateCalculationNodes();
  12799. },
  12800. /**
  12801. * This function can be called to open up a specific cluster. It is only called by
  12802. * It will unpack the cluster back one level.
  12803. *
  12804. * @param node | Node object: cluster to open.
  12805. */
  12806. openCluster : function(node) {
  12807. var isMovingBeforeClustering = this.moving;
  12808. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  12809. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  12810. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  12811. this._addSector(node);
  12812. var level = 0;
  12813. // we decluster until we reach a decent number of nodes
  12814. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  12815. this.decreaseClusterLevel();
  12816. level += 1;
  12817. }
  12818. }
  12819. else {
  12820. this._expandClusterNode(node,false,true);
  12821. // update the index list, dynamic edges and labels
  12822. this._updateNodeIndexList();
  12823. this._updateDynamicEdges();
  12824. this._updateCalculationNodes();
  12825. this.updateLabels();
  12826. }
  12827. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12828. if (this.moving != isMovingBeforeClustering) {
  12829. this.start();
  12830. }
  12831. },
  12832. /**
  12833. * This calls the updateClustes with default arguments
  12834. */
  12835. updateClustersDefault : function() {
  12836. if (this.constants.clustering.enabled == true) {
  12837. this.updateClusters(0,false,false);
  12838. }
  12839. },
  12840. /**
  12841. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  12842. * be clustered with their connected node. This can be repeated as many times as needed.
  12843. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  12844. */
  12845. increaseClusterLevel : function() {
  12846. this.updateClusters(-1,false,true);
  12847. },
  12848. /**
  12849. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  12850. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  12851. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  12852. */
  12853. decreaseClusterLevel : function() {
  12854. this.updateClusters(1,false,true);
  12855. },
  12856. /**
  12857. * This is the main clustering function. It clusters and declusters on zoom or forced
  12858. * This function clusters on zoom, it can be called with a predefined zoom direction
  12859. * If out, check if we can form clusters, if in, check if we can open clusters.
  12860. * This function is only called from _zoom()
  12861. *
  12862. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  12863. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  12864. * @param {Boolean} force | enabled or disable forcing
  12865. * @param {Boolean} doNotStart | if true do not call start
  12866. *
  12867. */
  12868. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  12869. var isMovingBeforeClustering = this.moving;
  12870. var amountOfNodes = this.nodeIndices.length;
  12871. // on zoom out collapse the sector if the scale is at the level the sector was made
  12872. if (this.previousScale > this.scale && zoomDirection == 0) {
  12873. this._collapseSector();
  12874. }
  12875. // check if we zoom in or out
  12876. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12877. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  12878. // outer nodes determines if it is being clustered
  12879. this._formClusters(force);
  12880. }
  12881. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  12882. if (force == true) {
  12883. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  12884. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  12885. this._openClusters(recursive,force);
  12886. }
  12887. else {
  12888. // if a cluster takes up a set percentage of the active window
  12889. this._openClustersBySize();
  12890. }
  12891. }
  12892. this._updateNodeIndexList();
  12893. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  12894. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  12895. this._aggregateHubs(force);
  12896. this._updateNodeIndexList();
  12897. }
  12898. // we now reduce chains.
  12899. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12900. this.handleChains();
  12901. this._updateNodeIndexList();
  12902. }
  12903. this.previousScale = this.scale;
  12904. // rest of the update the index list, dynamic edges and labels
  12905. this._updateDynamicEdges();
  12906. this.updateLabels();
  12907. // if a cluster was formed, we increase the clusterSession
  12908. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  12909. this.clusterSession += 1;
  12910. // if clusters have been made, we normalize the cluster level
  12911. this.normalizeClusterLevels();
  12912. }
  12913. if (doNotStart == false || doNotStart === undefined) {
  12914. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12915. if (this.moving != isMovingBeforeClustering) {
  12916. this.start();
  12917. }
  12918. }
  12919. this._updateCalculationNodes();
  12920. },
  12921. /**
  12922. * This function handles the chains. It is called on every updateClusters().
  12923. */
  12924. handleChains : function() {
  12925. // after clustering we check how many chains there are
  12926. var chainPercentage = this._getChainFraction();
  12927. if (chainPercentage > this.constants.clustering.chainThreshold) {
  12928. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  12929. }
  12930. },
  12931. /**
  12932. * this functions starts clustering by hubs
  12933. * The minimum hub threshold is set globally
  12934. *
  12935. * @private
  12936. */
  12937. _aggregateHubs : function(force) {
  12938. this._getHubSize();
  12939. this._formClustersByHub(force,false);
  12940. },
  12941. /**
  12942. * This function is fired by keypress. It forces hubs to form.
  12943. *
  12944. */
  12945. forceAggregateHubs : function(doNotStart) {
  12946. var isMovingBeforeClustering = this.moving;
  12947. var amountOfNodes = this.nodeIndices.length;
  12948. this._aggregateHubs(true);
  12949. // update the index list, dynamic edges and labels
  12950. this._updateNodeIndexList();
  12951. this._updateDynamicEdges();
  12952. this.updateLabels();
  12953. // if a cluster was formed, we increase the clusterSession
  12954. if (this.nodeIndices.length != amountOfNodes) {
  12955. this.clusterSession += 1;
  12956. }
  12957. if (doNotStart == false || doNotStart === undefined) {
  12958. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12959. if (this.moving != isMovingBeforeClustering) {
  12960. this.start();
  12961. }
  12962. }
  12963. },
  12964. /**
  12965. * If a cluster takes up more than a set percentage of the screen, open the cluster
  12966. *
  12967. * @private
  12968. */
  12969. _openClustersBySize : function() {
  12970. for (var nodeId in this.nodes) {
  12971. if (this.nodes.hasOwnProperty(nodeId)) {
  12972. var node = this.nodes[nodeId];
  12973. if (node.inView() == true) {
  12974. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12975. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12976. this.openCluster(node);
  12977. }
  12978. }
  12979. }
  12980. }
  12981. },
  12982. /**
  12983. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  12984. * has to be opened based on the current zoom level.
  12985. *
  12986. * @private
  12987. */
  12988. _openClusters : function(recursive,force) {
  12989. for (var i = 0; i < this.nodeIndices.length; i++) {
  12990. var node = this.nodes[this.nodeIndices[i]];
  12991. this._expandClusterNode(node,recursive,force);
  12992. this._updateCalculationNodes();
  12993. }
  12994. },
  12995. /**
  12996. * This function checks if a node has to be opened. This is done by checking the zoom level.
  12997. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  12998. * This recursive behaviour is optional and can be set by the recursive argument.
  12999. *
  13000. * @param {Node} parentNode | to check for cluster and expand
  13001. * @param {Boolean} recursive | enabled or disable recursive calling
  13002. * @param {Boolean} force | enabled or disable forcing
  13003. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  13004. * @private
  13005. */
  13006. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  13007. // first check if node is a cluster
  13008. if (parentNode.clusterSize > 1) {
  13009. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  13010. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  13011. openAll = true;
  13012. }
  13013. recursive = openAll ? true : recursive;
  13014. // if the last child has been added on a smaller scale than current scale decluster
  13015. if (parentNode.formationScale < this.scale || force == true) {
  13016. // we will check if any of the contained child nodes should be removed from the cluster
  13017. for (var containedNodeId in parentNode.containedNodes) {
  13018. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  13019. var childNode = parentNode.containedNodes[containedNodeId];
  13020. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  13021. // the largest cluster is the one that comes from outside
  13022. if (force == true) {
  13023. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  13024. || openAll) {
  13025. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  13026. }
  13027. }
  13028. else {
  13029. if (this._nodeInActiveArea(parentNode)) {
  13030. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  13031. }
  13032. }
  13033. }
  13034. }
  13035. }
  13036. }
  13037. },
  13038. /**
  13039. * ONLY CALLED FROM _expandClusterNode
  13040. *
  13041. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  13042. * the child node from the parent contained_node object and put it back into the global nodes object.
  13043. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  13044. *
  13045. * @param {Node} parentNode | the parent node
  13046. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  13047. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  13048. * With force and recursive both true, the entire cluster is unpacked
  13049. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  13050. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  13051. * @private
  13052. */
  13053. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  13054. var childNode = parentNode.containedNodes[containedNodeId];
  13055. // if child node has been added on smaller scale than current, kick out
  13056. if (childNode.formationScale < this.scale || force == true) {
  13057. // unselect all selected items
  13058. this._unselectAll();
  13059. // put the child node back in the global nodes object
  13060. this.nodes[containedNodeId] = childNode;
  13061. // release the contained edges from this childNode back into the global edges
  13062. this._releaseContainedEdges(parentNode,childNode);
  13063. // reconnect rerouted edges to the childNode
  13064. this._connectEdgeBackToChild(parentNode,childNode);
  13065. // validate all edges in dynamicEdges
  13066. this._validateEdges(parentNode);
  13067. // undo the changes from the clustering operation on the parent node
  13068. parentNode.mass -= childNode.mass;
  13069. parentNode.clusterSize -= childNode.clusterSize;
  13070. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  13071. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  13072. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  13073. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  13074. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  13075. // remove node from the list
  13076. delete parentNode.containedNodes[containedNodeId];
  13077. // check if there are other childs with this clusterSession in the parent.
  13078. var othersPresent = false;
  13079. for (var childNodeId in parentNode.containedNodes) {
  13080. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  13081. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  13082. othersPresent = true;
  13083. break;
  13084. }
  13085. }
  13086. }
  13087. // if there are no others, remove the cluster session from the list
  13088. if (othersPresent == false) {
  13089. parentNode.clusterSessions.pop();
  13090. }
  13091. this._repositionBezierNodes(childNode);
  13092. // this._repositionBezierNodes(parentNode);
  13093. // remove the clusterSession from the child node
  13094. childNode.clusterSession = 0;
  13095. // recalculate the size of the node on the next time the node is rendered
  13096. parentNode.clearSizeCache();
  13097. // restart the simulation to reorganise all nodes
  13098. this.moving = true;
  13099. }
  13100. // check if a further expansion step is possible if recursivity is enabled
  13101. if (recursive == true) {
  13102. this._expandClusterNode(childNode,recursive,force,openAll);
  13103. }
  13104. },
  13105. /**
  13106. * position the bezier nodes at the center of the edges
  13107. *
  13108. * @param node
  13109. * @private
  13110. */
  13111. _repositionBezierNodes : function(node) {
  13112. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13113. node.dynamicEdges[i].positionBezierNode();
  13114. }
  13115. },
  13116. /**
  13117. * This function checks if any nodes at the end of their trees have edges below a threshold length
  13118. * This function is called only from updateClusters()
  13119. * forceLevelCollapse ignores the length of the edge and collapses one level
  13120. * This means that a node with only one edge will be clustered with its connected node
  13121. *
  13122. * @private
  13123. * @param {Boolean} force
  13124. */
  13125. _formClusters : function(force) {
  13126. if (force == false) {
  13127. this._formClustersByZoom();
  13128. }
  13129. else {
  13130. this._forceClustersByZoom();
  13131. }
  13132. },
  13133. /**
  13134. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  13135. *
  13136. * @private
  13137. */
  13138. _formClustersByZoom : function() {
  13139. var dx,dy,length,
  13140. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  13141. // check if any edges are shorter than minLength and start the clustering
  13142. // the clustering favours the node with the larger mass
  13143. for (var edgeId in this.edges) {
  13144. if (this.edges.hasOwnProperty(edgeId)) {
  13145. var edge = this.edges[edgeId];
  13146. if (edge.connected) {
  13147. if (edge.toId != edge.fromId) {
  13148. dx = (edge.to.x - edge.from.x);
  13149. dy = (edge.to.y - edge.from.y);
  13150. length = Math.sqrt(dx * dx + dy * dy);
  13151. if (length < minLength) {
  13152. // first check which node is larger
  13153. var parentNode = edge.from;
  13154. var childNode = edge.to;
  13155. if (edge.to.mass > edge.from.mass) {
  13156. parentNode = edge.to;
  13157. childNode = edge.from;
  13158. }
  13159. if (childNode.dynamicEdgesLength == 1) {
  13160. this._addToCluster(parentNode,childNode,false);
  13161. }
  13162. else if (parentNode.dynamicEdgesLength == 1) {
  13163. this._addToCluster(childNode,parentNode,false);
  13164. }
  13165. }
  13166. }
  13167. }
  13168. }
  13169. }
  13170. },
  13171. /**
  13172. * This function forces the graph to cluster all nodes with only one connecting edge to their
  13173. * connected node.
  13174. *
  13175. * @private
  13176. */
  13177. _forceClustersByZoom : function() {
  13178. for (var nodeId in this.nodes) {
  13179. // another node could have absorbed this child.
  13180. if (this.nodes.hasOwnProperty(nodeId)) {
  13181. var childNode = this.nodes[nodeId];
  13182. // the edges can be swallowed by another decrease
  13183. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  13184. var edge = childNode.dynamicEdges[0];
  13185. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  13186. // group to the largest node
  13187. if (childNode.id != parentNode.id) {
  13188. if (parentNode.mass > childNode.mass) {
  13189. this._addToCluster(parentNode,childNode,true);
  13190. }
  13191. else {
  13192. this._addToCluster(childNode,parentNode,true);
  13193. }
  13194. }
  13195. }
  13196. }
  13197. }
  13198. },
  13199. /**
  13200. * To keep the nodes of roughly equal size we normalize the cluster levels.
  13201. * This function clusters a node to its smallest connected neighbour.
  13202. *
  13203. * @param node
  13204. * @private
  13205. */
  13206. _clusterToSmallestNeighbour : function(node) {
  13207. var smallestNeighbour = -1;
  13208. var smallestNeighbourNode = null;
  13209. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13210. if (node.dynamicEdges[i] !== undefined) {
  13211. var neighbour = null;
  13212. if (node.dynamicEdges[i].fromId != node.id) {
  13213. neighbour = node.dynamicEdges[i].from;
  13214. }
  13215. else if (node.dynamicEdges[i].toId != node.id) {
  13216. neighbour = node.dynamicEdges[i].to;
  13217. }
  13218. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  13219. smallestNeighbour = neighbour.clusterSessions.length;
  13220. smallestNeighbourNode = neighbour;
  13221. }
  13222. }
  13223. }
  13224. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  13225. this._addToCluster(neighbour, node, true);
  13226. }
  13227. },
  13228. /**
  13229. * This function forms clusters from hubs, it loops over all nodes
  13230. *
  13231. * @param {Boolean} force | Disregard zoom level
  13232. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  13233. * @private
  13234. */
  13235. _formClustersByHub : function(force, onlyEqual) {
  13236. // we loop over all nodes in the list
  13237. for (var nodeId in this.nodes) {
  13238. // we check if it is still available since it can be used by the clustering in this loop
  13239. if (this.nodes.hasOwnProperty(nodeId)) {
  13240. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  13241. }
  13242. }
  13243. },
  13244. /**
  13245. * This function forms a cluster from a specific preselected hub node
  13246. *
  13247. * @param {Node} hubNode | the node we will cluster as a hub
  13248. * @param {Boolean} force | Disregard zoom level
  13249. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  13250. * @param {Number} [absorptionSizeOffset] |
  13251. * @private
  13252. */
  13253. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  13254. if (absorptionSizeOffset === undefined) {
  13255. absorptionSizeOffset = 0;
  13256. }
  13257. // we decide if the node is a hub
  13258. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  13259. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  13260. // initialize variables
  13261. var dx,dy,length;
  13262. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  13263. var allowCluster = false;
  13264. // we create a list of edges because the dynamicEdges change over the course of this loop
  13265. var edgesIdarray = [];
  13266. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  13267. for (var j = 0; j < amountOfInitialEdges; j++) {
  13268. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  13269. }
  13270. // if the hub clustering is not forces, we check if one of the edges connected
  13271. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  13272. if (force == false) {
  13273. allowCluster = false;
  13274. for (j = 0; j < amountOfInitialEdges; j++) {
  13275. var edge = this.edges[edgesIdarray[j]];
  13276. if (edge !== undefined) {
  13277. if (edge.connected) {
  13278. if (edge.toId != edge.fromId) {
  13279. dx = (edge.to.x - edge.from.x);
  13280. dy = (edge.to.y - edge.from.y);
  13281. length = Math.sqrt(dx * dx + dy * dy);
  13282. if (length < minLength) {
  13283. allowCluster = true;
  13284. break;
  13285. }
  13286. }
  13287. }
  13288. }
  13289. }
  13290. }
  13291. // start the clustering if allowed
  13292. if ((!force && allowCluster) || force) {
  13293. // we loop over all edges INITIALLY connected to this hub
  13294. for (j = 0; j < amountOfInitialEdges; j++) {
  13295. edge = this.edges[edgesIdarray[j]];
  13296. // the edge can be clustered by this function in a previous loop
  13297. if (edge !== undefined) {
  13298. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  13299. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  13300. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  13301. (childNode.id != hubNode.id)) {
  13302. this._addToCluster(hubNode,childNode,force);
  13303. }
  13304. }
  13305. }
  13306. }
  13307. }
  13308. },
  13309. /**
  13310. * This function adds the child node to the parent node, creating a cluster if it is not already.
  13311. *
  13312. * @param {Node} parentNode | this is the node that will house the child node
  13313. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  13314. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  13315. * @private
  13316. */
  13317. _addToCluster : function(parentNode, childNode, force) {
  13318. // join child node in the parent node
  13319. parentNode.containedNodes[childNode.id] = childNode;
  13320. // manage all the edges connected to the child and parent nodes
  13321. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  13322. var edge = childNode.dynamicEdges[i];
  13323. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  13324. this._addToContainedEdges(parentNode,childNode,edge);
  13325. }
  13326. else {
  13327. this._connectEdgeToCluster(parentNode,childNode,edge);
  13328. }
  13329. }
  13330. // a contained node has no dynamic edges.
  13331. childNode.dynamicEdges = [];
  13332. // remove circular edges from clusters
  13333. this._containCircularEdgesFromNode(parentNode,childNode);
  13334. // remove the childNode from the global nodes object
  13335. delete this.nodes[childNode.id];
  13336. // update the properties of the child and parent
  13337. var massBefore = parentNode.mass;
  13338. childNode.clusterSession = this.clusterSession;
  13339. parentNode.mass += childNode.mass;
  13340. parentNode.clusterSize += childNode.clusterSize;
  13341. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  13342. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  13343. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  13344. parentNode.clusterSessions.push(this.clusterSession);
  13345. }
  13346. // forced clusters only open from screen size and double tap
  13347. if (force == true) {
  13348. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  13349. parentNode.formationScale = 0;
  13350. }
  13351. else {
  13352. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  13353. }
  13354. // recalculate the size of the node on the next time the node is rendered
  13355. parentNode.clearSizeCache();
  13356. // set the pop-out scale for the childnode
  13357. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  13358. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  13359. childNode.clearVelocity();
  13360. // the mass has altered, preservation of energy dictates the velocity to be updated
  13361. parentNode.updateVelocity(massBefore);
  13362. // restart the simulation to reorganise all nodes
  13363. this.moving = true;
  13364. },
  13365. /**
  13366. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  13367. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  13368. * It has to be called if a level is collapsed. It is called by _formClusters().
  13369. * @private
  13370. */
  13371. _updateDynamicEdges : function() {
  13372. for (var i = 0; i < this.nodeIndices.length; i++) {
  13373. var node = this.nodes[this.nodeIndices[i]];
  13374. node.dynamicEdgesLength = node.dynamicEdges.length;
  13375. // this corrects for multiple edges pointing at the same other node
  13376. var correction = 0;
  13377. if (node.dynamicEdgesLength > 1) {
  13378. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  13379. var edgeToId = node.dynamicEdges[j].toId;
  13380. var edgeFromId = node.dynamicEdges[j].fromId;
  13381. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  13382. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  13383. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  13384. correction += 1;
  13385. }
  13386. }
  13387. }
  13388. }
  13389. node.dynamicEdgesLength -= correction;
  13390. }
  13391. },
  13392. /**
  13393. * This adds an edge from the childNode to the contained edges of the parent node
  13394. *
  13395. * @param parentNode | Node object
  13396. * @param childNode | Node object
  13397. * @param edge | Edge object
  13398. * @private
  13399. */
  13400. _addToContainedEdges : function(parentNode, childNode, edge) {
  13401. // create an array object if it does not yet exist for this childNode
  13402. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  13403. parentNode.containedEdges[childNode.id] = []
  13404. }
  13405. // add this edge to the list
  13406. parentNode.containedEdges[childNode.id].push(edge);
  13407. // remove the edge from the global edges object
  13408. delete this.edges[edge.id];
  13409. // remove the edge from the parent object
  13410. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13411. if (parentNode.dynamicEdges[i].id == edge.id) {
  13412. parentNode.dynamicEdges.splice(i,1);
  13413. break;
  13414. }
  13415. }
  13416. },
  13417. /**
  13418. * This function connects an edge that was connected to a child node to the parent node.
  13419. * It keeps track of which nodes it has been connected to with the originalId array.
  13420. *
  13421. * @param {Node} parentNode | Node object
  13422. * @param {Node} childNode | Node object
  13423. * @param {Edge} edge | Edge object
  13424. * @private
  13425. */
  13426. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  13427. // handle circular edges
  13428. if (edge.toId == edge.fromId) {
  13429. this._addToContainedEdges(parentNode, childNode, edge);
  13430. }
  13431. else {
  13432. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  13433. edge.originalToId.push(childNode.id);
  13434. edge.to = parentNode;
  13435. edge.toId = parentNode.id;
  13436. }
  13437. else { // edge connected to other node with the "from" side
  13438. edge.originalFromId.push(childNode.id);
  13439. edge.from = parentNode;
  13440. edge.fromId = parentNode.id;
  13441. }
  13442. this._addToReroutedEdges(parentNode,childNode,edge);
  13443. }
  13444. },
  13445. /**
  13446. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  13447. * these edges inside of the cluster.
  13448. *
  13449. * @param parentNode
  13450. * @param childNode
  13451. * @private
  13452. */
  13453. _containCircularEdgesFromNode : function(parentNode, childNode) {
  13454. // manage all the edges connected to the child and parent nodes
  13455. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13456. var edge = parentNode.dynamicEdges[i];
  13457. // handle circular edges
  13458. if (edge.toId == edge.fromId) {
  13459. this._addToContainedEdges(parentNode, childNode, edge);
  13460. }
  13461. }
  13462. },
  13463. /**
  13464. * This adds an edge from the childNode to the rerouted edges of the parent node
  13465. *
  13466. * @param parentNode | Node object
  13467. * @param childNode | Node object
  13468. * @param edge | Edge object
  13469. * @private
  13470. */
  13471. _addToReroutedEdges : function(parentNode, childNode, edge) {
  13472. // create an array object if it does not yet exist for this childNode
  13473. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  13474. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  13475. parentNode.reroutedEdges[childNode.id] = [];
  13476. }
  13477. parentNode.reroutedEdges[childNode.id].push(edge);
  13478. // this edge becomes part of the dynamicEdges of the cluster node
  13479. parentNode.dynamicEdges.push(edge);
  13480. },
  13481. /**
  13482. * This function connects an edge that was connected to a cluster node back to the child node.
  13483. *
  13484. * @param parentNode | Node object
  13485. * @param childNode | Node object
  13486. * @private
  13487. */
  13488. _connectEdgeBackToChild : function(parentNode, childNode) {
  13489. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  13490. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  13491. var edge = parentNode.reroutedEdges[childNode.id][i];
  13492. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  13493. edge.originalFromId.pop();
  13494. edge.fromId = childNode.id;
  13495. edge.from = childNode;
  13496. }
  13497. else {
  13498. edge.originalToId.pop();
  13499. edge.toId = childNode.id;
  13500. edge.to = childNode;
  13501. }
  13502. // append this edge to the list of edges connecting to the childnode
  13503. childNode.dynamicEdges.push(edge);
  13504. // remove the edge from the parent object
  13505. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  13506. if (parentNode.dynamicEdges[j].id == edge.id) {
  13507. parentNode.dynamicEdges.splice(j,1);
  13508. break;
  13509. }
  13510. }
  13511. }
  13512. // remove the entry from the rerouted edges
  13513. delete parentNode.reroutedEdges[childNode.id];
  13514. }
  13515. },
  13516. /**
  13517. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  13518. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  13519. * parentNode
  13520. *
  13521. * @param parentNode | Node object
  13522. * @private
  13523. */
  13524. _validateEdges : function(parentNode) {
  13525. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13526. var edge = parentNode.dynamicEdges[i];
  13527. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  13528. parentNode.dynamicEdges.splice(i,1);
  13529. }
  13530. }
  13531. },
  13532. /**
  13533. * This function released the contained edges back into the global domain and puts them back into the
  13534. * dynamic edges of both parent and child.
  13535. *
  13536. * @param {Node} parentNode |
  13537. * @param {Node} childNode |
  13538. * @private
  13539. */
  13540. _releaseContainedEdges : function(parentNode, childNode) {
  13541. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  13542. var edge = parentNode.containedEdges[childNode.id][i];
  13543. // put the edge back in the global edges object
  13544. this.edges[edge.id] = edge;
  13545. // put the edge back in the dynamic edges of the child and parent
  13546. childNode.dynamicEdges.push(edge);
  13547. parentNode.dynamicEdges.push(edge);
  13548. }
  13549. // remove the entry from the contained edges
  13550. delete parentNode.containedEdges[childNode.id];
  13551. },
  13552. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  13553. /**
  13554. * This updates the node labels for all nodes (for debugging purposes)
  13555. */
  13556. updateLabels : function() {
  13557. var nodeId;
  13558. // update node labels
  13559. for (nodeId in this.nodes) {
  13560. if (this.nodes.hasOwnProperty(nodeId)) {
  13561. var node = this.nodes[nodeId];
  13562. if (node.clusterSize > 1) {
  13563. node.label = "[".concat(String(node.clusterSize),"]");
  13564. }
  13565. }
  13566. }
  13567. // update node labels
  13568. for (nodeId in this.nodes) {
  13569. if (this.nodes.hasOwnProperty(nodeId)) {
  13570. node = this.nodes[nodeId];
  13571. if (node.clusterSize == 1) {
  13572. if (node.originalLabel !== undefined) {
  13573. node.label = node.originalLabel;
  13574. }
  13575. else {
  13576. node.label = String(node.id);
  13577. }
  13578. }
  13579. }
  13580. }
  13581. // /* Debug Override */
  13582. // for (nodeId in this.nodes) {
  13583. // if (this.nodes.hasOwnProperty(nodeId)) {
  13584. // node = this.nodes[nodeId];
  13585. // node.label = String(node.level);
  13586. // }
  13587. // }
  13588. },
  13589. /**
  13590. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  13591. * if the rest of the nodes are already a few cluster levels in.
  13592. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  13593. * clustered enough to the clusterToSmallestNeighbours function.
  13594. */
  13595. normalizeClusterLevels : function() {
  13596. var maxLevel = 0;
  13597. var minLevel = 1e9;
  13598. var clusterLevel = 0;
  13599. var nodeId;
  13600. // we loop over all nodes in the list
  13601. for (nodeId in this.nodes) {
  13602. if (this.nodes.hasOwnProperty(nodeId)) {
  13603. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  13604. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  13605. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  13606. }
  13607. }
  13608. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  13609. var amountOfNodes = this.nodeIndices.length;
  13610. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  13611. // we loop over all nodes in the list
  13612. for (nodeId in this.nodes) {
  13613. if (this.nodes.hasOwnProperty(nodeId)) {
  13614. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  13615. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  13616. }
  13617. }
  13618. }
  13619. this._updateNodeIndexList();
  13620. this._updateDynamicEdges();
  13621. // if a cluster was formed, we increase the clusterSession
  13622. if (this.nodeIndices.length != amountOfNodes) {
  13623. this.clusterSession += 1;
  13624. }
  13625. }
  13626. },
  13627. /**
  13628. * This function determines if the cluster we want to decluster is in the active area
  13629. * this means around the zoom center
  13630. *
  13631. * @param {Node} node
  13632. * @returns {boolean}
  13633. * @private
  13634. */
  13635. _nodeInActiveArea : function(node) {
  13636. return (
  13637. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13638. &&
  13639. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13640. )
  13641. },
  13642. /**
  13643. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  13644. * It puts large clusters away from the center and randomizes the order.
  13645. *
  13646. */
  13647. repositionNodes : function() {
  13648. for (var i = 0; i < this.nodeIndices.length; i++) {
  13649. var node = this.nodes[this.nodeIndices[i]];
  13650. if ((node.xFixed == false || node.yFixed == false)) {
  13651. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  13652. var angle = 2 * Math.PI * Math.random();
  13653. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  13654. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  13655. this._repositionBezierNodes(node);
  13656. }
  13657. }
  13658. },
  13659. /**
  13660. * We determine how many connections denote an important hub.
  13661. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  13662. *
  13663. * @private
  13664. */
  13665. _getHubSize : function() {
  13666. var average = 0;
  13667. var averageSquared = 0;
  13668. var hubCounter = 0;
  13669. var largestHub = 0;
  13670. for (var i = 0; i < this.nodeIndices.length; i++) {
  13671. var node = this.nodes[this.nodeIndices[i]];
  13672. if (node.dynamicEdgesLength > largestHub) {
  13673. largestHub = node.dynamicEdgesLength;
  13674. }
  13675. average += node.dynamicEdgesLength;
  13676. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  13677. hubCounter += 1;
  13678. }
  13679. average = average / hubCounter;
  13680. averageSquared = averageSquared / hubCounter;
  13681. var variance = averageSquared - Math.pow(average,2);
  13682. var standardDeviation = Math.sqrt(variance);
  13683. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  13684. // always have at least one to cluster
  13685. if (this.hubThreshold > largestHub) {
  13686. this.hubThreshold = largestHub;
  13687. }
  13688. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  13689. // console.log("hubThreshold:",this.hubThreshold);
  13690. },
  13691. /**
  13692. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13693. * with this amount we can cluster specifically on these chains.
  13694. *
  13695. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  13696. * @private
  13697. */
  13698. _reduceAmountOfChains : function(fraction) {
  13699. this.hubThreshold = 2;
  13700. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  13701. for (var nodeId in this.nodes) {
  13702. if (this.nodes.hasOwnProperty(nodeId)) {
  13703. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13704. if (reduceAmount > 0) {
  13705. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  13706. reduceAmount -= 1;
  13707. }
  13708. }
  13709. }
  13710. }
  13711. },
  13712. /**
  13713. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13714. * with this amount we can cluster specifically on these chains.
  13715. *
  13716. * @private
  13717. */
  13718. _getChainFraction : function() {
  13719. var chains = 0;
  13720. var total = 0;
  13721. for (var nodeId in this.nodes) {
  13722. if (this.nodes.hasOwnProperty(nodeId)) {
  13723. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13724. chains += 1;
  13725. }
  13726. total += 1;
  13727. }
  13728. }
  13729. return chains/total;
  13730. }
  13731. };
  13732. var SelectionMixin = {
  13733. /**
  13734. * This function can be called from the _doInAllSectors function
  13735. *
  13736. * @param object
  13737. * @param overlappingNodes
  13738. * @private
  13739. */
  13740. _getNodesOverlappingWith : function(object, overlappingNodes) {
  13741. var nodes = this.nodes;
  13742. for (var nodeId in nodes) {
  13743. if (nodes.hasOwnProperty(nodeId)) {
  13744. if (nodes[nodeId].isOverlappingWith(object)) {
  13745. overlappingNodes.push(nodeId);
  13746. }
  13747. }
  13748. }
  13749. },
  13750. /**
  13751. * retrieve all nodes overlapping with given object
  13752. * @param {Object} object An object with parameters left, top, right, bottom
  13753. * @return {Number[]} An array with id's of the overlapping nodes
  13754. * @private
  13755. */
  13756. _getAllNodesOverlappingWith : function (object) {
  13757. var overlappingNodes = [];
  13758. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  13759. return overlappingNodes;
  13760. },
  13761. /**
  13762. * Return a position object in canvasspace from a single point in screenspace
  13763. *
  13764. * @param pointer
  13765. * @returns {{left: number, top: number, right: number, bottom: number}}
  13766. * @private
  13767. */
  13768. _pointerToPositionObject : function(pointer) {
  13769. var x = this._XconvertDOMtoCanvas(pointer.x);
  13770. var y = this._YconvertDOMtoCanvas(pointer.y);
  13771. return {left: x,
  13772. top: y,
  13773. right: x,
  13774. bottom: y};
  13775. },
  13776. /**
  13777. * Get the top node at the a specific point (like a click)
  13778. *
  13779. * @param {{x: Number, y: Number}} pointer
  13780. * @return {Node | null} node
  13781. * @private
  13782. */
  13783. _getNodeAt : function (pointer) {
  13784. // we first check if this is an navigation controls element
  13785. var positionObject = this._pointerToPositionObject(pointer);
  13786. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  13787. // if there are overlapping nodes, select the last one, this is the
  13788. // one which is drawn on top of the others
  13789. if (overlappingNodes.length > 0) {
  13790. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  13791. }
  13792. else {
  13793. return null;
  13794. }
  13795. },
  13796. /**
  13797. * retrieve all edges overlapping with given object, selector is around center
  13798. * @param {Object} object An object with parameters left, top, right, bottom
  13799. * @return {Number[]} An array with id's of the overlapping nodes
  13800. * @private
  13801. */
  13802. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  13803. var edges = this.edges;
  13804. for (var edgeId in edges) {
  13805. if (edges.hasOwnProperty(edgeId)) {
  13806. if (edges[edgeId].isOverlappingWith(object)) {
  13807. overlappingEdges.push(edgeId);
  13808. }
  13809. }
  13810. }
  13811. },
  13812. /**
  13813. * retrieve all nodes overlapping with given object
  13814. * @param {Object} object An object with parameters left, top, right, bottom
  13815. * @return {Number[]} An array with id's of the overlapping nodes
  13816. * @private
  13817. */
  13818. _getAllEdgesOverlappingWith : function (object) {
  13819. var overlappingEdges = [];
  13820. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  13821. return overlappingEdges;
  13822. },
  13823. /**
  13824. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  13825. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  13826. *
  13827. * @param pointer
  13828. * @returns {null}
  13829. * @private
  13830. */
  13831. _getEdgeAt : function(pointer) {
  13832. var positionObject = this._pointerToPositionObject(pointer);
  13833. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  13834. if (overlappingEdges.length > 0) {
  13835. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  13836. }
  13837. else {
  13838. return null;
  13839. }
  13840. },
  13841. /**
  13842. * Add object to the selection array.
  13843. *
  13844. * @param obj
  13845. * @private
  13846. */
  13847. _addToSelection : function(obj) {
  13848. if (obj instanceof Node) {
  13849. this.selectionObj.nodes[obj.id] = obj;
  13850. }
  13851. else {
  13852. this.selectionObj.edges[obj.id] = obj;
  13853. }
  13854. },
  13855. /**
  13856. * Remove a single option from selection.
  13857. *
  13858. * @param {Object} obj
  13859. * @private
  13860. */
  13861. _removeFromSelection : function(obj) {
  13862. if (obj instanceof Node) {
  13863. delete this.selectionObj.nodes[obj.id];
  13864. }
  13865. else {
  13866. delete this.selectionObj.edges[obj.id];
  13867. }
  13868. },
  13869. /**
  13870. * Unselect all. The selectionObj is useful for this.
  13871. *
  13872. * @param {Boolean} [doNotTrigger] | ignore trigger
  13873. * @private
  13874. */
  13875. _unselectAll : function(doNotTrigger) {
  13876. if (doNotTrigger === undefined) {
  13877. doNotTrigger = false;
  13878. }
  13879. for(var nodeId in this.selectionObj.nodes) {
  13880. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13881. this.selectionObj.nodes[nodeId].unselect();
  13882. }
  13883. }
  13884. for(var edgeId in this.selectionObj.edges) {
  13885. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13886. this.selectionObj.edges[edgeId].unselect();
  13887. }
  13888. }
  13889. this.selectionObj = {nodes:{},edges:{}};
  13890. if (doNotTrigger == false) {
  13891. this.emit('select', this.getSelection());
  13892. }
  13893. },
  13894. /**
  13895. * Unselect all clusters. The selectionObj is useful for this.
  13896. *
  13897. * @param {Boolean} [doNotTrigger] | ignore trigger
  13898. * @private
  13899. */
  13900. _unselectClusters : function(doNotTrigger) {
  13901. if (doNotTrigger === undefined) {
  13902. doNotTrigger = false;
  13903. }
  13904. for (var nodeId in this.selectionObj.nodes) {
  13905. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13906. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13907. this.selectionObj.nodes[nodeId].unselect();
  13908. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  13909. }
  13910. }
  13911. }
  13912. if (doNotTrigger == false) {
  13913. this.emit('select', this.getSelection());
  13914. }
  13915. },
  13916. /**
  13917. * return the number of selected nodes
  13918. *
  13919. * @returns {number}
  13920. * @private
  13921. */
  13922. _getSelectedNodeCount : function() {
  13923. var count = 0;
  13924. for (var nodeId in this.selectionObj.nodes) {
  13925. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13926. count += 1;
  13927. }
  13928. }
  13929. return count;
  13930. },
  13931. /**
  13932. * return the number of selected nodes
  13933. *
  13934. * @returns {number}
  13935. * @private
  13936. */
  13937. _getSelectedNode : function() {
  13938. for (var nodeId in this.selectionObj.nodes) {
  13939. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13940. return this.selectionObj.nodes[nodeId];
  13941. }
  13942. }
  13943. return null;
  13944. },
  13945. /**
  13946. * return the number of selected edges
  13947. *
  13948. * @returns {number}
  13949. * @private
  13950. */
  13951. _getSelectedEdgeCount : function() {
  13952. var count = 0;
  13953. for (var edgeId in this.selectionObj.edges) {
  13954. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13955. count += 1;
  13956. }
  13957. }
  13958. return count;
  13959. },
  13960. /**
  13961. * return the number of selected objects.
  13962. *
  13963. * @returns {number}
  13964. * @private
  13965. */
  13966. _getSelectedObjectCount : function() {
  13967. var count = 0;
  13968. for(var nodeId in this.selectionObj.nodes) {
  13969. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13970. count += 1;
  13971. }
  13972. }
  13973. for(var edgeId in this.selectionObj.edges) {
  13974. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13975. count += 1;
  13976. }
  13977. }
  13978. return count;
  13979. },
  13980. /**
  13981. * Check if anything is selected
  13982. *
  13983. * @returns {boolean}
  13984. * @private
  13985. */
  13986. _selectionIsEmpty : function() {
  13987. for(var nodeId in this.selectionObj.nodes) {
  13988. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13989. return false;
  13990. }
  13991. }
  13992. for(var edgeId in this.selectionObj.edges) {
  13993. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13994. return false;
  13995. }
  13996. }
  13997. return true;
  13998. },
  13999. /**
  14000. * check if one of the selected nodes is a cluster.
  14001. *
  14002. * @returns {boolean}
  14003. * @private
  14004. */
  14005. _clusterInSelection : function() {
  14006. for(var nodeId in this.selectionObj.nodes) {
  14007. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  14008. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  14009. return true;
  14010. }
  14011. }
  14012. }
  14013. return false;
  14014. },
  14015. /**
  14016. * select the edges connected to the node that is being selected
  14017. *
  14018. * @param {Node} node
  14019. * @private
  14020. */
  14021. _selectConnectedEdges : function(node) {
  14022. for (var i = 0; i < node.dynamicEdges.length; i++) {
  14023. var edge = node.dynamicEdges[i];
  14024. edge.select();
  14025. this._addToSelection(edge);
  14026. }
  14027. },
  14028. /**
  14029. * unselect the edges connected to the node that is being selected
  14030. *
  14031. * @param {Node} node
  14032. * @private
  14033. */
  14034. _unselectConnectedEdges : function(node) {
  14035. for (var i = 0; i < node.dynamicEdges.length; i++) {
  14036. var edge = node.dynamicEdges[i];
  14037. edge.unselect();
  14038. this._removeFromSelection(edge);
  14039. }
  14040. },
  14041. /**
  14042. * This is called when someone clicks on a node. either select or deselect it.
  14043. * If there is an existing selection and we don't want to append to it, clear the existing selection
  14044. *
  14045. * @param {Node || Edge} object
  14046. * @param {Boolean} append
  14047. * @param {Boolean} [doNotTrigger] | ignore trigger
  14048. * @private
  14049. */
  14050. _selectObject : function(object, append, doNotTrigger) {
  14051. if (doNotTrigger === undefined) {
  14052. doNotTrigger = false;
  14053. }
  14054. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  14055. this._unselectAll(true);
  14056. }
  14057. if (object.selected == false) {
  14058. object.select();
  14059. this._addToSelection(object);
  14060. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  14061. this._selectConnectedEdges(object);
  14062. }
  14063. }
  14064. else {
  14065. object.unselect();
  14066. this._removeFromSelection(object);
  14067. }
  14068. if (doNotTrigger == false) {
  14069. this.emit('select', this.getSelection());
  14070. }
  14071. },
  14072. /**
  14073. * handles the selection part of the touch, only for navigation controls elements;
  14074. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  14075. * This is the most responsive solution
  14076. *
  14077. * @param {Object} pointer
  14078. * @private
  14079. */
  14080. _handleTouch : function(pointer) {
  14081. },
  14082. /**
  14083. * handles the selection part of the tap;
  14084. *
  14085. * @param {Object} pointer
  14086. * @private
  14087. */
  14088. _handleTap : function(pointer) {
  14089. var node = this._getNodeAt(pointer);
  14090. if (node != null) {
  14091. this._selectObject(node,false);
  14092. }
  14093. else {
  14094. var edge = this._getEdgeAt(pointer);
  14095. if (edge != null) {
  14096. this._selectObject(edge,false);
  14097. }
  14098. else {
  14099. this._unselectAll();
  14100. }
  14101. }
  14102. this.emit("click", this.getSelection());
  14103. this._redraw();
  14104. },
  14105. /**
  14106. * handles the selection part of the double tap and opens a cluster if needed
  14107. *
  14108. * @param {Object} pointer
  14109. * @private
  14110. */
  14111. _handleDoubleTap : function(pointer) {
  14112. var node = this._getNodeAt(pointer);
  14113. if (node != null && node !== undefined) {
  14114. // we reset the areaCenter here so the opening of the node will occur
  14115. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  14116. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  14117. this.openCluster(node);
  14118. }
  14119. this.emit("doubleClick", this.getSelection());
  14120. },
  14121. /**
  14122. * Handle the onHold selection part
  14123. *
  14124. * @param pointer
  14125. * @private
  14126. */
  14127. _handleOnHold : function(pointer) {
  14128. var node = this._getNodeAt(pointer);
  14129. if (node != null) {
  14130. this._selectObject(node,true);
  14131. }
  14132. else {
  14133. var edge = this._getEdgeAt(pointer);
  14134. if (edge != null) {
  14135. this._selectObject(edge,true);
  14136. }
  14137. }
  14138. this._redraw();
  14139. },
  14140. /**
  14141. * handle the onRelease event. These functions are here for the navigation controls module.
  14142. *
  14143. * @private
  14144. */
  14145. _handleOnRelease : function(pointer) {
  14146. },
  14147. /**
  14148. *
  14149. * retrieve the currently selected objects
  14150. * @return {Number[] | String[]} selection An array with the ids of the
  14151. * selected nodes.
  14152. */
  14153. getSelection : function() {
  14154. var nodeIds = this.getSelectedNodes();
  14155. var edgeIds = this.getSelectedEdges();
  14156. return {nodes:nodeIds, edges:edgeIds};
  14157. },
  14158. /**
  14159. *
  14160. * retrieve the currently selected nodes
  14161. * @return {String} selection An array with the ids of the
  14162. * selected nodes.
  14163. */
  14164. getSelectedNodes : function() {
  14165. var idArray = [];
  14166. for(var nodeId in this.selectionObj.nodes) {
  14167. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  14168. idArray.push(nodeId);
  14169. }
  14170. }
  14171. return idArray
  14172. },
  14173. /**
  14174. *
  14175. * retrieve the currently selected edges
  14176. * @return {Array} selection An array with the ids of the
  14177. * selected nodes.
  14178. */
  14179. getSelectedEdges : function() {
  14180. var idArray = [];
  14181. for(var edgeId in this.selectionObj.edges) {
  14182. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  14183. idArray.push(edgeId);
  14184. }
  14185. }
  14186. return idArray;
  14187. },
  14188. /**
  14189. * select zero or more nodes
  14190. * @param {Number[] | String[]} selection An array with the ids of the
  14191. * selected nodes.
  14192. */
  14193. setSelection : function(selection) {
  14194. var i, iMax, id;
  14195. if (!selection || (selection.length == undefined))
  14196. throw 'Selection must be an array with ids';
  14197. // first unselect any selected node
  14198. this._unselectAll(true);
  14199. for (i = 0, iMax = selection.length; i < iMax; i++) {
  14200. id = selection[i];
  14201. var node = this.nodes[id];
  14202. if (!node) {
  14203. throw new RangeError('Node with id "' + id + '" not found');
  14204. }
  14205. this._selectObject(node,true,true);
  14206. }
  14207. this.redraw();
  14208. },
  14209. /**
  14210. * Validate the selection: remove ids of nodes which no longer exist
  14211. * @private
  14212. */
  14213. _updateSelection : function () {
  14214. for(var nodeId in this.selectionObj.nodes) {
  14215. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  14216. if (!this.nodes.hasOwnProperty(nodeId)) {
  14217. delete this.selectionObj.nodes[nodeId];
  14218. }
  14219. }
  14220. }
  14221. for(var edgeId in this.selectionObj.edges) {
  14222. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  14223. if (!this.edges.hasOwnProperty(edgeId)) {
  14224. delete this.selectionObj.edges[edgeId];
  14225. }
  14226. }
  14227. }
  14228. }
  14229. };
  14230. /**
  14231. * Created by Alex on 1/22/14.
  14232. */
  14233. var NavigationMixin = {
  14234. _cleanNavigation : function() {
  14235. // clean up previosu navigation items
  14236. var wrapper = document.getElementById('graph-navigation_wrapper');
  14237. if (wrapper != null) {
  14238. this.containerElement.removeChild(wrapper);
  14239. }
  14240. document.onmouseup = null;
  14241. },
  14242. /**
  14243. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  14244. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  14245. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  14246. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  14247. *
  14248. * @private
  14249. */
  14250. _loadNavigationElements : function() {
  14251. this._cleanNavigation();
  14252. this.navigationDivs = {};
  14253. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  14254. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  14255. this.navigationDivs['wrapper'] = document.createElement('div');
  14256. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  14257. this.navigationDivs['wrapper'].style.position = "absolute";
  14258. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14259. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14260. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  14261. for (var i = 0; i < navigationDivs.length; i++) {
  14262. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  14263. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  14264. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  14265. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  14266. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  14267. }
  14268. document.onmouseup = this._stopMovement.bind(this);
  14269. },
  14270. /**
  14271. * this stops all movement induced by the navigation buttons
  14272. *
  14273. * @private
  14274. */
  14275. _stopMovement : function() {
  14276. this._xStopMoving();
  14277. this._yStopMoving();
  14278. this._stopZoom();
  14279. },
  14280. /**
  14281. * stops the actions performed by page up and down etc.
  14282. *
  14283. * @param event
  14284. * @private
  14285. */
  14286. _preventDefault : function(event) {
  14287. if (event !== undefined) {
  14288. if (event.preventDefault) {
  14289. event.preventDefault();
  14290. } else {
  14291. event.returnValue = false;
  14292. }
  14293. }
  14294. },
  14295. /**
  14296. * move the screen up
  14297. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  14298. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  14299. * To avoid this behaviour, we do the translation in the start loop.
  14300. *
  14301. * @private
  14302. */
  14303. _moveUp : function(event) {
  14304. this.yIncrement = this.constants.keyboard.speed.y;
  14305. this.start(); // if there is no node movement, the calculation wont be done
  14306. this._preventDefault(event);
  14307. if (this.navigationDivs) {
  14308. this.navigationDivs['up'].className += " active";
  14309. }
  14310. },
  14311. /**
  14312. * move the screen down
  14313. * @private
  14314. */
  14315. _moveDown : function(event) {
  14316. this.yIncrement = -this.constants.keyboard.speed.y;
  14317. this.start(); // if there is no node movement, the calculation wont be done
  14318. this._preventDefault(event);
  14319. if (this.navigationDivs) {
  14320. this.navigationDivs['down'].className += " active";
  14321. }
  14322. },
  14323. /**
  14324. * move the screen left
  14325. * @private
  14326. */
  14327. _moveLeft : function(event) {
  14328. this.xIncrement = this.constants.keyboard.speed.x;
  14329. this.start(); // if there is no node movement, the calculation wont be done
  14330. this._preventDefault(event);
  14331. if (this.navigationDivs) {
  14332. this.navigationDivs['left'].className += " active";
  14333. }
  14334. },
  14335. /**
  14336. * move the screen right
  14337. * @private
  14338. */
  14339. _moveRight : function(event) {
  14340. this.xIncrement = -this.constants.keyboard.speed.y;
  14341. this.start(); // if there is no node movement, the calculation wont be done
  14342. this._preventDefault(event);
  14343. if (this.navigationDivs) {
  14344. this.navigationDivs['right'].className += " active";
  14345. }
  14346. },
  14347. /**
  14348. * Zoom in, using the same method as the movement.
  14349. * @private
  14350. */
  14351. _zoomIn : function(event) {
  14352. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  14353. this.start(); // if there is no node movement, the calculation wont be done
  14354. this._preventDefault(event);
  14355. if (this.navigationDivs) {
  14356. this.navigationDivs['zoomIn'].className += " active";
  14357. }
  14358. },
  14359. /**
  14360. * Zoom out
  14361. * @private
  14362. */
  14363. _zoomOut : function() {
  14364. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  14365. this.start(); // if there is no node movement, the calculation wont be done
  14366. this._preventDefault(event);
  14367. if (this.navigationDivs) {
  14368. this.navigationDivs['zoomOut'].className += " active";
  14369. }
  14370. },
  14371. /**
  14372. * Stop zooming and unhighlight the zoom controls
  14373. * @private
  14374. */
  14375. _stopZoom : function() {
  14376. this.zoomIncrement = 0;
  14377. if (this.navigationDivs) {
  14378. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  14379. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  14380. }
  14381. },
  14382. /**
  14383. * Stop moving in the Y direction and unHighlight the up and down
  14384. * @private
  14385. */
  14386. _yStopMoving : function() {
  14387. this.yIncrement = 0;
  14388. if (this.navigationDivs) {
  14389. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  14390. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  14391. }
  14392. },
  14393. /**
  14394. * Stop moving in the X direction and unHighlight left and right.
  14395. * @private
  14396. */
  14397. _xStopMoving : function() {
  14398. this.xIncrement = 0;
  14399. if (this.navigationDivs) {
  14400. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  14401. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  14402. }
  14403. }
  14404. };
  14405. /**
  14406. * Created by Alex on 2/10/14.
  14407. */
  14408. var graphMixinLoaders = {
  14409. /**
  14410. * Load a mixin into the graph object
  14411. *
  14412. * @param {Object} sourceVariable | this object has to contain functions.
  14413. * @private
  14414. */
  14415. _loadMixin: function (sourceVariable) {
  14416. for (var mixinFunction in sourceVariable) {
  14417. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  14418. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  14419. }
  14420. }
  14421. },
  14422. /**
  14423. * removes a mixin from the graph object.
  14424. *
  14425. * @param {Object} sourceVariable | this object has to contain functions.
  14426. * @private
  14427. */
  14428. _clearMixin: function (sourceVariable) {
  14429. for (var mixinFunction in sourceVariable) {
  14430. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  14431. Graph.prototype[mixinFunction] = undefined;
  14432. }
  14433. }
  14434. },
  14435. /**
  14436. * Mixin the physics system and initialize the parameters required.
  14437. *
  14438. * @private
  14439. */
  14440. _loadPhysicsSystem: function () {
  14441. this._loadMixin(physicsMixin);
  14442. this._loadSelectedForceSolver();
  14443. if (this.constants.configurePhysics == true) {
  14444. this._loadPhysicsConfiguration();
  14445. }
  14446. },
  14447. /**
  14448. * Mixin the cluster system and initialize the parameters required.
  14449. *
  14450. * @private
  14451. */
  14452. _loadClusterSystem: function () {
  14453. this.clusterSession = 0;
  14454. this.hubThreshold = 5;
  14455. this._loadMixin(ClusterMixin);
  14456. },
  14457. /**
  14458. * Mixin the sector system and initialize the parameters required
  14459. *
  14460. * @private
  14461. */
  14462. _loadSectorSystem: function () {
  14463. this.sectors = {};
  14464. this.activeSector = ["default"];
  14465. this.sectors["active"] = {};
  14466. this.sectors["active"]["default"] = {"nodes": {},
  14467. "edges": {},
  14468. "nodeIndices": [],
  14469. "formationScale": 1.0,
  14470. "drawingNode": undefined };
  14471. this.sectors["frozen"] = {};
  14472. this.sectors["support"] = {"nodes": {},
  14473. "edges": {},
  14474. "nodeIndices": [],
  14475. "formationScale": 1.0,
  14476. "drawingNode": undefined };
  14477. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  14478. this._loadMixin(SectorMixin);
  14479. },
  14480. /**
  14481. * Mixin the selection system and initialize the parameters required
  14482. *
  14483. * @private
  14484. */
  14485. _loadSelectionSystem: function () {
  14486. this.selectionObj = {nodes: {}, edges: {}};
  14487. this._loadMixin(SelectionMixin);
  14488. },
  14489. /**
  14490. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  14491. *
  14492. * @private
  14493. */
  14494. _loadManipulationSystem: function () {
  14495. // reset global variables -- these are used by the selection of nodes and edges.
  14496. this.blockConnectingEdgeSelection = false;
  14497. this.forceAppendSelection = false;
  14498. if (this.constants.dataManipulation.enabled == true) {
  14499. // load the manipulator HTML elements. All styling done in css.
  14500. if (this.manipulationDiv === undefined) {
  14501. this.manipulationDiv = document.createElement('div');
  14502. this.manipulationDiv.className = 'graph-manipulationDiv';
  14503. this.manipulationDiv.id = 'graph-manipulationDiv';
  14504. if (this.editMode == true) {
  14505. this.manipulationDiv.style.display = "block";
  14506. }
  14507. else {
  14508. this.manipulationDiv.style.display = "none";
  14509. }
  14510. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  14511. }
  14512. if (this.editModeDiv === undefined) {
  14513. this.editModeDiv = document.createElement('div');
  14514. this.editModeDiv.className = 'graph-manipulation-editMode';
  14515. this.editModeDiv.id = 'graph-manipulation-editMode';
  14516. if (this.editMode == true) {
  14517. this.editModeDiv.style.display = "none";
  14518. }
  14519. else {
  14520. this.editModeDiv.style.display = "block";
  14521. }
  14522. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  14523. }
  14524. if (this.closeDiv === undefined) {
  14525. this.closeDiv = document.createElement('div');
  14526. this.closeDiv.className = 'graph-manipulation-closeDiv';
  14527. this.closeDiv.id = 'graph-manipulation-closeDiv';
  14528. this.closeDiv.style.display = this.manipulationDiv.style.display;
  14529. this.containerElement.insertBefore(this.closeDiv, this.frame);
  14530. }
  14531. // load the manipulation functions
  14532. this._loadMixin(manipulationMixin);
  14533. // create the manipulator toolbar
  14534. this._createManipulatorBar();
  14535. }
  14536. else {
  14537. if (this.manipulationDiv !== undefined) {
  14538. // removes all the bindings and overloads
  14539. this._createManipulatorBar();
  14540. // remove the manipulation divs
  14541. this.containerElement.removeChild(this.manipulationDiv);
  14542. this.containerElement.removeChild(this.editModeDiv);
  14543. this.containerElement.removeChild(this.closeDiv);
  14544. this.manipulationDiv = undefined;
  14545. this.editModeDiv = undefined;
  14546. this.closeDiv = undefined;
  14547. // remove the mixin functions
  14548. this._clearMixin(manipulationMixin);
  14549. }
  14550. }
  14551. },
  14552. /**
  14553. * Mixin the navigation (User Interface) system and initialize the parameters required
  14554. *
  14555. * @private
  14556. */
  14557. _loadNavigationControls: function () {
  14558. this._loadMixin(NavigationMixin);
  14559. // the clean function removes the button divs, this is done to remove the bindings.
  14560. this._cleanNavigation();
  14561. if (this.constants.navigation.enabled == true) {
  14562. this._loadNavigationElements();
  14563. }
  14564. },
  14565. /**
  14566. * Mixin the hierarchical layout system.
  14567. *
  14568. * @private
  14569. */
  14570. _loadHierarchySystem: function () {
  14571. this._loadMixin(HierarchicalLayoutMixin);
  14572. }
  14573. };
  14574. /**
  14575. * @constructor Graph
  14576. * Create a graph visualization, displaying nodes and edges.
  14577. *
  14578. * @param {Element} container The DOM element in which the Graph will
  14579. * be created. Normally a div element.
  14580. * @param {Object} data An object containing parameters
  14581. * {Array} nodes
  14582. * {Array} edges
  14583. * @param {Object} options Options
  14584. */
  14585. function Graph (container, data, options) {
  14586. this._initializeMixinLoaders();
  14587. // create variables and set default values
  14588. this.containerElement = container;
  14589. this.width = '100%';
  14590. this.height = '100%';
  14591. // render and calculation settings
  14592. this.renderRefreshRate = 60; // hz (fps)
  14593. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  14594. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  14595. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  14596. this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
  14597. this.stabilize = true; // stabilize before displaying the graph
  14598. this.selectable = true;
  14599. this.initializing = true;
  14600. // these functions are triggered when the dataset is edited
  14601. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  14602. // set constant values
  14603. this.constants = {
  14604. nodes: {
  14605. radiusMin: 5,
  14606. radiusMax: 20,
  14607. radius: 5,
  14608. shape: 'ellipse',
  14609. image: undefined,
  14610. widthMin: 16, // px
  14611. widthMax: 64, // px
  14612. fixed: false,
  14613. fontColor: 'black',
  14614. fontSize: 14, // px
  14615. fontFace: 'verdana',
  14616. level: -1,
  14617. color: {
  14618. border: '#2B7CE9',
  14619. background: '#97C2FC',
  14620. highlight: {
  14621. border: '#2B7CE9',
  14622. background: '#D2E5FF'
  14623. }
  14624. },
  14625. borderColor: '#2B7CE9',
  14626. backgroundColor: '#97C2FC',
  14627. highlightColor: '#D2E5FF',
  14628. group: undefined
  14629. },
  14630. edges: {
  14631. widthMin: 1,
  14632. widthMax: 15,
  14633. width: 1,
  14634. style: 'line',
  14635. color: {
  14636. color:'#848484',
  14637. highlight:'#848484'
  14638. },
  14639. fontColor: '#343434',
  14640. fontSize: 14, // px
  14641. fontFace: 'arial',
  14642. fontFill: 'white',
  14643. arrowScaleFactor: 1,
  14644. dash: {
  14645. length: 10,
  14646. gap: 5,
  14647. altLength: undefined
  14648. }
  14649. },
  14650. configurePhysics:false,
  14651. physics: {
  14652. barnesHut: {
  14653. enabled: true,
  14654. theta: 1 / 0.6, // inverted to save time during calculation
  14655. gravitationalConstant: -2000,
  14656. centralGravity: 0.3,
  14657. springLength: 95,
  14658. springConstant: 0.04,
  14659. damping: 0.09
  14660. },
  14661. repulsion: {
  14662. centralGravity: 0.1,
  14663. springLength: 200,
  14664. springConstant: 0.05,
  14665. nodeDistance: 100,
  14666. damping: 0.09
  14667. },
  14668. hierarchicalRepulsion: {
  14669. enabled: false,
  14670. centralGravity: 0.0,
  14671. springLength: 100,
  14672. springConstant: 0.01,
  14673. nodeDistance: 60,
  14674. damping: 0.09
  14675. },
  14676. damping: null,
  14677. centralGravity: null,
  14678. springLength: null,
  14679. springConstant: null
  14680. },
  14681. clustering: { // Per Node in Cluster = PNiC
  14682. enabled: false, // (Boolean) | global on/off switch for clustering.
  14683. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  14684. clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
  14685. reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
  14686. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  14687. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  14688. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  14689. screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
  14690. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  14691. maxFontSize: 1000,
  14692. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  14693. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  14694. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  14695. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  14696. height: 1, // (px PNiC) | growth of the height per node in cluster.
  14697. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  14698. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  14699. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  14700. clusterLevelDifference: 2
  14701. },
  14702. navigation: {
  14703. enabled: false
  14704. },
  14705. keyboard: {
  14706. enabled: false,
  14707. speed: {x: 10, y: 10, zoom: 0.02}
  14708. },
  14709. dataManipulation: {
  14710. enabled: false,
  14711. initiallyVisible: false
  14712. },
  14713. hierarchicalLayout: {
  14714. enabled:false,
  14715. levelSeparation: 150,
  14716. nodeSpacing: 100,
  14717. direction: "UD" // UD, DU, LR, RL
  14718. },
  14719. freezeForStabilization: false,
  14720. smoothCurves: true,
  14721. maxVelocity: 10,
  14722. minVelocity: 0.1, // px/s
  14723. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  14724. labels:{
  14725. add:"Add Node",
  14726. edit:"Edit",
  14727. link:"Add Link",
  14728. del:"Delete selected",
  14729. editNode:"Edit Node",
  14730. back:"Back",
  14731. addDescription:"Click in an empty space to place a new node.",
  14732. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  14733. addError:"The function for add does not support two arguments (data,callback).",
  14734. linkError:"The function for connect does not support two arguments (data,callback).",
  14735. editError:"The function for edit does not support two arguments (data, callback).",
  14736. editBoundError:"No edit function has been bound to this button.",
  14737. deleteError:"The function for delete does not support two arguments (data, callback).",
  14738. deleteClusterError:"Clusters cannot be deleted."
  14739. },
  14740. tooltip: {
  14741. delay: 300,
  14742. fontColor: 'black',
  14743. fontSize: 14, // px
  14744. fontFace: 'verdana',
  14745. color: {
  14746. border: '#666',
  14747. background: '#FFFFC6'
  14748. }
  14749. },
  14750. moveable: true,
  14751. zoomable: true
  14752. };
  14753. this.editMode = this.constants.dataManipulation.initiallyVisible;
  14754. // Node variables
  14755. var graph = this;
  14756. this.groups = new Groups(); // object with groups
  14757. this.images = new Images(); // object with images
  14758. this.images.setOnloadCallback(function () {
  14759. graph._redraw();
  14760. });
  14761. // keyboard navigation variables
  14762. this.xIncrement = 0;
  14763. this.yIncrement = 0;
  14764. this.zoomIncrement = 0;
  14765. // loading all the mixins:
  14766. // load the force calculation functions, grouped under the physics system.
  14767. this._loadPhysicsSystem();
  14768. // create a frame and canvas
  14769. this._create();
  14770. // load the sector system. (mandatory, fully integrated with Graph)
  14771. this._loadSectorSystem();
  14772. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  14773. this._loadClusterSystem();
  14774. // load the selection system. (mandatory, required by Graph)
  14775. this._loadSelectionSystem();
  14776. // load the selection system. (mandatory, required by Graph)
  14777. this._loadHierarchySystem();
  14778. // apply options
  14779. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14780. this._setScale(1);
  14781. this.setOptions(options);
  14782. // other vars
  14783. this.freezeSimulation = false;// freeze the simulation
  14784. this.cachedFunctions = {};
  14785. // containers for nodes and edges
  14786. this.calculationNodes = {};
  14787. this.calculationNodeIndices = [];
  14788. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  14789. this.nodes = {}; // object with Node objects
  14790. this.edges = {}; // object with Edge objects
  14791. // position and scale variables and objects
  14792. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  14793. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14794. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14795. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  14796. this.scale = 1; // defining the global scale variable in the constructor
  14797. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  14798. // datasets or dataviews
  14799. this.nodesData = null; // A DataSet or DataView
  14800. this.edgesData = null; // A DataSet or DataView
  14801. // create event listeners used to subscribe on the DataSets of the nodes and edges
  14802. this.nodesListeners = {
  14803. 'add': function (event, params) {
  14804. graph._addNodes(params.items);
  14805. graph.start();
  14806. },
  14807. 'update': function (event, params) {
  14808. graph._updateNodes(params.items);
  14809. graph.start();
  14810. },
  14811. 'remove': function (event, params) {
  14812. graph._removeNodes(params.items);
  14813. graph.start();
  14814. }
  14815. };
  14816. this.edgesListeners = {
  14817. 'add': function (event, params) {
  14818. graph._addEdges(params.items);
  14819. graph.start();
  14820. },
  14821. 'update': function (event, params) {
  14822. graph._updateEdges(params.items);
  14823. graph.start();
  14824. },
  14825. 'remove': function (event, params) {
  14826. graph._removeEdges(params.items);
  14827. graph.start();
  14828. }
  14829. };
  14830. // properties for the animation
  14831. this.moving = true;
  14832. this.timer = undefined; // Scheduling function. Is definded in this.start();
  14833. // load data (the disable start variable will be the same as the enabled clustering)
  14834. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  14835. // hierarchical layout
  14836. this.initializing = false;
  14837. if (this.constants.hierarchicalLayout.enabled == true) {
  14838. this._setupHierarchicalLayout();
  14839. }
  14840. else {
  14841. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  14842. if (this.stabilize == false) {
  14843. this.zoomExtent(true,this.constants.clustering.enabled);
  14844. }
  14845. }
  14846. // if clustering is disabled, the simulation will have started in the setData function
  14847. if (this.constants.clustering.enabled) {
  14848. this.startWithClustering();
  14849. }
  14850. }
  14851. // Extend Graph with an Emitter mixin
  14852. Emitter(Graph.prototype);
  14853. /**
  14854. * Get the script path where the vis.js library is located
  14855. *
  14856. * @returns {string | null} path Path or null when not found. Path does not
  14857. * end with a slash.
  14858. * @private
  14859. */
  14860. Graph.prototype._getScriptPath = function() {
  14861. var scripts = document.getElementsByTagName( 'script' );
  14862. // find script named vis.js or vis.min.js
  14863. for (var i = 0; i < scripts.length; i++) {
  14864. var src = scripts[i].src;
  14865. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  14866. if (match) {
  14867. // return path without the script name
  14868. return src.substring(0, src.length - match[0].length);
  14869. }
  14870. }
  14871. return null;
  14872. };
  14873. /**
  14874. * Find the center position of the graph
  14875. * @private
  14876. */
  14877. Graph.prototype._getRange = function() {
  14878. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  14879. for (var nodeId in this.nodes) {
  14880. if (this.nodes.hasOwnProperty(nodeId)) {
  14881. node = this.nodes[nodeId];
  14882. if (minX > (node.x)) {minX = node.x;}
  14883. if (maxX < (node.x)) {maxX = node.x;}
  14884. if (minY > (node.y)) {minY = node.y;}
  14885. if (maxY < (node.y)) {maxY = node.y;}
  14886. }
  14887. }
  14888. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  14889. minY = 0, maxY = 0, minX = 0, maxX = 0;
  14890. }
  14891. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14892. };
  14893. /**
  14894. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14895. * @returns {{x: number, y: number}}
  14896. * @private
  14897. */
  14898. Graph.prototype._findCenter = function(range) {
  14899. return {x: (0.5 * (range.maxX + range.minX)),
  14900. y: (0.5 * (range.maxY + range.minY))};
  14901. };
  14902. /**
  14903. * center the graph
  14904. *
  14905. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14906. */
  14907. Graph.prototype._centerGraph = function(range) {
  14908. var center = this._findCenter(range);
  14909. center.x *= this.scale;
  14910. center.y *= this.scale;
  14911. center.x -= 0.5 * this.frame.canvas.clientWidth;
  14912. center.y -= 0.5 * this.frame.canvas.clientHeight;
  14913. this._setTranslation(-center.x,-center.y); // set at 0,0
  14914. };
  14915. /**
  14916. * This function zooms out to fit all data on screen based on amount of nodes
  14917. *
  14918. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  14919. * @param {Boolean} [disableStart] | If true, start is not called.
  14920. */
  14921. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  14922. if (initialZoom === undefined) {
  14923. initialZoom = false;
  14924. }
  14925. if (disableStart === undefined) {
  14926. disableStart = false;
  14927. }
  14928. var range = this._getRange();
  14929. var zoomLevel;
  14930. if (initialZoom == true) {
  14931. var numberOfNodes = this.nodeIndices.length;
  14932. if (this.constants.smoothCurves == true) {
  14933. if (this.constants.clustering.enabled == true &&
  14934. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14935. zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14936. }
  14937. else {
  14938. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14939. }
  14940. }
  14941. else {
  14942. if (this.constants.clustering.enabled == true &&
  14943. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14944. zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14945. }
  14946. else {
  14947. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14948. }
  14949. }
  14950. // correct for larger canvasses.
  14951. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  14952. zoomLevel *= factor;
  14953. }
  14954. else {
  14955. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  14956. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  14957. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  14958. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  14959. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  14960. }
  14961. if (zoomLevel > 1.0) {
  14962. zoomLevel = 1.0;
  14963. }
  14964. this._setScale(zoomLevel);
  14965. this._centerGraph(range);
  14966. if (disableStart == false) {
  14967. this.moving = true;
  14968. this.start();
  14969. }
  14970. };
  14971. /**
  14972. * Update the this.nodeIndices with the most recent node index list
  14973. * @private
  14974. */
  14975. Graph.prototype._updateNodeIndexList = function() {
  14976. this._clearNodeIndexList();
  14977. for (var idx in this.nodes) {
  14978. if (this.nodes.hasOwnProperty(idx)) {
  14979. this.nodeIndices.push(idx);
  14980. }
  14981. }
  14982. };
  14983. /**
  14984. * Set nodes and edges, and optionally options as well.
  14985. *
  14986. * @param {Object} data Object containing parameters:
  14987. * {Array | DataSet | DataView} [nodes] Array with nodes
  14988. * {Array | DataSet | DataView} [edges] Array with edges
  14989. * {String} [dot] String containing data in DOT format
  14990. * {Options} [options] Object with options
  14991. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  14992. */
  14993. Graph.prototype.setData = function(data, disableStart) {
  14994. if (disableStart === undefined) {
  14995. disableStart = false;
  14996. }
  14997. if (data && data.dot && (data.nodes || data.edges)) {
  14998. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14999. ' parameter pair "nodes" and "edges", but not both.');
  15000. }
  15001. // set options
  15002. this.setOptions(data && data.options);
  15003. // set all data
  15004. if (data && data.dot) {
  15005. // parse DOT file
  15006. if(data && data.dot) {
  15007. var dotData = vis.util.DOTToGraph(data.dot);
  15008. this.setData(dotData);
  15009. return;
  15010. }
  15011. }
  15012. else {
  15013. this._setNodes(data && data.nodes);
  15014. this._setEdges(data && data.edges);
  15015. }
  15016. this._putDataInSector();
  15017. if (!disableStart) {
  15018. // find a stable position or start animating to a stable position
  15019. if (this.stabilize) {
  15020. var me = this;
  15021. setTimeout(function() {me._stabilize(); me.start();},0)
  15022. }
  15023. else {
  15024. this.start();
  15025. }
  15026. }
  15027. };
  15028. /**
  15029. * Set options
  15030. * @param {Object} options
  15031. * @param {Boolean} [initializeView] | set zoom and translation to default.
  15032. */
  15033. Graph.prototype.setOptions = function (options) {
  15034. if (options) {
  15035. var prop;
  15036. // retrieve parameter values
  15037. if (options.width !== undefined) {this.width = options.width;}
  15038. if (options.height !== undefined) {this.height = options.height;}
  15039. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  15040. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  15041. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  15042. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  15043. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  15044. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  15045. if (options.moveable !== undefined) {this.constants.moveable = options.moveable;}
  15046. if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
  15047. if (options.labels !== undefined) {
  15048. for (prop in options.labels) {
  15049. if (options.labels.hasOwnProperty(prop)) {
  15050. this.constants.labels[prop] = options.labels[prop];
  15051. }
  15052. }
  15053. }
  15054. if (options.onAdd) {
  15055. this.triggerFunctions.add = options.onAdd;
  15056. }
  15057. if (options.onEdit) {
  15058. this.triggerFunctions.edit = options.onEdit;
  15059. }
  15060. if (options.onConnect) {
  15061. this.triggerFunctions.connect = options.onConnect;
  15062. }
  15063. if (options.onDelete) {
  15064. this.triggerFunctions.del = options.onDelete;
  15065. }
  15066. if (options.physics) {
  15067. if (options.physics.barnesHut) {
  15068. this.constants.physics.barnesHut.enabled = true;
  15069. for (prop in options.physics.barnesHut) {
  15070. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  15071. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  15072. }
  15073. }
  15074. }
  15075. if (options.physics.repulsion) {
  15076. this.constants.physics.barnesHut.enabled = false;
  15077. for (prop in options.physics.repulsion) {
  15078. if (options.physics.repulsion.hasOwnProperty(prop)) {
  15079. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  15080. }
  15081. }
  15082. }
  15083. if (options.physics.hierarchicalRepulsion) {
  15084. this.constants.hierarchicalLayout.enabled = true;
  15085. this.constants.physics.hierarchicalRepulsion.enabled = true;
  15086. this.constants.physics.barnesHut.enabled = false;
  15087. for (prop in options.physics.hierarchicalRepulsion) {
  15088. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  15089. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  15090. }
  15091. }
  15092. }
  15093. }
  15094. if (options.hierarchicalLayout) {
  15095. this.constants.hierarchicalLayout.enabled = true;
  15096. for (prop in options.hierarchicalLayout) {
  15097. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  15098. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  15099. }
  15100. }
  15101. }
  15102. else if (options.hierarchicalLayout !== undefined) {
  15103. this.constants.hierarchicalLayout.enabled = false;
  15104. }
  15105. if (options.clustering) {
  15106. this.constants.clustering.enabled = true;
  15107. for (prop in options.clustering) {
  15108. if (options.clustering.hasOwnProperty(prop)) {
  15109. this.constants.clustering[prop] = options.clustering[prop];
  15110. }
  15111. }
  15112. }
  15113. else if (options.clustering !== undefined) {
  15114. this.constants.clustering.enabled = false;
  15115. }
  15116. if (options.navigation) {
  15117. this.constants.navigation.enabled = true;
  15118. for (prop in options.navigation) {
  15119. if (options.navigation.hasOwnProperty(prop)) {
  15120. this.constants.navigation[prop] = options.navigation[prop];
  15121. }
  15122. }
  15123. }
  15124. else if (options.navigation !== undefined) {
  15125. this.constants.navigation.enabled = false;
  15126. }
  15127. if (options.keyboard) {
  15128. this.constants.keyboard.enabled = true;
  15129. for (prop in options.keyboard) {
  15130. if (options.keyboard.hasOwnProperty(prop)) {
  15131. this.constants.keyboard[prop] = options.keyboard[prop];
  15132. }
  15133. }
  15134. }
  15135. else if (options.keyboard !== undefined) {
  15136. this.constants.keyboard.enabled = false;
  15137. }
  15138. if (options.dataManipulation) {
  15139. this.constants.dataManipulation.enabled = true;
  15140. for (prop in options.dataManipulation) {
  15141. if (options.dataManipulation.hasOwnProperty(prop)) {
  15142. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  15143. }
  15144. }
  15145. }
  15146. else if (options.dataManipulation !== undefined) {
  15147. this.constants.dataManipulation.enabled = false;
  15148. }
  15149. // TODO: work out these options and document them
  15150. if (options.edges) {
  15151. for (prop in options.edges) {
  15152. if (options.edges.hasOwnProperty(prop)) {
  15153. if (typeof options.edges[prop] != "object") {
  15154. this.constants.edges[prop] = options.edges[prop];
  15155. }
  15156. }
  15157. }
  15158. if (options.edges.color !== undefined) {
  15159. if (util.isString(options.edges.color)) {
  15160. this.constants.edges.color = {};
  15161. this.constants.edges.color.color = options.edges.color;
  15162. this.constants.edges.color.highlight = options.edges.color;
  15163. }
  15164. else {
  15165. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  15166. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  15167. }
  15168. }
  15169. if (!options.edges.fontColor) {
  15170. if (options.edges.color !== undefined) {
  15171. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  15172. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  15173. }
  15174. }
  15175. // Added to support dashed lines
  15176. // David Jordan
  15177. // 2012-08-08
  15178. if (options.edges.dash) {
  15179. if (options.edges.dash.length !== undefined) {
  15180. this.constants.edges.dash.length = options.edges.dash.length;
  15181. }
  15182. if (options.edges.dash.gap !== undefined) {
  15183. this.constants.edges.dash.gap = options.edges.dash.gap;
  15184. }
  15185. if (options.edges.dash.altLength !== undefined) {
  15186. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  15187. }
  15188. }
  15189. }
  15190. if (options.nodes) {
  15191. for (prop in options.nodes) {
  15192. if (options.nodes.hasOwnProperty(prop)) {
  15193. this.constants.nodes[prop] = options.nodes[prop];
  15194. }
  15195. }
  15196. if (options.nodes.color) {
  15197. this.constants.nodes.color = util.parseColor(options.nodes.color);
  15198. }
  15199. /*
  15200. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  15201. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  15202. */
  15203. }
  15204. if (options.groups) {
  15205. for (var groupname in options.groups) {
  15206. if (options.groups.hasOwnProperty(groupname)) {
  15207. var group = options.groups[groupname];
  15208. this.groups.add(groupname, group);
  15209. }
  15210. }
  15211. }
  15212. if (options.tooltip) {
  15213. for (prop in options.tooltip) {
  15214. if (options.tooltip.hasOwnProperty(prop)) {
  15215. this.constants.tooltip[prop] = options.tooltip[prop];
  15216. }
  15217. }
  15218. if (options.tooltip.color) {
  15219. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  15220. }
  15221. }
  15222. }
  15223. // (Re)loading the mixins that can be enabled or disabled in the options.
  15224. // load the force calculation functions, grouped under the physics system.
  15225. this._loadPhysicsSystem();
  15226. // load the navigation system.
  15227. this._loadNavigationControls();
  15228. // load the data manipulation system
  15229. this._loadManipulationSystem();
  15230. // configure the smooth curves
  15231. this._configureSmoothCurves();
  15232. // bind keys. If disabled, this will not do anything;
  15233. this._createKeyBinds();
  15234. this.setSize(this.width, this.height);
  15235. this.moving = true;
  15236. this.start();
  15237. };
  15238. /**
  15239. * Create the main frame for the Graph.
  15240. * This function is executed once when a Graph object is created. The frame
  15241. * contains a canvas, and this canvas contains all objects like the axis and
  15242. * nodes.
  15243. * @private
  15244. */
  15245. Graph.prototype._create = function () {
  15246. // remove all elements from the container element.
  15247. while (this.containerElement.hasChildNodes()) {
  15248. this.containerElement.removeChild(this.containerElement.firstChild);
  15249. }
  15250. this.frame = document.createElement('div');
  15251. this.frame.className = 'graph-frame';
  15252. this.frame.style.position = 'relative';
  15253. this.frame.style.overflow = 'hidden';
  15254. // create the graph canvas (HTML canvas element)
  15255. this.frame.canvas = document.createElement( 'canvas' );
  15256. this.frame.canvas.style.position = 'relative';
  15257. this.frame.appendChild(this.frame.canvas);
  15258. if (!this.frame.canvas.getContext) {
  15259. var noCanvas = document.createElement( 'DIV' );
  15260. noCanvas.style.color = 'red';
  15261. noCanvas.style.fontWeight = 'bold' ;
  15262. noCanvas.style.padding = '10px';
  15263. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  15264. this.frame.canvas.appendChild(noCanvas);
  15265. }
  15266. var me = this;
  15267. this.drag = {};
  15268. this.pinch = {};
  15269. this.hammer = Hammer(this.frame.canvas, {
  15270. prevent_default: true
  15271. });
  15272. this.hammer.on('tap', me._onTap.bind(me) );
  15273. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  15274. this.hammer.on('hold', me._onHold.bind(me) );
  15275. this.hammer.on('pinch', me._onPinch.bind(me) );
  15276. this.hammer.on('touch', me._onTouch.bind(me) );
  15277. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  15278. this.hammer.on('drag', me._onDrag.bind(me) );
  15279. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  15280. this.hammer.on('release', me._onRelease.bind(me) );
  15281. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  15282. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  15283. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  15284. // add the frame to the container element
  15285. this.containerElement.appendChild(this.frame);
  15286. };
  15287. /**
  15288. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  15289. * @private
  15290. */
  15291. Graph.prototype._createKeyBinds = function() {
  15292. var me = this;
  15293. this.mousetrap = mousetrap;
  15294. this.mousetrap.reset();
  15295. if (this.constants.keyboard.enabled == true) {
  15296. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  15297. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  15298. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  15299. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  15300. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  15301. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  15302. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  15303. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  15304. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  15305. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  15306. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  15307. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  15308. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  15309. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  15310. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  15311. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  15312. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  15313. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  15314. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  15315. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  15316. }
  15317. if (this.constants.dataManipulation.enabled == true) {
  15318. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  15319. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  15320. }
  15321. };
  15322. /**
  15323. * Get the pointer location from a touch location
  15324. * @param {{pageX: Number, pageY: Number}} touch
  15325. * @return {{x: Number, y: Number}} pointer
  15326. * @private
  15327. */
  15328. Graph.prototype._getPointer = function (touch) {
  15329. return {
  15330. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  15331. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  15332. };
  15333. };
  15334. /**
  15335. * On start of a touch gesture, store the pointer
  15336. * @param event
  15337. * @private
  15338. */
  15339. Graph.prototype._onTouch = function (event) {
  15340. this.drag.pointer = this._getPointer(event.gesture.center);
  15341. this.drag.pinched = false;
  15342. this.pinch.scale = this._getScale();
  15343. this._handleTouch(this.drag.pointer);
  15344. };
  15345. /**
  15346. * handle drag start event
  15347. * @private
  15348. */
  15349. Graph.prototype._onDragStart = function () {
  15350. this._handleDragStart();
  15351. };
  15352. /**
  15353. * This function is called by _onDragStart.
  15354. * It is separated out because we can then overload it for the datamanipulation system.
  15355. *
  15356. * @private
  15357. */
  15358. Graph.prototype._handleDragStart = function() {
  15359. var drag = this.drag;
  15360. var node = this._getNodeAt(drag.pointer);
  15361. // note: drag.pointer is set in _onTouch to get the initial touch location
  15362. drag.dragging = true;
  15363. drag.selection = [];
  15364. drag.translation = this._getTranslation();
  15365. drag.nodeId = null;
  15366. if (node != null) {
  15367. drag.nodeId = node.id;
  15368. // select the clicked node if not yet selected
  15369. if (!node.isSelected()) {
  15370. this._selectObject(node,false);
  15371. }
  15372. // create an array with the selected nodes and their original location and status
  15373. for (var objectId in this.selectionObj.nodes) {
  15374. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  15375. var object = this.selectionObj.nodes[objectId];
  15376. var s = {
  15377. id: object.id,
  15378. node: object,
  15379. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  15380. x: object.x,
  15381. y: object.y,
  15382. xFixed: object.xFixed,
  15383. yFixed: object.yFixed
  15384. };
  15385. object.xFixed = true;
  15386. object.yFixed = true;
  15387. drag.selection.push(s);
  15388. }
  15389. }
  15390. }
  15391. };
  15392. /**
  15393. * handle drag event
  15394. * @private
  15395. */
  15396. Graph.prototype._onDrag = function (event) {
  15397. this._handleOnDrag(event)
  15398. };
  15399. /**
  15400. * This function is called by _onDrag.
  15401. * It is separated out because we can then overload it for the datamanipulation system.
  15402. *
  15403. * @private
  15404. */
  15405. Graph.prototype._handleOnDrag = function(event) {
  15406. if (this.drag.pinched) {
  15407. return;
  15408. }
  15409. var pointer = this._getPointer(event.gesture.center);
  15410. var me = this,
  15411. drag = this.drag,
  15412. selection = drag.selection;
  15413. if (selection && selection.length) {
  15414. // calculate delta's and new location
  15415. var deltaX = pointer.x - drag.pointer.x,
  15416. deltaY = pointer.y - drag.pointer.y;
  15417. // update position of all selected nodes
  15418. selection.forEach(function (s) {
  15419. var node = s.node;
  15420. if (!s.xFixed) {
  15421. node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
  15422. }
  15423. if (!s.yFixed) {
  15424. node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
  15425. }
  15426. });
  15427. // start _animationStep if not yet running
  15428. if (!this.moving) {
  15429. this.moving = true;
  15430. this.start();
  15431. }
  15432. }
  15433. else {
  15434. if (this.constants.moveable == true) {
  15435. // move the graph
  15436. var diffX = pointer.x - this.drag.pointer.x;
  15437. var diffY = pointer.y - this.drag.pointer.y;
  15438. this._setTranslation(
  15439. this.drag.translation.x + diffX,
  15440. this.drag.translation.y + diffY);
  15441. this._redraw();
  15442. this.moving = true;
  15443. this.start();
  15444. }
  15445. }
  15446. };
  15447. /**
  15448. * handle drag start event
  15449. * @private
  15450. */
  15451. Graph.prototype._onDragEnd = function () {
  15452. this.drag.dragging = false;
  15453. var selection = this.drag.selection;
  15454. if (selection) {
  15455. selection.forEach(function (s) {
  15456. // restore original xFixed and yFixed
  15457. s.node.xFixed = s.xFixed;
  15458. s.node.yFixed = s.yFixed;
  15459. });
  15460. }
  15461. };
  15462. /**
  15463. * handle tap/click event: select/unselect a node
  15464. * @private
  15465. */
  15466. Graph.prototype._onTap = function (event) {
  15467. var pointer = this._getPointer(event.gesture.center);
  15468. this.pointerPosition = pointer;
  15469. this._handleTap(pointer);
  15470. };
  15471. /**
  15472. * handle doubletap event
  15473. * @private
  15474. */
  15475. Graph.prototype._onDoubleTap = function (event) {
  15476. var pointer = this._getPointer(event.gesture.center);
  15477. this._handleDoubleTap(pointer);
  15478. };
  15479. /**
  15480. * handle long tap event: multi select nodes
  15481. * @private
  15482. */
  15483. Graph.prototype._onHold = function (event) {
  15484. var pointer = this._getPointer(event.gesture.center);
  15485. this.pointerPosition = pointer;
  15486. this._handleOnHold(pointer);
  15487. };
  15488. /**
  15489. * handle the release of the screen
  15490. *
  15491. * @private
  15492. */
  15493. Graph.prototype._onRelease = function (event) {
  15494. var pointer = this._getPointer(event.gesture.center);
  15495. this._handleOnRelease(pointer);
  15496. };
  15497. /**
  15498. * Handle pinch event
  15499. * @param event
  15500. * @private
  15501. */
  15502. Graph.prototype._onPinch = function (event) {
  15503. var pointer = this._getPointer(event.gesture.center);
  15504. this.drag.pinched = true;
  15505. if (!('scale' in this.pinch)) {
  15506. this.pinch.scale = 1;
  15507. }
  15508. // TODO: enabled moving while pinching?
  15509. var scale = this.pinch.scale * event.gesture.scale;
  15510. this._zoom(scale, pointer)
  15511. };
  15512. /**
  15513. * Zoom the graph in or out
  15514. * @param {Number} scale a number around 1, and between 0.01 and 10
  15515. * @param {{x: Number, y: Number}} pointer Position on screen
  15516. * @return {Number} appliedScale scale is limited within the boundaries
  15517. * @private
  15518. */
  15519. Graph.prototype._zoom = function(scale, pointer) {
  15520. console.log(pointer);
  15521. if (this.constants.zoomable == true) {
  15522. var scaleOld = this._getScale();
  15523. if (scale < 0.00001) {
  15524. scale = 0.00001;
  15525. }
  15526. if (scale > 10) {
  15527. scale = 10;
  15528. }
  15529. // + this.frame.canvas.clientHeight / 2
  15530. var translation = this._getTranslation();
  15531. var scaleFrac = scale / scaleOld;
  15532. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  15533. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  15534. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  15535. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  15536. this._setScale(scale);
  15537. this._setTranslation(tx, ty);
  15538. this.updateClustersDefault();
  15539. this._redraw();
  15540. if (scaleOld < scale) {
  15541. this.emit("zoom", {direction:"+"});
  15542. }
  15543. else {
  15544. this.emit("zoom", {direction:"-"});
  15545. }
  15546. return scale;
  15547. }
  15548. };
  15549. /**
  15550. * Event handler for mouse wheel event, used to zoom the timeline
  15551. * See http://adomas.org/javascript-mouse-wheel/
  15552. * https://github.com/EightMedia/hammer.js/issues/256
  15553. * @param {MouseEvent} event
  15554. * @private
  15555. */
  15556. Graph.prototype._onMouseWheel = function(event) {
  15557. // retrieve delta
  15558. var delta = 0;
  15559. if (event.wheelDelta) { /* IE/Opera. */
  15560. delta = event.wheelDelta/120;
  15561. } else if (event.detail) { /* Mozilla case. */
  15562. // In Mozilla, sign of delta is different than in IE.
  15563. // Also, delta is multiple of 3.
  15564. delta = -event.detail/3;
  15565. }
  15566. // If delta is nonzero, handle it.
  15567. // Basically, delta is now positive if wheel was scrolled up,
  15568. // and negative, if wheel was scrolled down.
  15569. if (delta) {
  15570. // calculate the new scale
  15571. var scale = this._getScale();
  15572. var zoom = delta / 10;
  15573. if (delta < 0) {
  15574. zoom = zoom / (1 - zoom);
  15575. }
  15576. scale *= (1 + zoom);
  15577. // calculate the pointer location
  15578. var gesture = util.fakeGesture(this, event);
  15579. var pointer = this._getPointer(gesture.center);
  15580. // apply the new scale
  15581. this._zoom(scale, pointer);
  15582. }
  15583. // Prevent default actions caused by mouse wheel.
  15584. event.preventDefault();
  15585. };
  15586. /**
  15587. * Mouse move handler for checking whether the title moves over a node with a title.
  15588. * @param {Event} event
  15589. * @private
  15590. */
  15591. Graph.prototype._onMouseMoveTitle = function (event) {
  15592. var gesture = util.fakeGesture(this, event);
  15593. var pointer = this._getPointer(gesture.center);
  15594. // check if the previously selected node is still selected
  15595. if (this.popupNode) {
  15596. this._checkHidePopup(pointer);
  15597. }
  15598. // start a timeout that will check if the mouse is positioned above
  15599. // an element
  15600. var me = this;
  15601. var checkShow = function() {
  15602. me._checkShowPopup(pointer);
  15603. };
  15604. if (this.popupTimer) {
  15605. clearInterval(this.popupTimer); // stop any running calculationTimer
  15606. }
  15607. if (!this.drag.dragging) {
  15608. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  15609. }
  15610. };
  15611. /**
  15612. * Check if there is an element on the given position in the graph
  15613. * (a node or edge). If so, and if this element has a title,
  15614. * show a popup window with its title.
  15615. *
  15616. * @param {{x:Number, y:Number}} pointer
  15617. * @private
  15618. */
  15619. Graph.prototype._checkShowPopup = function (pointer) {
  15620. var obj = {
  15621. left: this._XconvertDOMtoCanvas(pointer.x),
  15622. top: this._YconvertDOMtoCanvas(pointer.y),
  15623. right: this._XconvertDOMtoCanvas(pointer.x),
  15624. bottom: this._YconvertDOMtoCanvas(pointer.y)
  15625. };
  15626. var id;
  15627. var lastPopupNode = this.popupNode;
  15628. if (this.popupNode == undefined) {
  15629. // search the nodes for overlap, select the top one in case of multiple nodes
  15630. var nodes = this.nodes;
  15631. for (id in nodes) {
  15632. if (nodes.hasOwnProperty(id)) {
  15633. var node = nodes[id];
  15634. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  15635. this.popupNode = node;
  15636. break;
  15637. }
  15638. }
  15639. }
  15640. }
  15641. if (this.popupNode === undefined) {
  15642. // search the edges for overlap
  15643. var edges = this.edges;
  15644. for (id in edges) {
  15645. if (edges.hasOwnProperty(id)) {
  15646. var edge = edges[id];
  15647. if (edge.connected && (edge.getTitle() !== undefined) &&
  15648. edge.isOverlappingWith(obj)) {
  15649. this.popupNode = edge;
  15650. break;
  15651. }
  15652. }
  15653. }
  15654. }
  15655. if (this.popupNode) {
  15656. // show popup message window
  15657. if (this.popupNode != lastPopupNode) {
  15658. var me = this;
  15659. if (!me.popup) {
  15660. me.popup = new Popup(me.frame, me.constants.tooltip);
  15661. }
  15662. // adjust a small offset such that the mouse cursor is located in the
  15663. // bottom left location of the popup, and you can easily move over the
  15664. // popup area
  15665. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  15666. me.popup.setText(me.popupNode.getTitle());
  15667. me.popup.show();
  15668. }
  15669. }
  15670. else {
  15671. if (this.popup) {
  15672. this.popup.hide();
  15673. }
  15674. }
  15675. };
  15676. /**
  15677. * Check if the popup must be hided, which is the case when the mouse is no
  15678. * longer hovering on the object
  15679. * @param {{x:Number, y:Number}} pointer
  15680. * @private
  15681. */
  15682. Graph.prototype._checkHidePopup = function (pointer) {
  15683. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  15684. this.popupNode = undefined;
  15685. if (this.popup) {
  15686. this.popup.hide();
  15687. }
  15688. }
  15689. };
  15690. /**
  15691. * Set a new size for the graph
  15692. * @param {string} width Width in pixels or percentage (for example '800px'
  15693. * or '50%')
  15694. * @param {string} height Height in pixels or percentage (for example '400px'
  15695. * or '30%')
  15696. */
  15697. Graph.prototype.setSize = function(width, height) {
  15698. this.frame.style.width = width;
  15699. this.frame.style.height = height;
  15700. this.frame.canvas.style.width = '100%';
  15701. this.frame.canvas.style.height = '100%';
  15702. this.frame.canvas.width = this.frame.canvas.clientWidth;
  15703. this.frame.canvas.height = this.frame.canvas.clientHeight;
  15704. if (this.manipulationDiv !== undefined) {
  15705. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  15706. }
  15707. if (this.navigationDivs !== undefined) {
  15708. if (this.navigationDivs['wrapper'] !== undefined) {
  15709. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  15710. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  15711. }
  15712. }
  15713. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  15714. };
  15715. /**
  15716. * Set a data set with nodes for the graph
  15717. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  15718. * @private
  15719. */
  15720. Graph.prototype._setNodes = function(nodes) {
  15721. var oldNodesData = this.nodesData;
  15722. if (nodes instanceof DataSet || nodes instanceof DataView) {
  15723. this.nodesData = nodes;
  15724. }
  15725. else if (nodes instanceof Array) {
  15726. this.nodesData = new DataSet();
  15727. this.nodesData.add(nodes);
  15728. }
  15729. else if (!nodes) {
  15730. this.nodesData = new DataSet();
  15731. }
  15732. else {
  15733. throw new TypeError('Array or DataSet expected');
  15734. }
  15735. if (oldNodesData) {
  15736. // unsubscribe from old dataset
  15737. util.forEach(this.nodesListeners, function (callback, event) {
  15738. oldNodesData.off(event, callback);
  15739. });
  15740. }
  15741. // remove drawn nodes
  15742. this.nodes = {};
  15743. if (this.nodesData) {
  15744. // subscribe to new dataset
  15745. var me = this;
  15746. util.forEach(this.nodesListeners, function (callback, event) {
  15747. me.nodesData.on(event, callback);
  15748. });
  15749. // draw all new nodes
  15750. var ids = this.nodesData.getIds();
  15751. this._addNodes(ids);
  15752. }
  15753. this._updateSelection();
  15754. };
  15755. /**
  15756. * Add nodes
  15757. * @param {Number[] | String[]} ids
  15758. * @private
  15759. */
  15760. Graph.prototype._addNodes = function(ids) {
  15761. var id;
  15762. for (var i = 0, len = ids.length; i < len; i++) {
  15763. id = ids[i];
  15764. var data = this.nodesData.get(id);
  15765. var node = new Node(data, this.images, this.groups, this.constants);
  15766. this.nodes[id] = node; // note: this may replace an existing node
  15767. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  15768. var radius = 10 * 0.1*ids.length;
  15769. var angle = 2 * Math.PI * Math.random();
  15770. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15771. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15772. }
  15773. this.moving = true;
  15774. }
  15775. this._updateNodeIndexList();
  15776. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15777. this._resetLevels();
  15778. this._setupHierarchicalLayout();
  15779. }
  15780. this._updateCalculationNodes();
  15781. this._reconnectEdges();
  15782. this._updateValueRange(this.nodes);
  15783. this.updateLabels();
  15784. };
  15785. /**
  15786. * Update existing nodes, or create them when not yet existing
  15787. * @param {Number[] | String[]} ids
  15788. * @private
  15789. */
  15790. Graph.prototype._updateNodes = function(ids) {
  15791. var nodes = this.nodes,
  15792. nodesData = this.nodesData;
  15793. for (var i = 0, len = ids.length; i < len; i++) {
  15794. var id = ids[i];
  15795. var node = nodes[id];
  15796. var data = nodesData.get(id);
  15797. if (node) {
  15798. // update node
  15799. node.setProperties(data, this.constants);
  15800. }
  15801. else {
  15802. // create node
  15803. node = new Node(properties, this.images, this.groups, this.constants);
  15804. nodes[id] = node;
  15805. }
  15806. }
  15807. this.moving = true;
  15808. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15809. this._resetLevels();
  15810. this._setupHierarchicalLayout();
  15811. }
  15812. this._updateNodeIndexList();
  15813. this._reconnectEdges();
  15814. this._updateValueRange(nodes);
  15815. };
  15816. /**
  15817. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  15818. * @param {Number[] | String[]} ids
  15819. * @private
  15820. */
  15821. Graph.prototype._removeNodes = function(ids) {
  15822. var nodes = this.nodes;
  15823. for (var i = 0, len = ids.length; i < len; i++) {
  15824. var id = ids[i];
  15825. delete nodes[id];
  15826. }
  15827. this._updateNodeIndexList();
  15828. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15829. this._resetLevels();
  15830. this._setupHierarchicalLayout();
  15831. }
  15832. this._updateCalculationNodes();
  15833. this._reconnectEdges();
  15834. this._updateSelection();
  15835. this._updateValueRange(nodes);
  15836. };
  15837. /**
  15838. * Load edges by reading the data table
  15839. * @param {Array | DataSet | DataView} edges The data containing the edges.
  15840. * @private
  15841. * @private
  15842. */
  15843. Graph.prototype._setEdges = function(edges) {
  15844. var oldEdgesData = this.edgesData;
  15845. if (edges instanceof DataSet || edges instanceof DataView) {
  15846. this.edgesData = edges;
  15847. }
  15848. else if (edges instanceof Array) {
  15849. this.edgesData = new DataSet();
  15850. this.edgesData.add(edges);
  15851. }
  15852. else if (!edges) {
  15853. this.edgesData = new DataSet();
  15854. }
  15855. else {
  15856. throw new TypeError('Array or DataSet expected');
  15857. }
  15858. if (oldEdgesData) {
  15859. // unsubscribe from old dataset
  15860. util.forEach(this.edgesListeners, function (callback, event) {
  15861. oldEdgesData.off(event, callback);
  15862. });
  15863. }
  15864. // remove drawn edges
  15865. this.edges = {};
  15866. if (this.edgesData) {
  15867. // subscribe to new dataset
  15868. var me = this;
  15869. util.forEach(this.edgesListeners, function (callback, event) {
  15870. me.edgesData.on(event, callback);
  15871. });
  15872. // draw all new nodes
  15873. var ids = this.edgesData.getIds();
  15874. this._addEdges(ids);
  15875. }
  15876. this._reconnectEdges();
  15877. };
  15878. /**
  15879. * Add edges
  15880. * @param {Number[] | String[]} ids
  15881. * @private
  15882. */
  15883. Graph.prototype._addEdges = function (ids) {
  15884. var edges = this.edges,
  15885. edgesData = this.edgesData;
  15886. for (var i = 0, len = ids.length; i < len; i++) {
  15887. var id = ids[i];
  15888. var oldEdge = edges[id];
  15889. if (oldEdge) {
  15890. oldEdge.disconnect();
  15891. }
  15892. var data = edgesData.get(id, {"showInternalIds" : true});
  15893. edges[id] = new Edge(data, this, this.constants);
  15894. }
  15895. this.moving = true;
  15896. this._updateValueRange(edges);
  15897. this._createBezierNodes();
  15898. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15899. this._resetLevels();
  15900. this._setupHierarchicalLayout();
  15901. }
  15902. this._updateCalculationNodes();
  15903. };
  15904. /**
  15905. * Update existing edges, or create them when not yet existing
  15906. * @param {Number[] | String[]} ids
  15907. * @private
  15908. */
  15909. Graph.prototype._updateEdges = function (ids) {
  15910. var edges = this.edges,
  15911. edgesData = this.edgesData;
  15912. for (var i = 0, len = ids.length; i < len; i++) {
  15913. var id = ids[i];
  15914. var data = edgesData.get(id);
  15915. var edge = edges[id];
  15916. if (edge) {
  15917. // update edge
  15918. edge.disconnect();
  15919. edge.setProperties(data, this.constants);
  15920. edge.connect();
  15921. }
  15922. else {
  15923. // create edge
  15924. edge = new Edge(data, this, this.constants);
  15925. this.edges[id] = edge;
  15926. }
  15927. }
  15928. this._createBezierNodes();
  15929. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15930. this._resetLevels();
  15931. this._setupHierarchicalLayout();
  15932. }
  15933. this.moving = true;
  15934. this._updateValueRange(edges);
  15935. };
  15936. /**
  15937. * Remove existing edges. Non existing ids will be ignored
  15938. * @param {Number[] | String[]} ids
  15939. * @private
  15940. */
  15941. Graph.prototype._removeEdges = function (ids) {
  15942. var edges = this.edges;
  15943. for (var i = 0, len = ids.length; i < len; i++) {
  15944. var id = ids[i];
  15945. var edge = edges[id];
  15946. if (edge) {
  15947. if (edge.via != null) {
  15948. delete this.sectors['support']['nodes'][edge.via.id];
  15949. }
  15950. edge.disconnect();
  15951. delete edges[id];
  15952. }
  15953. }
  15954. this.moving = true;
  15955. this._updateValueRange(edges);
  15956. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15957. this._resetLevels();
  15958. this._setupHierarchicalLayout();
  15959. }
  15960. this._updateCalculationNodes();
  15961. };
  15962. /**
  15963. * Reconnect all edges
  15964. * @private
  15965. */
  15966. Graph.prototype._reconnectEdges = function() {
  15967. var id,
  15968. nodes = this.nodes,
  15969. edges = this.edges;
  15970. for (id in nodes) {
  15971. if (nodes.hasOwnProperty(id)) {
  15972. nodes[id].edges = [];
  15973. }
  15974. }
  15975. for (id in edges) {
  15976. if (edges.hasOwnProperty(id)) {
  15977. var edge = edges[id];
  15978. edge.from = null;
  15979. edge.to = null;
  15980. edge.connect();
  15981. }
  15982. }
  15983. };
  15984. /**
  15985. * Update the values of all object in the given array according to the current
  15986. * value range of the objects in the array.
  15987. * @param {Object} obj An object containing a set of Edges or Nodes
  15988. * The objects must have a method getValue() and
  15989. * setValueRange(min, max).
  15990. * @private
  15991. */
  15992. Graph.prototype._updateValueRange = function(obj) {
  15993. var id;
  15994. // determine the range of the objects
  15995. var valueMin = undefined;
  15996. var valueMax = undefined;
  15997. for (id in obj) {
  15998. if (obj.hasOwnProperty(id)) {
  15999. var value = obj[id].getValue();
  16000. if (value !== undefined) {
  16001. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  16002. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  16003. }
  16004. }
  16005. }
  16006. // adjust the range of all objects
  16007. if (valueMin !== undefined && valueMax !== undefined) {
  16008. for (id in obj) {
  16009. if (obj.hasOwnProperty(id)) {
  16010. obj[id].setValueRange(valueMin, valueMax);
  16011. }
  16012. }
  16013. }
  16014. };
  16015. /**
  16016. * Redraw the graph with the current data
  16017. * chart will be resized too.
  16018. */
  16019. Graph.prototype.redraw = function() {
  16020. this.setSize(this.width, this.height);
  16021. this._redraw();
  16022. };
  16023. /**
  16024. * Redraw the graph with the current data
  16025. * @private
  16026. */
  16027. Graph.prototype._redraw = function() {
  16028. var ctx = this.frame.canvas.getContext('2d');
  16029. // clear the canvas
  16030. var w = this.frame.canvas.width;
  16031. var h = this.frame.canvas.height;
  16032. ctx.clearRect(0, 0, w, h);
  16033. // set scaling and translation
  16034. ctx.save();
  16035. ctx.translate(this.translation.x, this.translation.y);
  16036. ctx.scale(this.scale, this.scale);
  16037. this.canvasTopLeft = {
  16038. "x": this._XconvertDOMtoCanvas(0),
  16039. "y": this._YconvertDOMtoCanvas(0)
  16040. };
  16041. this.canvasBottomRight = {
  16042. "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
  16043. "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
  16044. };
  16045. this._doInAllSectors("_drawAllSectorNodes",ctx);
  16046. this._doInAllSectors("_drawEdges",ctx);
  16047. this._doInAllSectors("_drawNodes",ctx,false);
  16048. // this._doInSupportSector("_drawNodes",ctx,true);
  16049. // this._drawTree(ctx,"#F00F0F");
  16050. // restore original scaling and translation
  16051. ctx.restore();
  16052. };
  16053. /**
  16054. * Set the translation of the graph
  16055. * @param {Number} offsetX Horizontal offset
  16056. * @param {Number} offsetY Vertical offset
  16057. * @private
  16058. */
  16059. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  16060. if (this.translation === undefined) {
  16061. this.translation = {
  16062. x: 0,
  16063. y: 0
  16064. };
  16065. }
  16066. if (offsetX !== undefined) {
  16067. this.translation.x = offsetX;
  16068. }
  16069. if (offsetY !== undefined) {
  16070. this.translation.y = offsetY;
  16071. }
  16072. this.emit('viewChanged');
  16073. };
  16074. /**
  16075. * Get the translation of the graph
  16076. * @return {Object} translation An object with parameters x and y, both a number
  16077. * @private
  16078. */
  16079. Graph.prototype._getTranslation = function() {
  16080. return {
  16081. x: this.translation.x,
  16082. y: this.translation.y
  16083. };
  16084. };
  16085. /**
  16086. * Scale the graph
  16087. * @param {Number} scale Scaling factor 1.0 is unscaled
  16088. * @private
  16089. */
  16090. Graph.prototype._setScale = function(scale) {
  16091. this.scale = scale;
  16092. };
  16093. /**
  16094. * Get the current scale of the graph
  16095. * @return {Number} scale Scaling factor 1.0 is unscaled
  16096. * @private
  16097. */
  16098. Graph.prototype._getScale = function() {
  16099. return this.scale;
  16100. };
  16101. /**
  16102. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  16103. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  16104. * @param {number} x
  16105. * @returns {number}
  16106. * @private
  16107. */
  16108. Graph.prototype._XconvertDOMtoCanvas = function(x) {
  16109. return (x - this.translation.x) / this.scale;
  16110. };
  16111. /**
  16112. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  16113. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  16114. * @param {number} x
  16115. * @returns {number}
  16116. * @private
  16117. */
  16118. Graph.prototype._XconvertCanvasToDOM = function(x) {
  16119. return x * this.scale + this.translation.x;
  16120. };
  16121. /**
  16122. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  16123. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  16124. * @param {number} y
  16125. * @returns {number}
  16126. * @private
  16127. */
  16128. Graph.prototype._YconvertDOMtoCanvas = function(y) {
  16129. return (y - this.translation.y) / this.scale;
  16130. };
  16131. /**
  16132. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  16133. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  16134. * @param {number} y
  16135. * @returns {number}
  16136. * @private
  16137. */
  16138. Graph.prototype._YconvertCanvasToDOM = function(y) {
  16139. return y * this.scale + this.translation.y ;
  16140. };
  16141. /**
  16142. *
  16143. * @param {object} pos = {x: number, y: number}
  16144. * @returns {{x: number, y: number}}
  16145. * @constructor
  16146. */
  16147. Graph.prototype.canvasToDOM = function(pos) {
  16148. return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
  16149. }
  16150. /**
  16151. *
  16152. * @param {object} pos = {x: number, y: number}
  16153. * @returns {{x: number, y: number}}
  16154. * @constructor
  16155. */
  16156. Graph.prototype.DOMtoCanvas = function(pos) {
  16157. return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
  16158. }
  16159. /**
  16160. * Redraw all nodes
  16161. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  16162. * @param {CanvasRenderingContext2D} ctx
  16163. * @param {Boolean} [alwaysShow]
  16164. * @private
  16165. */
  16166. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  16167. if (alwaysShow === undefined) {
  16168. alwaysShow = false;
  16169. }
  16170. // first draw the unselected nodes
  16171. var nodes = this.nodes;
  16172. var selected = [];
  16173. for (var id in nodes) {
  16174. if (nodes.hasOwnProperty(id)) {
  16175. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  16176. if (nodes[id].isSelected()) {
  16177. selected.push(id);
  16178. }
  16179. else {
  16180. if (nodes[id].inArea() || alwaysShow) {
  16181. nodes[id].draw(ctx);
  16182. }
  16183. }
  16184. }
  16185. }
  16186. // draw the selected nodes on top
  16187. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  16188. if (nodes[selected[s]].inArea() || alwaysShow) {
  16189. nodes[selected[s]].draw(ctx);
  16190. }
  16191. }
  16192. };
  16193. /**
  16194. * Redraw all edges
  16195. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  16196. * @param {CanvasRenderingContext2D} ctx
  16197. * @private
  16198. */
  16199. Graph.prototype._drawEdges = function(ctx) {
  16200. var edges = this.edges;
  16201. for (var id in edges) {
  16202. if (edges.hasOwnProperty(id)) {
  16203. var edge = edges[id];
  16204. edge.setScale(this.scale);
  16205. if (edge.connected) {
  16206. edges[id].draw(ctx);
  16207. }
  16208. }
  16209. }
  16210. };
  16211. /**
  16212. * Find a stable position for all nodes
  16213. * @private
  16214. */
  16215. Graph.prototype._stabilize = function() {
  16216. if (this.constants.freezeForStabilization == true) {
  16217. this._freezeDefinedNodes();
  16218. }
  16219. // find stable position
  16220. var count = 0;
  16221. while (this.moving && count < this.constants.stabilizationIterations) {
  16222. this._physicsTick();
  16223. count++;
  16224. }
  16225. this.zoomExtent(false,true);
  16226. if (this.constants.freezeForStabilization == true) {
  16227. this._restoreFrozenNodes();
  16228. }
  16229. this.emit("stabilized",{iterations:count});
  16230. };
  16231. /**
  16232. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  16233. * because only the supportnodes for the smoothCurves have to settle.
  16234. *
  16235. * @private
  16236. */
  16237. Graph.prototype._freezeDefinedNodes = function() {
  16238. var nodes = this.nodes;
  16239. for (var id in nodes) {
  16240. if (nodes.hasOwnProperty(id)) {
  16241. if (nodes[id].x != null && nodes[id].y != null) {
  16242. nodes[id].fixedData.x = nodes[id].xFixed;
  16243. nodes[id].fixedData.y = nodes[id].yFixed;
  16244. nodes[id].xFixed = true;
  16245. nodes[id].yFixed = true;
  16246. }
  16247. }
  16248. }
  16249. };
  16250. /**
  16251. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  16252. *
  16253. * @private
  16254. */
  16255. Graph.prototype._restoreFrozenNodes = function() {
  16256. var nodes = this.nodes;
  16257. for (var id in nodes) {
  16258. if (nodes.hasOwnProperty(id)) {
  16259. if (nodes[id].fixedData.x != null) {
  16260. nodes[id].xFixed = nodes[id].fixedData.x;
  16261. nodes[id].yFixed = nodes[id].fixedData.y;
  16262. }
  16263. }
  16264. }
  16265. };
  16266. /**
  16267. * Check if any of the nodes is still moving
  16268. * @param {number} vmin the minimum velocity considered as 'moving'
  16269. * @return {boolean} true if moving, false if non of the nodes is moving
  16270. * @private
  16271. */
  16272. Graph.prototype._isMoving = function(vmin) {
  16273. var nodes = this.nodes;
  16274. for (var id in nodes) {
  16275. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  16276. return true;
  16277. }
  16278. }
  16279. return false;
  16280. };
  16281. /**
  16282. * /**
  16283. * Perform one discrete step for all nodes
  16284. *
  16285. * @private
  16286. */
  16287. Graph.prototype._discreteStepNodes = function() {
  16288. var interval = this.physicsDiscreteStepsize;
  16289. var nodes = this.nodes;
  16290. var nodeId;
  16291. var nodesPresent = false;
  16292. if (this.constants.maxVelocity > 0) {
  16293. for (nodeId in nodes) {
  16294. if (nodes.hasOwnProperty(nodeId)) {
  16295. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  16296. nodesPresent = true;
  16297. }
  16298. }
  16299. }
  16300. else {
  16301. for (nodeId in nodes) {
  16302. if (nodes.hasOwnProperty(nodeId)) {
  16303. nodes[nodeId].discreteStep(interval);
  16304. nodesPresent = true;
  16305. }
  16306. }
  16307. }
  16308. if (nodesPresent == true) {
  16309. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  16310. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  16311. this.moving = true;
  16312. }
  16313. else {
  16314. this.moving = this._isMoving(vminCorrected);
  16315. }
  16316. }
  16317. };
  16318. /**
  16319. * A single simulation step (or "tick") in the physics simulation
  16320. *
  16321. * @private
  16322. */
  16323. Graph.prototype._physicsTick = function() {
  16324. if (!this.freezeSimulation) {
  16325. if (this.moving) {
  16326. this._doInAllActiveSectors("_initializeForceCalculation");
  16327. this._doInAllActiveSectors("_discreteStepNodes");
  16328. if (this.constants.smoothCurves) {
  16329. this._doInSupportSector("_discreteStepNodes");
  16330. }
  16331. this._findCenter(this._getRange())
  16332. }
  16333. }
  16334. };
  16335. /**
  16336. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  16337. * It reschedules itself at the beginning of the function
  16338. *
  16339. * @private
  16340. */
  16341. Graph.prototype._animationStep = function() {
  16342. // reset the timer so a new scheduled animation step can be set
  16343. this.timer = undefined;
  16344. // handle the keyboad movement
  16345. this._handleNavigation();
  16346. // this schedules a new animation step
  16347. this.start();
  16348. // start the physics simulation
  16349. var calculationTime = Date.now();
  16350. var maxSteps = 1;
  16351. this._physicsTick();
  16352. var timeRequired = Date.now() - calculationTime;
  16353. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  16354. this._physicsTick();
  16355. timeRequired = Date.now() - calculationTime;
  16356. maxSteps++;
  16357. }
  16358. // start the rendering process
  16359. var renderTime = Date.now();
  16360. this._redraw();
  16361. this.renderTime = Date.now() - renderTime;
  16362. };
  16363. if (typeof window !== 'undefined') {
  16364. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  16365. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  16366. }
  16367. /**
  16368. * Schedule a animation step with the refreshrate interval.
  16369. */
  16370. Graph.prototype.start = function() {
  16371. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  16372. if (!this.timer) {
  16373. var ua = navigator.userAgent.toLowerCase();
  16374. var requiresTimeout = false;
  16375. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  16376. requiresTimeout = true;
  16377. }
  16378. else if (ua.indexOf('safari') != -1) { // safari
  16379. if (ua.indexOf('chrome') <= -1) {
  16380. requiresTimeout = true;
  16381. }
  16382. }
  16383. if (requiresTimeout == true) {
  16384. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  16385. }
  16386. else{
  16387. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  16388. }
  16389. }
  16390. }
  16391. else {
  16392. this._redraw();
  16393. }
  16394. };
  16395. /**
  16396. * Move the graph according to the keyboard presses.
  16397. *
  16398. * @private
  16399. */
  16400. Graph.prototype._handleNavigation = function() {
  16401. if (this.xIncrement != 0 || this.yIncrement != 0) {
  16402. var translation = this._getTranslation();
  16403. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  16404. }
  16405. if (this.zoomIncrement != 0) {
  16406. var center = {
  16407. x: this.frame.canvas.clientWidth / 2,
  16408. y: this.frame.canvas.clientHeight / 2
  16409. };
  16410. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  16411. }
  16412. };
  16413. /**
  16414. * Freeze the _animationStep
  16415. */
  16416. Graph.prototype.toggleFreeze = function() {
  16417. if (this.freezeSimulation == false) {
  16418. this.freezeSimulation = true;
  16419. }
  16420. else {
  16421. this.freezeSimulation = false;
  16422. this.start();
  16423. }
  16424. };
  16425. /**
  16426. * This function cleans the support nodes if they are not needed and adds them when they are.
  16427. *
  16428. * @param {boolean} [disableStart]
  16429. * @private
  16430. */
  16431. Graph.prototype._configureSmoothCurves = function(disableStart) {
  16432. if (disableStart === undefined) {
  16433. disableStart = true;
  16434. }
  16435. if (this.constants.smoothCurves == true) {
  16436. this._createBezierNodes();
  16437. }
  16438. else {
  16439. // delete the support nodes
  16440. this.sectors['support']['nodes'] = {};
  16441. for (var edgeId in this.edges) {
  16442. if (this.edges.hasOwnProperty(edgeId)) {
  16443. this.edges[edgeId].smooth = false;
  16444. this.edges[edgeId].via = null;
  16445. }
  16446. }
  16447. }
  16448. this._updateCalculationNodes();
  16449. if (!disableStart) {
  16450. this.moving = true;
  16451. this.start();
  16452. }
  16453. };
  16454. /**
  16455. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  16456. * are used for the force calculation.
  16457. *
  16458. * @private
  16459. */
  16460. Graph.prototype._createBezierNodes = function() {
  16461. if (this.constants.smoothCurves == true) {
  16462. for (var edgeId in this.edges) {
  16463. if (this.edges.hasOwnProperty(edgeId)) {
  16464. var edge = this.edges[edgeId];
  16465. if (edge.via == null) {
  16466. edge.smooth = true;
  16467. var nodeId = "edgeId:".concat(edge.id);
  16468. this.sectors['support']['nodes'][nodeId] = new Node(
  16469. {id:nodeId,
  16470. mass:1,
  16471. shape:'circle',
  16472. image:"",
  16473. internalMultiplier:1
  16474. },{},{},this.constants);
  16475. edge.via = this.sectors['support']['nodes'][nodeId];
  16476. edge.via.parentEdgeId = edge.id;
  16477. edge.positionBezierNode();
  16478. }
  16479. }
  16480. }
  16481. }
  16482. };
  16483. /**
  16484. * load the functions that load the mixins into the prototype.
  16485. *
  16486. * @private
  16487. */
  16488. Graph.prototype._initializeMixinLoaders = function () {
  16489. for (var mixinFunction in graphMixinLoaders) {
  16490. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  16491. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  16492. }
  16493. }
  16494. };
  16495. /**
  16496. * Load the XY positions of the nodes into the dataset.
  16497. */
  16498. Graph.prototype.storePosition = function() {
  16499. var dataArray = [];
  16500. for (var nodeId in this.nodes) {
  16501. if (this.nodes.hasOwnProperty(nodeId)) {
  16502. var node = this.nodes[nodeId];
  16503. var allowedToMoveX = !this.nodes.xFixed;
  16504. var allowedToMoveY = !this.nodes.yFixed;
  16505. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  16506. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  16507. }
  16508. }
  16509. }
  16510. this.nodesData.update(dataArray);
  16511. };
  16512. /**
  16513. * vis.js module exports
  16514. */
  16515. var vis = {
  16516. util: util,
  16517. moment: moment,
  16518. DataSet: DataSet,
  16519. DataView: DataView,
  16520. Range: Range,
  16521. stack: stack,
  16522. TimeStep: TimeStep,
  16523. components: {
  16524. items: {
  16525. Item: Item,
  16526. ItemBox: ItemBox,
  16527. ItemPoint: ItemPoint,
  16528. ItemRange: ItemRange
  16529. },
  16530. Component: Component,
  16531. Panel: Panel,
  16532. RootPanel: RootPanel,
  16533. ItemSet: ItemSet,
  16534. TimeAxis: TimeAxis
  16535. },
  16536. graph: {
  16537. Node: Node,
  16538. Edge: Edge,
  16539. Popup: Popup,
  16540. Groups: Groups,
  16541. Images: Images
  16542. },
  16543. Timeline: Timeline,
  16544. Graph: Graph
  16545. };
  16546. /**
  16547. * CommonJS module exports
  16548. */
  16549. if (typeof exports !== 'undefined') {
  16550. exports = vis;
  16551. }
  16552. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  16553. module.exports = vis;
  16554. }
  16555. /**
  16556. * AMD module exports
  16557. */
  16558. if (typeof(define) === 'function') {
  16559. define(function () {
  16560. return vis;
  16561. });
  16562. }
  16563. /**
  16564. * Window exports
  16565. */
  16566. if (typeof window !== 'undefined') {
  16567. // attach the module to the window, load as a regular javascript file
  16568. window['vis'] = vis;
  16569. }
  16570. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  16571. /**
  16572. * Expose `Emitter`.
  16573. */
  16574. module.exports = Emitter;
  16575. /**
  16576. * Initialize a new `Emitter`.
  16577. *
  16578. * @api public
  16579. */
  16580. function Emitter(obj) {
  16581. if (obj) return mixin(obj);
  16582. };
  16583. /**
  16584. * Mixin the emitter properties.
  16585. *
  16586. * @param {Object} obj
  16587. * @return {Object}
  16588. * @api private
  16589. */
  16590. function mixin(obj) {
  16591. for (var key in Emitter.prototype) {
  16592. obj[key] = Emitter.prototype[key];
  16593. }
  16594. return obj;
  16595. }
  16596. /**
  16597. * Listen on the given `event` with `fn`.
  16598. *
  16599. * @param {String} event
  16600. * @param {Function} fn
  16601. * @return {Emitter}
  16602. * @api public
  16603. */
  16604. Emitter.prototype.on =
  16605. Emitter.prototype.addEventListener = function(event, fn){
  16606. this._callbacks = this._callbacks || {};
  16607. (this._callbacks[event] = this._callbacks[event] || [])
  16608. .push(fn);
  16609. return this;
  16610. };
  16611. /**
  16612. * Adds an `event` listener that will be invoked a single
  16613. * time then automatically removed.
  16614. *
  16615. * @param {String} event
  16616. * @param {Function} fn
  16617. * @return {Emitter}
  16618. * @api public
  16619. */
  16620. Emitter.prototype.once = function(event, fn){
  16621. var self = this;
  16622. this._callbacks = this._callbacks || {};
  16623. function on() {
  16624. self.off(event, on);
  16625. fn.apply(this, arguments);
  16626. }
  16627. on.fn = fn;
  16628. this.on(event, on);
  16629. return this;
  16630. };
  16631. /**
  16632. * Remove the given callback for `event` or all
  16633. * registered callbacks.
  16634. *
  16635. * @param {String} event
  16636. * @param {Function} fn
  16637. * @return {Emitter}
  16638. * @api public
  16639. */
  16640. Emitter.prototype.off =
  16641. Emitter.prototype.removeListener =
  16642. Emitter.prototype.removeAllListeners =
  16643. Emitter.prototype.removeEventListener = function(event, fn){
  16644. this._callbacks = this._callbacks || {};
  16645. // all
  16646. if (0 == arguments.length) {
  16647. this._callbacks = {};
  16648. return this;
  16649. }
  16650. // specific event
  16651. var callbacks = this._callbacks[event];
  16652. if (!callbacks) return this;
  16653. // remove all handlers
  16654. if (1 == arguments.length) {
  16655. delete this._callbacks[event];
  16656. return this;
  16657. }
  16658. // remove specific handler
  16659. var cb;
  16660. for (var i = 0; i < callbacks.length; i++) {
  16661. cb = callbacks[i];
  16662. if (cb === fn || cb.fn === fn) {
  16663. callbacks.splice(i, 1);
  16664. break;
  16665. }
  16666. }
  16667. return this;
  16668. };
  16669. /**
  16670. * Emit `event` with the given args.
  16671. *
  16672. * @param {String} event
  16673. * @param {Mixed} ...
  16674. * @return {Emitter}
  16675. */
  16676. Emitter.prototype.emit = function(event){
  16677. this._callbacks = this._callbacks || {};
  16678. var args = [].slice.call(arguments, 1)
  16679. , callbacks = this._callbacks[event];
  16680. if (callbacks) {
  16681. callbacks = callbacks.slice(0);
  16682. for (var i = 0, len = callbacks.length; i < len; ++i) {
  16683. callbacks[i].apply(this, args);
  16684. }
  16685. }
  16686. return this;
  16687. };
  16688. /**
  16689. * Return array of callbacks for `event`.
  16690. *
  16691. * @param {String} event
  16692. * @return {Array}
  16693. * @api public
  16694. */
  16695. Emitter.prototype.listeners = function(event){
  16696. this._callbacks = this._callbacks || {};
  16697. return this._callbacks[event] || [];
  16698. };
  16699. /**
  16700. * Check if this emitter has `event` handlers.
  16701. *
  16702. * @param {String} event
  16703. * @return {Boolean}
  16704. * @api public
  16705. */
  16706. Emitter.prototype.hasListeners = function(event){
  16707. return !! this.listeners(event).length;
  16708. };
  16709. },{}],3:[function(require,module,exports){
  16710. /*! Hammer.JS - v1.0.5 - 2013-04-07
  16711. * http://eightmedia.github.com/hammer.js
  16712. *
  16713. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  16714. * Licensed under the MIT license */
  16715. (function(window, undefined) {
  16716. 'use strict';
  16717. /**
  16718. * Hammer
  16719. * use this to create instances
  16720. * @param {HTMLElement} element
  16721. * @param {Object} options
  16722. * @returns {Hammer.Instance}
  16723. * @constructor
  16724. */
  16725. var Hammer = function(element, options) {
  16726. return new Hammer.Instance(element, options || {});
  16727. };
  16728. // default settings
  16729. Hammer.defaults = {
  16730. // add styles and attributes to the element to prevent the browser from doing
  16731. // its native behavior. this doesnt prevent the scrolling, but cancels
  16732. // the contextmenu, tap highlighting etc
  16733. // set to false to disable this
  16734. stop_browser_behavior: {
  16735. // this also triggers onselectstart=false for IE
  16736. userSelect: 'none',
  16737. // this makes the element blocking in IE10 >, you could experiment with the value
  16738. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  16739. touchAction: 'none',
  16740. touchCallout: 'none',
  16741. contentZooming: 'none',
  16742. userDrag: 'none',
  16743. tapHighlightColor: 'rgba(0,0,0,0)'
  16744. }
  16745. // more settings are defined per gesture at gestures.js
  16746. };
  16747. // detect touchevents
  16748. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  16749. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  16750. // dont use mouseevents on mobile devices
  16751. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  16752. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  16753. // eventtypes per touchevent (start, move, end)
  16754. // are filled by Hammer.event.determineEventTypes on setup
  16755. Hammer.EVENT_TYPES = {};
  16756. // direction defines
  16757. Hammer.DIRECTION_DOWN = 'down';
  16758. Hammer.DIRECTION_LEFT = 'left';
  16759. Hammer.DIRECTION_UP = 'up';
  16760. Hammer.DIRECTION_RIGHT = 'right';
  16761. // pointer type
  16762. Hammer.POINTER_MOUSE = 'mouse';
  16763. Hammer.POINTER_TOUCH = 'touch';
  16764. Hammer.POINTER_PEN = 'pen';
  16765. // touch event defines
  16766. Hammer.EVENT_START = 'start';
  16767. Hammer.EVENT_MOVE = 'move';
  16768. Hammer.EVENT_END = 'end';
  16769. // hammer document where the base events are added at
  16770. Hammer.DOCUMENT = document;
  16771. // plugins namespace
  16772. Hammer.plugins = {};
  16773. // if the window events are set...
  16774. Hammer.READY = false;
  16775. /**
  16776. * setup events to detect gestures on the document
  16777. */
  16778. function setup() {
  16779. if(Hammer.READY) {
  16780. return;
  16781. }
  16782. // find what eventtypes we add listeners to
  16783. Hammer.event.determineEventTypes();
  16784. // Register all gestures inside Hammer.gestures
  16785. for(var name in Hammer.gestures) {
  16786. if(Hammer.gestures.hasOwnProperty(name)) {
  16787. Hammer.detection.register(Hammer.gestures[name]);
  16788. }
  16789. }
  16790. // Add touch events on the document
  16791. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  16792. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  16793. // Hammer is ready...!
  16794. Hammer.READY = true;
  16795. }
  16796. /**
  16797. * create new hammer instance
  16798. * all methods should return the instance itself, so it is chainable.
  16799. * @param {HTMLElement} element
  16800. * @param {Object} [options={}]
  16801. * @returns {Hammer.Instance}
  16802. * @constructor
  16803. */
  16804. Hammer.Instance = function(element, options) {
  16805. var self = this;
  16806. // setup HammerJS window events and register all gestures
  16807. // this also sets up the default options
  16808. setup();
  16809. this.element = element;
  16810. // start/stop detection option
  16811. this.enabled = true;
  16812. // merge options
  16813. this.options = Hammer.utils.extend(
  16814. Hammer.utils.extend({}, Hammer.defaults),
  16815. options || {});
  16816. // add some css to the element to prevent the browser from doing its native behavoir
  16817. if(this.options.stop_browser_behavior) {
  16818. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  16819. }
  16820. // start detection on touchstart
  16821. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  16822. if(self.enabled) {
  16823. Hammer.detection.startDetect(self, ev);
  16824. }
  16825. });
  16826. // return instance
  16827. return this;
  16828. };
  16829. Hammer.Instance.prototype = {
  16830. /**
  16831. * bind events to the instance
  16832. * @param {String} gesture
  16833. * @param {Function} handler
  16834. * @returns {Hammer.Instance}
  16835. */
  16836. on: function onEvent(gesture, handler){
  16837. var gestures = gesture.split(' ');
  16838. for(var t=0; t<gestures.length; t++) {
  16839. this.element.addEventListener(gestures[t], handler, false);
  16840. }
  16841. return this;
  16842. },
  16843. /**
  16844. * unbind events to the instance
  16845. * @param {String} gesture
  16846. * @param {Function} handler
  16847. * @returns {Hammer.Instance}
  16848. */
  16849. off: function offEvent(gesture, handler){
  16850. var gestures = gesture.split(' ');
  16851. for(var t=0; t<gestures.length; t++) {
  16852. this.element.removeEventListener(gestures[t], handler, false);
  16853. }
  16854. return this;
  16855. },
  16856. /**
  16857. * trigger gesture event
  16858. * @param {String} gesture
  16859. * @param {Object} eventData
  16860. * @returns {Hammer.Instance}
  16861. */
  16862. trigger: function triggerEvent(gesture, eventData){
  16863. // create DOM event
  16864. var event = Hammer.DOCUMENT.createEvent('Event');
  16865. event.initEvent(gesture, true, true);
  16866. event.gesture = eventData;
  16867. // trigger on the target if it is in the instance element,
  16868. // this is for event delegation tricks
  16869. var element = this.element;
  16870. if(Hammer.utils.hasParent(eventData.target, element)) {
  16871. element = eventData.target;
  16872. }
  16873. element.dispatchEvent(event);
  16874. return this;
  16875. },
  16876. /**
  16877. * enable of disable hammer.js detection
  16878. * @param {Boolean} state
  16879. * @returns {Hammer.Instance}
  16880. */
  16881. enable: function enable(state) {
  16882. this.enabled = state;
  16883. return this;
  16884. }
  16885. };
  16886. /**
  16887. * this holds the last move event,
  16888. * used to fix empty touchend issue
  16889. * see the onTouch event for an explanation
  16890. * @type {Object}
  16891. */
  16892. var last_move_event = null;
  16893. /**
  16894. * when the mouse is hold down, this is true
  16895. * @type {Boolean}
  16896. */
  16897. var enable_detect = false;
  16898. /**
  16899. * when touch events have been fired, this is true
  16900. * @type {Boolean}
  16901. */
  16902. var touch_triggered = false;
  16903. Hammer.event = {
  16904. /**
  16905. * simple addEventListener
  16906. * @param {HTMLElement} element
  16907. * @param {String} type
  16908. * @param {Function} handler
  16909. */
  16910. bindDom: function(element, type, handler) {
  16911. var types = type.split(' ');
  16912. for(var t=0; t<types.length; t++) {
  16913. element.addEventListener(types[t], handler, false);
  16914. }
  16915. },
  16916. /**
  16917. * touch events with mouse fallback
  16918. * @param {HTMLElement} element
  16919. * @param {String} eventType like Hammer.EVENT_MOVE
  16920. * @param {Function} handler
  16921. */
  16922. onTouch: function onTouch(element, eventType, handler) {
  16923. var self = this;
  16924. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  16925. var sourceEventType = ev.type.toLowerCase();
  16926. // onmouseup, but when touchend has been fired we do nothing.
  16927. // this is for touchdevices which also fire a mouseup on touchend
  16928. if(sourceEventType.match(/mouse/) && touch_triggered) {
  16929. return;
  16930. }
  16931. // mousebutton must be down or a touch event
  16932. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  16933. sourceEventType.match(/pointerdown/) || // pointerevents touch
  16934. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  16935. ){
  16936. enable_detect = true;
  16937. }
  16938. // we are in a touch event, set the touch triggered bool to true,
  16939. // this for the conflicts that may occur on ios and android
  16940. if(sourceEventType.match(/touch|pointer/)) {
  16941. touch_triggered = true;
  16942. }
  16943. // count the total touches on the screen
  16944. var count_touches = 0;
  16945. // when touch has been triggered in this detection session
  16946. // and we are now handling a mouse event, we stop that to prevent conflicts
  16947. if(enable_detect) {
  16948. // update pointerevent
  16949. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  16950. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16951. }
  16952. // touch
  16953. else if(sourceEventType.match(/touch/)) {
  16954. count_touches = ev.touches.length;
  16955. }
  16956. // mouse
  16957. else if(!touch_triggered) {
  16958. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  16959. }
  16960. // if we are in a end event, but when we remove one touch and
  16961. // we still have enough, set eventType to move
  16962. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  16963. eventType = Hammer.EVENT_MOVE;
  16964. }
  16965. // no touches, force the end event
  16966. else if(!count_touches) {
  16967. eventType = Hammer.EVENT_END;
  16968. }
  16969. // because touchend has no touches, and we often want to use these in our gestures,
  16970. // we send the last move event as our eventData in touchend
  16971. if(!count_touches && last_move_event !== null) {
  16972. ev = last_move_event;
  16973. }
  16974. // store the last move event
  16975. else {
  16976. last_move_event = ev;
  16977. }
  16978. // trigger the handler
  16979. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  16980. // remove pointerevent from list
  16981. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  16982. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16983. }
  16984. }
  16985. //debug(sourceEventType +" "+ eventType);
  16986. // on the end we reset everything
  16987. if(!count_touches) {
  16988. last_move_event = null;
  16989. enable_detect = false;
  16990. touch_triggered = false;
  16991. Hammer.PointerEvent.reset();
  16992. }
  16993. });
  16994. },
  16995. /**
  16996. * we have different events for each device/browser
  16997. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  16998. */
  16999. determineEventTypes: function determineEventTypes() {
  17000. // determine the eventtype we want to set
  17001. var types;
  17002. // pointerEvents magic
  17003. if(Hammer.HAS_POINTEREVENTS) {
  17004. types = Hammer.PointerEvent.getEvents();
  17005. }
  17006. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  17007. else if(Hammer.NO_MOUSEEVENTS) {
  17008. types = [
  17009. 'touchstart',
  17010. 'touchmove',
  17011. 'touchend touchcancel'];
  17012. }
  17013. // for non pointer events browsers and mixed browsers,
  17014. // like chrome on windows8 touch laptop
  17015. else {
  17016. types = [
  17017. 'touchstart mousedown',
  17018. 'touchmove mousemove',
  17019. 'touchend touchcancel mouseup'];
  17020. }
  17021. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  17022. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  17023. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  17024. },
  17025. /**
  17026. * create touchlist depending on the event
  17027. * @param {Object} ev
  17028. * @param {String} eventType used by the fakemultitouch plugin
  17029. */
  17030. getTouchList: function getTouchList(ev/*, eventType*/) {
  17031. // get the fake pointerEvent touchlist
  17032. if(Hammer.HAS_POINTEREVENTS) {
  17033. return Hammer.PointerEvent.getTouchList();
  17034. }
  17035. // get the touchlist
  17036. else if(ev.touches) {
  17037. return ev.touches;
  17038. }
  17039. // make fake touchlist from mouse position
  17040. else {
  17041. return [{
  17042. identifier: 1,
  17043. pageX: ev.pageX,
  17044. pageY: ev.pageY,
  17045. target: ev.target
  17046. }];
  17047. }
  17048. },
  17049. /**
  17050. * collect event data for Hammer js
  17051. * @param {HTMLElement} element
  17052. * @param {String} eventType like Hammer.EVENT_MOVE
  17053. * @param {Object} eventData
  17054. */
  17055. collectEventData: function collectEventData(element, eventType, ev) {
  17056. var touches = this.getTouchList(ev, eventType);
  17057. // find out pointerType
  17058. var pointerType = Hammer.POINTER_TOUCH;
  17059. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  17060. pointerType = Hammer.POINTER_MOUSE;
  17061. }
  17062. return {
  17063. center : Hammer.utils.getCenter(touches),
  17064. timeStamp : new Date().getTime(),
  17065. target : ev.target,
  17066. touches : touches,
  17067. eventType : eventType,
  17068. pointerType : pointerType,
  17069. srcEvent : ev,
  17070. /**
  17071. * prevent the browser default actions
  17072. * mostly used to disable scrolling of the browser
  17073. */
  17074. preventDefault: function() {
  17075. if(this.srcEvent.preventManipulation) {
  17076. this.srcEvent.preventManipulation();
  17077. }
  17078. if(this.srcEvent.preventDefault) {
  17079. this.srcEvent.preventDefault();
  17080. }
  17081. },
  17082. /**
  17083. * stop bubbling the event up to its parents
  17084. */
  17085. stopPropagation: function() {
  17086. this.srcEvent.stopPropagation();
  17087. },
  17088. /**
  17089. * immediately stop gesture detection
  17090. * might be useful after a swipe was detected
  17091. * @return {*}
  17092. */
  17093. stopDetect: function() {
  17094. return Hammer.detection.stopDetect();
  17095. }
  17096. };
  17097. }
  17098. };
  17099. Hammer.PointerEvent = {
  17100. /**
  17101. * holds all pointers
  17102. * @type {Object}
  17103. */
  17104. pointers: {},
  17105. /**
  17106. * get a list of pointers
  17107. * @returns {Array} touchlist
  17108. */
  17109. getTouchList: function() {
  17110. var self = this;
  17111. var touchlist = [];
  17112. // we can use forEach since pointerEvents only is in IE10
  17113. Object.keys(self.pointers).sort().forEach(function(id) {
  17114. touchlist.push(self.pointers[id]);
  17115. });
  17116. return touchlist;
  17117. },
  17118. /**
  17119. * update the position of a pointer
  17120. * @param {String} type Hammer.EVENT_END
  17121. * @param {Object} pointerEvent
  17122. */
  17123. updatePointer: function(type, pointerEvent) {
  17124. if(type == Hammer.EVENT_END) {
  17125. this.pointers = {};
  17126. }
  17127. else {
  17128. pointerEvent.identifier = pointerEvent.pointerId;
  17129. this.pointers[pointerEvent.pointerId] = pointerEvent;
  17130. }
  17131. return Object.keys(this.pointers).length;
  17132. },
  17133. /**
  17134. * check if ev matches pointertype
  17135. * @param {String} pointerType Hammer.POINTER_MOUSE
  17136. * @param {PointerEvent} ev
  17137. */
  17138. matchType: function(pointerType, ev) {
  17139. if(!ev.pointerType) {
  17140. return false;
  17141. }
  17142. var types = {};
  17143. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  17144. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  17145. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  17146. return types[pointerType];
  17147. },
  17148. /**
  17149. * get events
  17150. */
  17151. getEvents: function() {
  17152. return [
  17153. 'pointerdown MSPointerDown',
  17154. 'pointermove MSPointerMove',
  17155. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  17156. ];
  17157. },
  17158. /**
  17159. * reset the list
  17160. */
  17161. reset: function() {
  17162. this.pointers = {};
  17163. }
  17164. };
  17165. Hammer.utils = {
  17166. /**
  17167. * extend method,
  17168. * also used for cloning when dest is an empty object
  17169. * @param {Object} dest
  17170. * @param {Object} src
  17171. * @parm {Boolean} merge do a merge
  17172. * @returns {Object} dest
  17173. */
  17174. extend: function extend(dest, src, merge) {
  17175. for (var key in src) {
  17176. if(dest[key] !== undefined && merge) {
  17177. continue;
  17178. }
  17179. dest[key] = src[key];
  17180. }
  17181. return dest;
  17182. },
  17183. /**
  17184. * find if a node is in the given parent
  17185. * used for event delegation tricks
  17186. * @param {HTMLElement} node
  17187. * @param {HTMLElement} parent
  17188. * @returns {boolean} has_parent
  17189. */
  17190. hasParent: function(node, parent) {
  17191. while(node){
  17192. if(node == parent) {
  17193. return true;
  17194. }
  17195. node = node.parentNode;
  17196. }
  17197. return false;
  17198. },
  17199. /**
  17200. * get the center of all the touches
  17201. * @param {Array} touches
  17202. * @returns {Object} center
  17203. */
  17204. getCenter: function getCenter(touches) {
  17205. var valuesX = [], valuesY = [];
  17206. for(var t= 0,len=touches.length; t<len; t++) {
  17207. valuesX.push(touches[t].pageX);
  17208. valuesY.push(touches[t].pageY);
  17209. }
  17210. return {
  17211. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  17212. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  17213. };
  17214. },
  17215. /**
  17216. * calculate the velocity between two points
  17217. * @param {Number} delta_time
  17218. * @param {Number} delta_x
  17219. * @param {Number} delta_y
  17220. * @returns {Object} velocity
  17221. */
  17222. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  17223. return {
  17224. x: Math.abs(delta_x / delta_time) || 0,
  17225. y: Math.abs(delta_y / delta_time) || 0
  17226. };
  17227. },
  17228. /**
  17229. * calculate the angle between two coordinates
  17230. * @param {Touch} touch1
  17231. * @param {Touch} touch2
  17232. * @returns {Number} angle
  17233. */
  17234. getAngle: function getAngle(touch1, touch2) {
  17235. var y = touch2.pageY - touch1.pageY,
  17236. x = touch2.pageX - touch1.pageX;
  17237. return Math.atan2(y, x) * 180 / Math.PI;
  17238. },
  17239. /**
  17240. * angle to direction define
  17241. * @param {Touch} touch1
  17242. * @param {Touch} touch2
  17243. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  17244. */
  17245. getDirection: function getDirection(touch1, touch2) {
  17246. var x = Math.abs(touch1.pageX - touch2.pageX),
  17247. y = Math.abs(touch1.pageY - touch2.pageY);
  17248. if(x >= y) {
  17249. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  17250. }
  17251. else {
  17252. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  17253. }
  17254. },
  17255. /**
  17256. * calculate the distance between two touches
  17257. * @param {Touch} touch1
  17258. * @param {Touch} touch2
  17259. * @returns {Number} distance
  17260. */
  17261. getDistance: function getDistance(touch1, touch2) {
  17262. var x = touch2.pageX - touch1.pageX,
  17263. y = touch2.pageY - touch1.pageY;
  17264. return Math.sqrt((x*x) + (y*y));
  17265. },
  17266. /**
  17267. * calculate the scale factor between two touchLists (fingers)
  17268. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  17269. * @param {Array} start
  17270. * @param {Array} end
  17271. * @returns {Number} scale
  17272. */
  17273. getScale: function getScale(start, end) {
  17274. // need two fingers...
  17275. if(start.length >= 2 && end.length >= 2) {
  17276. return this.getDistance(end[0], end[1]) /
  17277. this.getDistance(start[0], start[1]);
  17278. }
  17279. return 1;
  17280. },
  17281. /**
  17282. * calculate the rotation degrees between two touchLists (fingers)
  17283. * @param {Array} start
  17284. * @param {Array} end
  17285. * @returns {Number} rotation
  17286. */
  17287. getRotation: function getRotation(start, end) {
  17288. // need two fingers
  17289. if(start.length >= 2 && end.length >= 2) {
  17290. return this.getAngle(end[1], end[0]) -
  17291. this.getAngle(start[1], start[0]);
  17292. }
  17293. return 0;
  17294. },
  17295. /**
  17296. * boolean if the direction is vertical
  17297. * @param {String} direction
  17298. * @returns {Boolean} is_vertical
  17299. */
  17300. isVertical: function isVertical(direction) {
  17301. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  17302. },
  17303. /**
  17304. * stop browser default behavior with css props
  17305. * @param {HtmlElement} element
  17306. * @param {Object} css_props
  17307. */
  17308. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  17309. var prop,
  17310. vendors = ['webkit','khtml','moz','ms','o',''];
  17311. if(!css_props || !element.style) {
  17312. return;
  17313. }
  17314. // with css properties for modern browsers
  17315. for(var i = 0; i < vendors.length; i++) {
  17316. for(var p in css_props) {
  17317. if(css_props.hasOwnProperty(p)) {
  17318. prop = p;
  17319. // vender prefix at the property
  17320. if(vendors[i]) {
  17321. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  17322. }
  17323. // set the style
  17324. element.style[prop] = css_props[p];
  17325. }
  17326. }
  17327. }
  17328. // also the disable onselectstart
  17329. if(css_props.userSelect == 'none') {
  17330. element.onselectstart = function() {
  17331. return false;
  17332. };
  17333. }
  17334. }
  17335. };
  17336. Hammer.detection = {
  17337. // contains all registred Hammer.gestures in the correct order
  17338. gestures: [],
  17339. // data of the current Hammer.gesture detection session
  17340. current: null,
  17341. // the previous Hammer.gesture session data
  17342. // is a full clone of the previous gesture.current object
  17343. previous: null,
  17344. // when this becomes true, no gestures are fired
  17345. stopped: false,
  17346. /**
  17347. * start Hammer.gesture detection
  17348. * @param {Hammer.Instance} inst
  17349. * @param {Object} eventData
  17350. */
  17351. startDetect: function startDetect(inst, eventData) {
  17352. // already busy with a Hammer.gesture detection on an element
  17353. if(this.current) {
  17354. return;
  17355. }
  17356. this.stopped = false;
  17357. this.current = {
  17358. inst : inst, // reference to HammerInstance we're working for
  17359. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  17360. lastEvent : false, // last eventData
  17361. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  17362. };
  17363. this.detect(eventData);
  17364. },
  17365. /**
  17366. * Hammer.gesture detection
  17367. * @param {Object} eventData
  17368. * @param {Object} eventData
  17369. */
  17370. detect: function detect(eventData) {
  17371. if(!this.current || this.stopped) {
  17372. return;
  17373. }
  17374. // extend event data with calculations about scale, distance etc
  17375. eventData = this.extendEventData(eventData);
  17376. // instance options
  17377. var inst_options = this.current.inst.options;
  17378. // call Hammer.gesture handlers
  17379. for(var g=0,len=this.gestures.length; g<len; g++) {
  17380. var gesture = this.gestures[g];
  17381. // only when the instance options have enabled this gesture
  17382. if(!this.stopped && inst_options[gesture.name] !== false) {
  17383. // if a handler returns false, we stop with the detection
  17384. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  17385. this.stopDetect();
  17386. break;
  17387. }
  17388. }
  17389. }
  17390. // store as previous event event
  17391. if(this.current) {
  17392. this.current.lastEvent = eventData;
  17393. }
  17394. // endevent, but not the last touch, so dont stop
  17395. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  17396. this.stopDetect();
  17397. }
  17398. return eventData;
  17399. },
  17400. /**
  17401. * clear the Hammer.gesture vars
  17402. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  17403. * to stop other Hammer.gestures from being fired
  17404. */
  17405. stopDetect: function stopDetect() {
  17406. // clone current data to the store as the previous gesture
  17407. // used for the double tap gesture, since this is an other gesture detect session
  17408. this.previous = Hammer.utils.extend({}, this.current);
  17409. // reset the current
  17410. this.current = null;
  17411. // stopped!
  17412. this.stopped = true;
  17413. },
  17414. /**
  17415. * extend eventData for Hammer.gestures
  17416. * @param {Object} ev
  17417. * @returns {Object} ev
  17418. */
  17419. extendEventData: function extendEventData(ev) {
  17420. var startEv = this.current.startEvent;
  17421. // if the touches change, set the new touches over the startEvent touches
  17422. // this because touchevents don't have all the touches on touchstart, or the
  17423. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  17424. // but, sometimes it happens that both fingers are touching at the EXACT same time
  17425. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  17426. // extend 1 level deep to get the touchlist with the touch objects
  17427. startEv.touches = [];
  17428. for(var i=0,len=ev.touches.length; i<len; i++) {
  17429. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  17430. }
  17431. }
  17432. var delta_time = ev.timeStamp - startEv.timeStamp,
  17433. delta_x = ev.center.pageX - startEv.center.pageX,
  17434. delta_y = ev.center.pageY - startEv.center.pageY,
  17435. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  17436. Hammer.utils.extend(ev, {
  17437. deltaTime : delta_time,
  17438. deltaX : delta_x,
  17439. deltaY : delta_y,
  17440. velocityX : velocity.x,
  17441. velocityY : velocity.y,
  17442. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  17443. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  17444. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  17445. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  17446. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  17447. startEvent : startEv
  17448. });
  17449. return ev;
  17450. },
  17451. /**
  17452. * register new gesture
  17453. * @param {Object} gesture object, see gestures.js for documentation
  17454. * @returns {Array} gestures
  17455. */
  17456. register: function register(gesture) {
  17457. // add an enable gesture options if there is no given
  17458. var options = gesture.defaults || {};
  17459. if(options[gesture.name] === undefined) {
  17460. options[gesture.name] = true;
  17461. }
  17462. // extend Hammer default options with the Hammer.gesture options
  17463. Hammer.utils.extend(Hammer.defaults, options, true);
  17464. // set its index
  17465. gesture.index = gesture.index || 1000;
  17466. // add Hammer.gesture to the list
  17467. this.gestures.push(gesture);
  17468. // sort the list by index
  17469. this.gestures.sort(function(a, b) {
  17470. if (a.index < b.index) {
  17471. return -1;
  17472. }
  17473. if (a.index > b.index) {
  17474. return 1;
  17475. }
  17476. return 0;
  17477. });
  17478. return this.gestures;
  17479. }
  17480. };
  17481. Hammer.gestures = Hammer.gestures || {};
  17482. /**
  17483. * Custom gestures
  17484. * ==============================
  17485. *
  17486. * Gesture object
  17487. * --------------------
  17488. * The object structure of a gesture:
  17489. *
  17490. * { name: 'mygesture',
  17491. * index: 1337,
  17492. * defaults: {
  17493. * mygesture_option: true
  17494. * }
  17495. * handler: function(type, ev, inst) {
  17496. * // trigger gesture event
  17497. * inst.trigger(this.name, ev);
  17498. * }
  17499. * }
  17500. * @param {String} name
  17501. * this should be the name of the gesture, lowercase
  17502. * it is also being used to disable/enable the gesture per instance config.
  17503. *
  17504. * @param {Number} [index=1000]
  17505. * the index of the gesture, where it is going to be in the stack of gestures detection
  17506. * like when you build an gesture that depends on the drag gesture, it is a good
  17507. * idea to place it after the index of the drag gesture.
  17508. *
  17509. * @param {Object} [defaults={}]
  17510. * the default settings of the gesture. these are added to the instance settings,
  17511. * and can be overruled per instance. you can also add the name of the gesture,
  17512. * but this is also added by default (and set to true).
  17513. *
  17514. * @param {Function} handler
  17515. * this handles the gesture detection of your custom gesture and receives the
  17516. * following arguments:
  17517. *
  17518. * @param {Object} eventData
  17519. * event data containing the following properties:
  17520. * timeStamp {Number} time the event occurred
  17521. * target {HTMLElement} target element
  17522. * touches {Array} touches (fingers, pointers, mouse) on the screen
  17523. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  17524. * center {Object} center position of the touches. contains pageX and pageY
  17525. * deltaTime {Number} the total time of the touches in the screen
  17526. * deltaX {Number} the delta on x axis we haved moved
  17527. * deltaY {Number} the delta on y axis we haved moved
  17528. * velocityX {Number} the velocity on the x
  17529. * velocityY {Number} the velocity on y
  17530. * angle {Number} the angle we are moving
  17531. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  17532. * distance {Number} the distance we haved moved
  17533. * scale {Number} scaling of the touches, needs 2 touches
  17534. * rotation {Number} rotation of the touches, needs 2 touches *
  17535. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  17536. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  17537. * startEvent {Object} contains the same properties as above,
  17538. * but from the first touch. this is used to calculate
  17539. * distances, deltaTime, scaling etc
  17540. *
  17541. * @param {Hammer.Instance} inst
  17542. * the instance we are doing the detection for. you can get the options from
  17543. * the inst.options object and trigger the gesture event by calling inst.trigger
  17544. *
  17545. *
  17546. * Handle gestures
  17547. * --------------------
  17548. * inside the handler you can get/set Hammer.detection.current. This is the current
  17549. * detection session. It has the following properties
  17550. * @param {String} name
  17551. * contains the name of the gesture we have detected. it has not a real function,
  17552. * only to check in other gestures if something is detected.
  17553. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  17554. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  17555. *
  17556. * @readonly
  17557. * @param {Hammer.Instance} inst
  17558. * the instance we do the detection for
  17559. *
  17560. * @readonly
  17561. * @param {Object} startEvent
  17562. * contains the properties of the first gesture detection in this session.
  17563. * Used for calculations about timing, distance, etc.
  17564. *
  17565. * @readonly
  17566. * @param {Object} lastEvent
  17567. * contains all the properties of the last gesture detect in this session.
  17568. *
  17569. * after the gesture detection session has been completed (user has released the screen)
  17570. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  17571. * this is usefull for gestures like doubletap, where you need to know if the
  17572. * previous gesture was a tap
  17573. *
  17574. * options that have been set by the instance can be received by calling inst.options
  17575. *
  17576. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  17577. * The first param is the name of your gesture, the second the event argument
  17578. *
  17579. *
  17580. * Register gestures
  17581. * --------------------
  17582. * When an gesture is added to the Hammer.gestures object, it is auto registered
  17583. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  17584. * manually and pass your gesture object as a param
  17585. *
  17586. */
  17587. /**
  17588. * Hold
  17589. * Touch stays at the same place for x time
  17590. * @events hold
  17591. */
  17592. Hammer.gestures.Hold = {
  17593. name: 'hold',
  17594. index: 10,
  17595. defaults: {
  17596. hold_timeout : 500,
  17597. hold_threshold : 1
  17598. },
  17599. timer: null,
  17600. handler: function holdGesture(ev, inst) {
  17601. switch(ev.eventType) {
  17602. case Hammer.EVENT_START:
  17603. // clear any running timers
  17604. clearTimeout(this.timer);
  17605. // set the gesture so we can check in the timeout if it still is
  17606. Hammer.detection.current.name = this.name;
  17607. // set timer and if after the timeout it still is hold,
  17608. // we trigger the hold event
  17609. this.timer = setTimeout(function() {
  17610. if(Hammer.detection.current.name == 'hold') {
  17611. inst.trigger('hold', ev);
  17612. }
  17613. }, inst.options.hold_timeout);
  17614. break;
  17615. // when you move or end we clear the timer
  17616. case Hammer.EVENT_MOVE:
  17617. if(ev.distance > inst.options.hold_threshold) {
  17618. clearTimeout(this.timer);
  17619. }
  17620. break;
  17621. case Hammer.EVENT_END:
  17622. clearTimeout(this.timer);
  17623. break;
  17624. }
  17625. }
  17626. };
  17627. /**
  17628. * Tap/DoubleTap
  17629. * Quick touch at a place or double at the same place
  17630. * @events tap, doubletap
  17631. */
  17632. Hammer.gestures.Tap = {
  17633. name: 'tap',
  17634. index: 100,
  17635. defaults: {
  17636. tap_max_touchtime : 250,
  17637. tap_max_distance : 10,
  17638. tap_always : true,
  17639. doubletap_distance : 20,
  17640. doubletap_interval : 300
  17641. },
  17642. handler: function tapGesture(ev, inst) {
  17643. if(ev.eventType == Hammer.EVENT_END) {
  17644. // previous gesture, for the double tap since these are two different gesture detections
  17645. var prev = Hammer.detection.previous,
  17646. did_doubletap = false;
  17647. // when the touchtime is higher then the max touch time
  17648. // or when the moving distance is too much
  17649. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  17650. ev.distance > inst.options.tap_max_distance) {
  17651. return;
  17652. }
  17653. // check if double tap
  17654. if(prev && prev.name == 'tap' &&
  17655. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  17656. ev.distance < inst.options.doubletap_distance) {
  17657. inst.trigger('doubletap', ev);
  17658. did_doubletap = true;
  17659. }
  17660. // do a single tap
  17661. if(!did_doubletap || inst.options.tap_always) {
  17662. Hammer.detection.current.name = 'tap';
  17663. inst.trigger(Hammer.detection.current.name, ev);
  17664. }
  17665. }
  17666. }
  17667. };
  17668. /**
  17669. * Swipe
  17670. * triggers swipe events when the end velocity is above the threshold
  17671. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  17672. */
  17673. Hammer.gestures.Swipe = {
  17674. name: 'swipe',
  17675. index: 40,
  17676. defaults: {
  17677. // set 0 for unlimited, but this can conflict with transform
  17678. swipe_max_touches : 1,
  17679. swipe_velocity : 0.7
  17680. },
  17681. handler: function swipeGesture(ev, inst) {
  17682. if(ev.eventType == Hammer.EVENT_END) {
  17683. // max touches
  17684. if(inst.options.swipe_max_touches > 0 &&
  17685. ev.touches.length > inst.options.swipe_max_touches) {
  17686. return;
  17687. }
  17688. // when the distance we moved is too small we skip this gesture
  17689. // or we can be already in dragging
  17690. if(ev.velocityX > inst.options.swipe_velocity ||
  17691. ev.velocityY > inst.options.swipe_velocity) {
  17692. // trigger swipe events
  17693. inst.trigger(this.name, ev);
  17694. inst.trigger(this.name + ev.direction, ev);
  17695. }
  17696. }
  17697. }
  17698. };
  17699. /**
  17700. * Drag
  17701. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  17702. * moving left and right is a good practice. When all the drag events are blocking
  17703. * you disable scrolling on that area.
  17704. * @events drag, drapleft, dragright, dragup, dragdown
  17705. */
  17706. Hammer.gestures.Drag = {
  17707. name: 'drag',
  17708. index: 50,
  17709. defaults: {
  17710. drag_min_distance : 10,
  17711. // set 0 for unlimited, but this can conflict with transform
  17712. drag_max_touches : 1,
  17713. // prevent default browser behavior when dragging occurs
  17714. // be careful with it, it makes the element a blocking element
  17715. // when you are using the drag gesture, it is a good practice to set this true
  17716. drag_block_horizontal : false,
  17717. drag_block_vertical : false,
  17718. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  17719. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  17720. drag_lock_to_axis : false,
  17721. // drag lock only kicks in when distance > drag_lock_min_distance
  17722. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  17723. drag_lock_min_distance : 25
  17724. },
  17725. triggered: false,
  17726. handler: function dragGesture(ev, inst) {
  17727. // current gesture isnt drag, but dragged is true
  17728. // this means an other gesture is busy. now call dragend
  17729. if(Hammer.detection.current.name != this.name && this.triggered) {
  17730. inst.trigger(this.name +'end', ev);
  17731. this.triggered = false;
  17732. return;
  17733. }
  17734. // max touches
  17735. if(inst.options.drag_max_touches > 0 &&
  17736. ev.touches.length > inst.options.drag_max_touches) {
  17737. return;
  17738. }
  17739. switch(ev.eventType) {
  17740. case Hammer.EVENT_START:
  17741. this.triggered = false;
  17742. break;
  17743. case Hammer.EVENT_MOVE:
  17744. // when the distance we moved is too small we skip this gesture
  17745. // or we can be already in dragging
  17746. if(ev.distance < inst.options.drag_min_distance &&
  17747. Hammer.detection.current.name != this.name) {
  17748. return;
  17749. }
  17750. // we are dragging!
  17751. Hammer.detection.current.name = this.name;
  17752. // lock drag to axis?
  17753. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  17754. ev.drag_locked_to_axis = true;
  17755. }
  17756. var last_direction = Hammer.detection.current.lastEvent.direction;
  17757. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  17758. // keep direction on the axis that the drag gesture started on
  17759. if(Hammer.utils.isVertical(last_direction)) {
  17760. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  17761. }
  17762. else {
  17763. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  17764. }
  17765. }
  17766. // first time, trigger dragstart event
  17767. if(!this.triggered) {
  17768. inst.trigger(this.name +'start', ev);
  17769. this.triggered = true;
  17770. }
  17771. // trigger normal event
  17772. inst.trigger(this.name, ev);
  17773. // direction event, like dragdown
  17774. inst.trigger(this.name + ev.direction, ev);
  17775. // block the browser events
  17776. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  17777. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  17778. ev.preventDefault();
  17779. }
  17780. break;
  17781. case Hammer.EVENT_END:
  17782. // trigger dragend
  17783. if(this.triggered) {
  17784. inst.trigger(this.name +'end', ev);
  17785. }
  17786. this.triggered = false;
  17787. break;
  17788. }
  17789. }
  17790. };
  17791. /**
  17792. * Transform
  17793. * User want to scale or rotate with 2 fingers
  17794. * @events transform, pinch, pinchin, pinchout, rotate
  17795. */
  17796. Hammer.gestures.Transform = {
  17797. name: 'transform',
  17798. index: 45,
  17799. defaults: {
  17800. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  17801. transform_min_scale : 0.01,
  17802. // rotation in degrees
  17803. transform_min_rotation : 1,
  17804. // prevent default browser behavior when two touches are on the screen
  17805. // but it makes the element a blocking element
  17806. // when you are using the transform gesture, it is a good practice to set this true
  17807. transform_always_block : false
  17808. },
  17809. triggered: false,
  17810. handler: function transformGesture(ev, inst) {
  17811. // current gesture isnt drag, but dragged is true
  17812. // this means an other gesture is busy. now call dragend
  17813. if(Hammer.detection.current.name != this.name && this.triggered) {
  17814. inst.trigger(this.name +'end', ev);
  17815. this.triggered = false;
  17816. return;
  17817. }
  17818. // atleast multitouch
  17819. if(ev.touches.length < 2) {
  17820. return;
  17821. }
  17822. // prevent default when two fingers are on the screen
  17823. if(inst.options.transform_always_block) {
  17824. ev.preventDefault();
  17825. }
  17826. switch(ev.eventType) {
  17827. case Hammer.EVENT_START:
  17828. this.triggered = false;
  17829. break;
  17830. case Hammer.EVENT_MOVE:
  17831. var scale_threshold = Math.abs(1-ev.scale);
  17832. var rotation_threshold = Math.abs(ev.rotation);
  17833. // when the distance we moved is too small we skip this gesture
  17834. // or we can be already in dragging
  17835. if(scale_threshold < inst.options.transform_min_scale &&
  17836. rotation_threshold < inst.options.transform_min_rotation) {
  17837. return;
  17838. }
  17839. // we are transforming!
  17840. Hammer.detection.current.name = this.name;
  17841. // first time, trigger dragstart event
  17842. if(!this.triggered) {
  17843. inst.trigger(this.name +'start', ev);
  17844. this.triggered = true;
  17845. }
  17846. inst.trigger(this.name, ev); // basic transform event
  17847. // trigger rotate event
  17848. if(rotation_threshold > inst.options.transform_min_rotation) {
  17849. inst.trigger('rotate', ev);
  17850. }
  17851. // trigger pinch event
  17852. if(scale_threshold > inst.options.transform_min_scale) {
  17853. inst.trigger('pinch', ev);
  17854. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  17855. }
  17856. break;
  17857. case Hammer.EVENT_END:
  17858. // trigger dragend
  17859. if(this.triggered) {
  17860. inst.trigger(this.name +'end', ev);
  17861. }
  17862. this.triggered = false;
  17863. break;
  17864. }
  17865. }
  17866. };
  17867. /**
  17868. * Touch
  17869. * Called as first, tells the user has touched the screen
  17870. * @events touch
  17871. */
  17872. Hammer.gestures.Touch = {
  17873. name: 'touch',
  17874. index: -Infinity,
  17875. defaults: {
  17876. // call preventDefault at touchstart, and makes the element blocking by
  17877. // disabling the scrolling of the page, but it improves gestures like
  17878. // transforming and dragging.
  17879. // be careful with using this, it can be very annoying for users to be stuck
  17880. // on the page
  17881. prevent_default: false,
  17882. // disable mouse events, so only touch (or pen!) input triggers events
  17883. prevent_mouseevents: false
  17884. },
  17885. handler: function touchGesture(ev, inst) {
  17886. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  17887. ev.stopDetect();
  17888. return;
  17889. }
  17890. if(inst.options.prevent_default) {
  17891. ev.preventDefault();
  17892. }
  17893. if(ev.eventType == Hammer.EVENT_START) {
  17894. inst.trigger(this.name, ev);
  17895. }
  17896. }
  17897. };
  17898. /**
  17899. * Release
  17900. * Called as last, tells the user has released the screen
  17901. * @events release
  17902. */
  17903. Hammer.gestures.Release = {
  17904. name: 'release',
  17905. index: Infinity,
  17906. handler: function releaseGesture(ev, inst) {
  17907. if(ev.eventType == Hammer.EVENT_END) {
  17908. inst.trigger(this.name, ev);
  17909. }
  17910. }
  17911. };
  17912. // node export
  17913. if(typeof module === 'object' && typeof module.exports === 'object'){
  17914. module.exports = Hammer;
  17915. }
  17916. // just window export
  17917. else {
  17918. window.Hammer = Hammer;
  17919. // requireJS module definition
  17920. if(typeof window.define === 'function' && window.define.amd) {
  17921. window.define('hammer', [], function() {
  17922. return Hammer;
  17923. });
  17924. }
  17925. }
  17926. })(this);
  17927. },{}],4:[function(require,module,exports){
  17928. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  17929. //! version : 2.6.0
  17930. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  17931. //! license : MIT
  17932. //! momentjs.com
  17933. (function (undefined) {
  17934. /************************************
  17935. Constants
  17936. ************************************/
  17937. var moment,
  17938. VERSION = "2.6.0",
  17939. // the global-scope this is NOT the global object in Node.js
  17940. globalScope = typeof global !== 'undefined' ? global : this,
  17941. oldGlobalMoment,
  17942. round = Math.round,
  17943. i,
  17944. YEAR = 0,
  17945. MONTH = 1,
  17946. DATE = 2,
  17947. HOUR = 3,
  17948. MINUTE = 4,
  17949. SECOND = 5,
  17950. MILLISECOND = 6,
  17951. // internal storage for language config files
  17952. languages = {},
  17953. // moment internal properties
  17954. momentProperties = {
  17955. _isAMomentObject: null,
  17956. _i : null,
  17957. _f : null,
  17958. _l : null,
  17959. _strict : null,
  17960. _isUTC : null,
  17961. _offset : null, // optional. Combine with _isUTC
  17962. _pf : null,
  17963. _lang : null // optional
  17964. },
  17965. // check for nodeJS
  17966. hasModule = (typeof module !== 'undefined' && module.exports),
  17967. // ASP.NET json date format regex
  17968. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  17969. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  17970. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  17971. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  17972. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  17973. // format tokens
  17974. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  17975. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  17976. // parsing token regexes
  17977. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  17978. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  17979. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  17980. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  17981. parseTokenDigits = /\d+/, // nonzero number of digits
  17982. parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
  17983. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  17984. parseTokenT = /T/i, // T (ISO separator)
  17985. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  17986. parseTokenOrdinal = /\d{1,2}/,
  17987. //strict parsing regexes
  17988. parseTokenOneDigit = /\d/, // 0 - 9
  17989. parseTokenTwoDigits = /\d\d/, // 00 - 99
  17990. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  17991. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  17992. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  17993. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  17994. // iso 8601 regex
  17995. // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
  17996. isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
  17997. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  17998. isoDates = [
  17999. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  18000. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  18001. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  18002. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  18003. ['YYYY-DDD', /\d{4}-\d{3}/]
  18004. ],
  18005. // iso time formats and regexes
  18006. isoTimes = [
  18007. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  18008. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  18009. ['HH:mm', /(T| )\d\d:\d\d/],
  18010. ['HH', /(T| )\d\d/]
  18011. ],
  18012. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  18013. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  18014. // getter and setter names
  18015. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  18016. unitMillisecondFactors = {
  18017. 'Milliseconds' : 1,
  18018. 'Seconds' : 1e3,
  18019. 'Minutes' : 6e4,
  18020. 'Hours' : 36e5,
  18021. 'Days' : 864e5,
  18022. 'Months' : 2592e6,
  18023. 'Years' : 31536e6
  18024. },
  18025. unitAliases = {
  18026. ms : 'millisecond',
  18027. s : 'second',
  18028. m : 'minute',
  18029. h : 'hour',
  18030. d : 'day',
  18031. D : 'date',
  18032. w : 'week',
  18033. W : 'isoWeek',
  18034. M : 'month',
  18035. Q : 'quarter',
  18036. y : 'year',
  18037. DDD : 'dayOfYear',
  18038. e : 'weekday',
  18039. E : 'isoWeekday',
  18040. gg: 'weekYear',
  18041. GG: 'isoWeekYear'
  18042. },
  18043. camelFunctions = {
  18044. dayofyear : 'dayOfYear',
  18045. isoweekday : 'isoWeekday',
  18046. isoweek : 'isoWeek',
  18047. weekyear : 'weekYear',
  18048. isoweekyear : 'isoWeekYear'
  18049. },
  18050. // format function strings
  18051. formatFunctions = {},
  18052. // tokens to ordinalize and pad
  18053. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  18054. paddedTokens = 'M D H h m s w W'.split(' '),
  18055. formatTokenFunctions = {
  18056. M : function () {
  18057. return this.month() + 1;
  18058. },
  18059. MMM : function (format) {
  18060. return this.lang().monthsShort(this, format);
  18061. },
  18062. MMMM : function (format) {
  18063. return this.lang().months(this, format);
  18064. },
  18065. D : function () {
  18066. return this.date();
  18067. },
  18068. DDD : function () {
  18069. return this.dayOfYear();
  18070. },
  18071. d : function () {
  18072. return this.day();
  18073. },
  18074. dd : function (format) {
  18075. return this.lang().weekdaysMin(this, format);
  18076. },
  18077. ddd : function (format) {
  18078. return this.lang().weekdaysShort(this, format);
  18079. },
  18080. dddd : function (format) {
  18081. return this.lang().weekdays(this, format);
  18082. },
  18083. w : function () {
  18084. return this.week();
  18085. },
  18086. W : function () {
  18087. return this.isoWeek();
  18088. },
  18089. YY : function () {
  18090. return leftZeroFill(this.year() % 100, 2);
  18091. },
  18092. YYYY : function () {
  18093. return leftZeroFill(this.year(), 4);
  18094. },
  18095. YYYYY : function () {
  18096. return leftZeroFill(this.year(), 5);
  18097. },
  18098. YYYYYY : function () {
  18099. var y = this.year(), sign = y >= 0 ? '+' : '-';
  18100. return sign + leftZeroFill(Math.abs(y), 6);
  18101. },
  18102. gg : function () {
  18103. return leftZeroFill(this.weekYear() % 100, 2);
  18104. },
  18105. gggg : function () {
  18106. return leftZeroFill(this.weekYear(), 4);
  18107. },
  18108. ggggg : function () {
  18109. return leftZeroFill(this.weekYear(), 5);
  18110. },
  18111. GG : function () {
  18112. return leftZeroFill(this.isoWeekYear() % 100, 2);
  18113. },
  18114. GGGG : function () {
  18115. return leftZeroFill(this.isoWeekYear(), 4);
  18116. },
  18117. GGGGG : function () {
  18118. return leftZeroFill(this.isoWeekYear(), 5);
  18119. },
  18120. e : function () {
  18121. return this.weekday();
  18122. },
  18123. E : function () {
  18124. return this.isoWeekday();
  18125. },
  18126. a : function () {
  18127. return this.lang().meridiem(this.hours(), this.minutes(), true);
  18128. },
  18129. A : function () {
  18130. return this.lang().meridiem(this.hours(), this.minutes(), false);
  18131. },
  18132. H : function () {
  18133. return this.hours();
  18134. },
  18135. h : function () {
  18136. return this.hours() % 12 || 12;
  18137. },
  18138. m : function () {
  18139. return this.minutes();
  18140. },
  18141. s : function () {
  18142. return this.seconds();
  18143. },
  18144. S : function () {
  18145. return toInt(this.milliseconds() / 100);
  18146. },
  18147. SS : function () {
  18148. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  18149. },
  18150. SSS : function () {
  18151. return leftZeroFill(this.milliseconds(), 3);
  18152. },
  18153. SSSS : function () {
  18154. return leftZeroFill(this.milliseconds(), 3);
  18155. },
  18156. Z : function () {
  18157. var a = -this.zone(),
  18158. b = "+";
  18159. if (a < 0) {
  18160. a = -a;
  18161. b = "-";
  18162. }
  18163. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  18164. },
  18165. ZZ : function () {
  18166. var a = -this.zone(),
  18167. b = "+";
  18168. if (a < 0) {
  18169. a = -a;
  18170. b = "-";
  18171. }
  18172. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  18173. },
  18174. z : function () {
  18175. return this.zoneAbbr();
  18176. },
  18177. zz : function () {
  18178. return this.zoneName();
  18179. },
  18180. X : function () {
  18181. return this.unix();
  18182. },
  18183. Q : function () {
  18184. return this.quarter();
  18185. }
  18186. },
  18187. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  18188. function defaultParsingFlags() {
  18189. // We need to deep clone this object, and es5 standard is not very
  18190. // helpful.
  18191. return {
  18192. empty : false,
  18193. unusedTokens : [],
  18194. unusedInput : [],
  18195. overflow : -2,
  18196. charsLeftOver : 0,
  18197. nullInput : false,
  18198. invalidMonth : null,
  18199. invalidFormat : false,
  18200. userInvalidated : false,
  18201. iso: false
  18202. };
  18203. }
  18204. function deprecate(msg, fn) {
  18205. var firstTime = true;
  18206. function printMsg() {
  18207. if (moment.suppressDeprecationWarnings === false &&
  18208. typeof console !== 'undefined' && console.warn) {
  18209. console.warn("Deprecation warning: " + msg);
  18210. }
  18211. }
  18212. return extend(function () {
  18213. if (firstTime) {
  18214. printMsg();
  18215. firstTime = false;
  18216. }
  18217. return fn.apply(this, arguments);
  18218. }, fn);
  18219. }
  18220. function padToken(func, count) {
  18221. return function (a) {
  18222. return leftZeroFill(func.call(this, a), count);
  18223. };
  18224. }
  18225. function ordinalizeToken(func, period) {
  18226. return function (a) {
  18227. return this.lang().ordinal(func.call(this, a), period);
  18228. };
  18229. }
  18230. while (ordinalizeTokens.length) {
  18231. i = ordinalizeTokens.pop();
  18232. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  18233. }
  18234. while (paddedTokens.length) {
  18235. i = paddedTokens.pop();
  18236. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  18237. }
  18238. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  18239. /************************************
  18240. Constructors
  18241. ************************************/
  18242. function Language() {
  18243. }
  18244. // Moment prototype object
  18245. function Moment(config) {
  18246. checkOverflow(config);
  18247. extend(this, config);
  18248. }
  18249. // Duration Constructor
  18250. function Duration(duration) {
  18251. var normalizedInput = normalizeObjectUnits(duration),
  18252. years = normalizedInput.year || 0,
  18253. quarters = normalizedInput.quarter || 0,
  18254. months = normalizedInput.month || 0,
  18255. weeks = normalizedInput.week || 0,
  18256. days = normalizedInput.day || 0,
  18257. hours = normalizedInput.hour || 0,
  18258. minutes = normalizedInput.minute || 0,
  18259. seconds = normalizedInput.second || 0,
  18260. milliseconds = normalizedInput.millisecond || 0;
  18261. // representation for dateAddRemove
  18262. this._milliseconds = +milliseconds +
  18263. seconds * 1e3 + // 1000
  18264. minutes * 6e4 + // 1000 * 60
  18265. hours * 36e5; // 1000 * 60 * 60
  18266. // Because of dateAddRemove treats 24 hours as different from a
  18267. // day when working around DST, we need to store them separately
  18268. this._days = +days +
  18269. weeks * 7;
  18270. // It is impossible translate months into days without knowing
  18271. // which months you are are talking about, so we have to store
  18272. // it separately.
  18273. this._months = +months +
  18274. quarters * 3 +
  18275. years * 12;
  18276. this._data = {};
  18277. this._bubble();
  18278. }
  18279. /************************************
  18280. Helpers
  18281. ************************************/
  18282. function extend(a, b) {
  18283. for (var i in b) {
  18284. if (b.hasOwnProperty(i)) {
  18285. a[i] = b[i];
  18286. }
  18287. }
  18288. if (b.hasOwnProperty("toString")) {
  18289. a.toString = b.toString;
  18290. }
  18291. if (b.hasOwnProperty("valueOf")) {
  18292. a.valueOf = b.valueOf;
  18293. }
  18294. return a;
  18295. }
  18296. function cloneMoment(m) {
  18297. var result = {}, i;
  18298. for (i in m) {
  18299. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  18300. result[i] = m[i];
  18301. }
  18302. }
  18303. return result;
  18304. }
  18305. function absRound(number) {
  18306. if (number < 0) {
  18307. return Math.ceil(number);
  18308. } else {
  18309. return Math.floor(number);
  18310. }
  18311. }
  18312. // left zero fill a number
  18313. // see http://jsperf.com/left-zero-filling for performance comparison
  18314. function leftZeroFill(number, targetLength, forceSign) {
  18315. var output = '' + Math.abs(number),
  18316. sign = number >= 0;
  18317. while (output.length < targetLength) {
  18318. output = '0' + output;
  18319. }
  18320. return (sign ? (forceSign ? '+' : '') : '-') + output;
  18321. }
  18322. // helper function for _.addTime and _.subtractTime
  18323. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  18324. var milliseconds = duration._milliseconds,
  18325. days = duration._days,
  18326. months = duration._months;
  18327. updateOffset = updateOffset == null ? true : updateOffset;
  18328. if (milliseconds) {
  18329. mom._d.setTime(+mom._d + milliseconds * isAdding);
  18330. }
  18331. if (days) {
  18332. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  18333. }
  18334. if (months) {
  18335. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  18336. }
  18337. if (updateOffset) {
  18338. moment.updateOffset(mom, days || months);
  18339. }
  18340. }
  18341. // check if is an array
  18342. function isArray(input) {
  18343. return Object.prototype.toString.call(input) === '[object Array]';
  18344. }
  18345. function isDate(input) {
  18346. return Object.prototype.toString.call(input) === '[object Date]' ||
  18347. input instanceof Date;
  18348. }
  18349. // compare two arrays, return the number of differences
  18350. function compareArrays(array1, array2, dontConvert) {
  18351. var len = Math.min(array1.length, array2.length),
  18352. lengthDiff = Math.abs(array1.length - array2.length),
  18353. diffs = 0,
  18354. i;
  18355. for (i = 0; i < len; i++) {
  18356. if ((dontConvert && array1[i] !== array2[i]) ||
  18357. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  18358. diffs++;
  18359. }
  18360. }
  18361. return diffs + lengthDiff;
  18362. }
  18363. function normalizeUnits(units) {
  18364. if (units) {
  18365. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  18366. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  18367. }
  18368. return units;
  18369. }
  18370. function normalizeObjectUnits(inputObject) {
  18371. var normalizedInput = {},
  18372. normalizedProp,
  18373. prop;
  18374. for (prop in inputObject) {
  18375. if (inputObject.hasOwnProperty(prop)) {
  18376. normalizedProp = normalizeUnits(prop);
  18377. if (normalizedProp) {
  18378. normalizedInput[normalizedProp] = inputObject[prop];
  18379. }
  18380. }
  18381. }
  18382. return normalizedInput;
  18383. }
  18384. function makeList(field) {
  18385. var count, setter;
  18386. if (field.indexOf('week') === 0) {
  18387. count = 7;
  18388. setter = 'day';
  18389. }
  18390. else if (field.indexOf('month') === 0) {
  18391. count = 12;
  18392. setter = 'month';
  18393. }
  18394. else {
  18395. return;
  18396. }
  18397. moment[field] = function (format, index) {
  18398. var i, getter,
  18399. method = moment.fn._lang[field],
  18400. results = [];
  18401. if (typeof format === 'number') {
  18402. index = format;
  18403. format = undefined;
  18404. }
  18405. getter = function (i) {
  18406. var m = moment().utc().set(setter, i);
  18407. return method.call(moment.fn._lang, m, format || '');
  18408. };
  18409. if (index != null) {
  18410. return getter(index);
  18411. }
  18412. else {
  18413. for (i = 0; i < count; i++) {
  18414. results.push(getter(i));
  18415. }
  18416. return results;
  18417. }
  18418. };
  18419. }
  18420. function toInt(argumentForCoercion) {
  18421. var coercedNumber = +argumentForCoercion,
  18422. value = 0;
  18423. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  18424. if (coercedNumber >= 0) {
  18425. value = Math.floor(coercedNumber);
  18426. } else {
  18427. value = Math.ceil(coercedNumber);
  18428. }
  18429. }
  18430. return value;
  18431. }
  18432. function daysInMonth(year, month) {
  18433. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  18434. }
  18435. function weeksInYear(year, dow, doy) {
  18436. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  18437. }
  18438. function daysInYear(year) {
  18439. return isLeapYear(year) ? 366 : 365;
  18440. }
  18441. function isLeapYear(year) {
  18442. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  18443. }
  18444. function checkOverflow(m) {
  18445. var overflow;
  18446. if (m._a && m._pf.overflow === -2) {
  18447. overflow =
  18448. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  18449. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  18450. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  18451. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  18452. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  18453. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  18454. -1;
  18455. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  18456. overflow = DATE;
  18457. }
  18458. m._pf.overflow = overflow;
  18459. }
  18460. }
  18461. function isValid(m) {
  18462. if (m._isValid == null) {
  18463. m._isValid = !isNaN(m._d.getTime()) &&
  18464. m._pf.overflow < 0 &&
  18465. !m._pf.empty &&
  18466. !m._pf.invalidMonth &&
  18467. !m._pf.nullInput &&
  18468. !m._pf.invalidFormat &&
  18469. !m._pf.userInvalidated;
  18470. if (m._strict) {
  18471. m._isValid = m._isValid &&
  18472. m._pf.charsLeftOver === 0 &&
  18473. m._pf.unusedTokens.length === 0;
  18474. }
  18475. }
  18476. return m._isValid;
  18477. }
  18478. function normalizeLanguage(key) {
  18479. return key ? key.toLowerCase().replace('_', '-') : key;
  18480. }
  18481. // Return a moment from input, that is local/utc/zone equivalent to model.
  18482. function makeAs(input, model) {
  18483. return model._isUTC ? moment(input).zone(model._offset || 0) :
  18484. moment(input).local();
  18485. }
  18486. /************************************
  18487. Languages
  18488. ************************************/
  18489. extend(Language.prototype, {
  18490. set : function (config) {
  18491. var prop, i;
  18492. for (i in config) {
  18493. prop = config[i];
  18494. if (typeof prop === 'function') {
  18495. this[i] = prop;
  18496. } else {
  18497. this['_' + i] = prop;
  18498. }
  18499. }
  18500. },
  18501. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  18502. months : function (m) {
  18503. return this._months[m.month()];
  18504. },
  18505. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  18506. monthsShort : function (m) {
  18507. return this._monthsShort[m.month()];
  18508. },
  18509. monthsParse : function (monthName) {
  18510. var i, mom, regex;
  18511. if (!this._monthsParse) {
  18512. this._monthsParse = [];
  18513. }
  18514. for (i = 0; i < 12; i++) {
  18515. // make the regex if we don't have it already
  18516. if (!this._monthsParse[i]) {
  18517. mom = moment.utc([2000, i]);
  18518. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  18519. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  18520. }
  18521. // test the regex
  18522. if (this._monthsParse[i].test(monthName)) {
  18523. return i;
  18524. }
  18525. }
  18526. },
  18527. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  18528. weekdays : function (m) {
  18529. return this._weekdays[m.day()];
  18530. },
  18531. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  18532. weekdaysShort : function (m) {
  18533. return this._weekdaysShort[m.day()];
  18534. },
  18535. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  18536. weekdaysMin : function (m) {
  18537. return this._weekdaysMin[m.day()];
  18538. },
  18539. weekdaysParse : function (weekdayName) {
  18540. var i, mom, regex;
  18541. if (!this._weekdaysParse) {
  18542. this._weekdaysParse = [];
  18543. }
  18544. for (i = 0; i < 7; i++) {
  18545. // make the regex if we don't have it already
  18546. if (!this._weekdaysParse[i]) {
  18547. mom = moment([2000, 1]).day(i);
  18548. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  18549. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  18550. }
  18551. // test the regex
  18552. if (this._weekdaysParse[i].test(weekdayName)) {
  18553. return i;
  18554. }
  18555. }
  18556. },
  18557. _longDateFormat : {
  18558. LT : "h:mm A",
  18559. L : "MM/DD/YYYY",
  18560. LL : "MMMM D YYYY",
  18561. LLL : "MMMM D YYYY LT",
  18562. LLLL : "dddd, MMMM D YYYY LT"
  18563. },
  18564. longDateFormat : function (key) {
  18565. var output = this._longDateFormat[key];
  18566. if (!output && this._longDateFormat[key.toUpperCase()]) {
  18567. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  18568. return val.slice(1);
  18569. });
  18570. this._longDateFormat[key] = output;
  18571. }
  18572. return output;
  18573. },
  18574. isPM : function (input) {
  18575. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  18576. // Using charAt should be more compatible.
  18577. return ((input + '').toLowerCase().charAt(0) === 'p');
  18578. },
  18579. _meridiemParse : /[ap]\.?m?\.?/i,
  18580. meridiem : function (hours, minutes, isLower) {
  18581. if (hours > 11) {
  18582. return isLower ? 'pm' : 'PM';
  18583. } else {
  18584. return isLower ? 'am' : 'AM';
  18585. }
  18586. },
  18587. _calendar : {
  18588. sameDay : '[Today at] LT',
  18589. nextDay : '[Tomorrow at] LT',
  18590. nextWeek : 'dddd [at] LT',
  18591. lastDay : '[Yesterday at] LT',
  18592. lastWeek : '[Last] dddd [at] LT',
  18593. sameElse : 'L'
  18594. },
  18595. calendar : function (key, mom) {
  18596. var output = this._calendar[key];
  18597. return typeof output === 'function' ? output.apply(mom) : output;
  18598. },
  18599. _relativeTime : {
  18600. future : "in %s",
  18601. past : "%s ago",
  18602. s : "a few seconds",
  18603. m : "a minute",
  18604. mm : "%d minutes",
  18605. h : "an hour",
  18606. hh : "%d hours",
  18607. d : "a day",
  18608. dd : "%d days",
  18609. M : "a month",
  18610. MM : "%d months",
  18611. y : "a year",
  18612. yy : "%d years"
  18613. },
  18614. relativeTime : function (number, withoutSuffix, string, isFuture) {
  18615. var output = this._relativeTime[string];
  18616. return (typeof output === 'function') ?
  18617. output(number, withoutSuffix, string, isFuture) :
  18618. output.replace(/%d/i, number);
  18619. },
  18620. pastFuture : function (diff, output) {
  18621. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  18622. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  18623. },
  18624. ordinal : function (number) {
  18625. return this._ordinal.replace("%d", number);
  18626. },
  18627. _ordinal : "%d",
  18628. preparse : function (string) {
  18629. return string;
  18630. },
  18631. postformat : function (string) {
  18632. return string;
  18633. },
  18634. week : function (mom) {
  18635. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  18636. },
  18637. _week : {
  18638. dow : 0, // Sunday is the first day of the week.
  18639. doy : 6 // The week that contains Jan 1st is the first week of the year.
  18640. },
  18641. _invalidDate: 'Invalid date',
  18642. invalidDate: function () {
  18643. return this._invalidDate;
  18644. }
  18645. });
  18646. // Loads a language definition into the `languages` cache. The function
  18647. // takes a key and optionally values. If not in the browser and no values
  18648. // are provided, it will load the language file module. As a convenience,
  18649. // this function also returns the language values.
  18650. function loadLang(key, values) {
  18651. values.abbr = key;
  18652. if (!languages[key]) {
  18653. languages[key] = new Language();
  18654. }
  18655. languages[key].set(values);
  18656. return languages[key];
  18657. }
  18658. // Remove a language from the `languages` cache. Mostly useful in tests.
  18659. function unloadLang(key) {
  18660. delete languages[key];
  18661. }
  18662. // Determines which language definition to use and returns it.
  18663. //
  18664. // With no parameters, it will return the global language. If you
  18665. // pass in a language key, such as 'en', it will return the
  18666. // definition for 'en', so long as 'en' has already been loaded using
  18667. // moment.lang.
  18668. function getLangDefinition(key) {
  18669. var i = 0, j, lang, next, split,
  18670. get = function (k) {
  18671. if (!languages[k] && hasModule) {
  18672. try {
  18673. require('./lang/' + k);
  18674. } catch (e) { }
  18675. }
  18676. return languages[k];
  18677. };
  18678. if (!key) {
  18679. return moment.fn._lang;
  18680. }
  18681. if (!isArray(key)) {
  18682. //short-circuit everything else
  18683. lang = get(key);
  18684. if (lang) {
  18685. return lang;
  18686. }
  18687. key = [key];
  18688. }
  18689. //pick the language from the array
  18690. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  18691. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  18692. while (i < key.length) {
  18693. split = normalizeLanguage(key[i]).split('-');
  18694. j = split.length;
  18695. next = normalizeLanguage(key[i + 1]);
  18696. next = next ? next.split('-') : null;
  18697. while (j > 0) {
  18698. lang = get(split.slice(0, j).join('-'));
  18699. if (lang) {
  18700. return lang;
  18701. }
  18702. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  18703. //the next array item is better than a shallower substring of this one
  18704. break;
  18705. }
  18706. j--;
  18707. }
  18708. i++;
  18709. }
  18710. return moment.fn._lang;
  18711. }
  18712. /************************************
  18713. Formatting
  18714. ************************************/
  18715. function removeFormattingTokens(input) {
  18716. if (input.match(/\[[\s\S]/)) {
  18717. return input.replace(/^\[|\]$/g, "");
  18718. }
  18719. return input.replace(/\\/g, "");
  18720. }
  18721. function makeFormatFunction(format) {
  18722. var array = format.match(formattingTokens), i, length;
  18723. for (i = 0, length = array.length; i < length; i++) {
  18724. if (formatTokenFunctions[array[i]]) {
  18725. array[i] = formatTokenFunctions[array[i]];
  18726. } else {
  18727. array[i] = removeFormattingTokens(array[i]);
  18728. }
  18729. }
  18730. return function (mom) {
  18731. var output = "";
  18732. for (i = 0; i < length; i++) {
  18733. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  18734. }
  18735. return output;
  18736. };
  18737. }
  18738. // format date using native date object
  18739. function formatMoment(m, format) {
  18740. if (!m.isValid()) {
  18741. return m.lang().invalidDate();
  18742. }
  18743. format = expandFormat(format, m.lang());
  18744. if (!formatFunctions[format]) {
  18745. formatFunctions[format] = makeFormatFunction(format);
  18746. }
  18747. return formatFunctions[format](m);
  18748. }
  18749. function expandFormat(format, lang) {
  18750. var i = 5;
  18751. function replaceLongDateFormatTokens(input) {
  18752. return lang.longDateFormat(input) || input;
  18753. }
  18754. localFormattingTokens.lastIndex = 0;
  18755. while (i >= 0 && localFormattingTokens.test(format)) {
  18756. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  18757. localFormattingTokens.lastIndex = 0;
  18758. i -= 1;
  18759. }
  18760. return format;
  18761. }
  18762. /************************************
  18763. Parsing
  18764. ************************************/
  18765. // get the regex to find the next token
  18766. function getParseRegexForToken(token, config) {
  18767. var a, strict = config._strict;
  18768. switch (token) {
  18769. case 'Q':
  18770. return parseTokenOneDigit;
  18771. case 'DDDD':
  18772. return parseTokenThreeDigits;
  18773. case 'YYYY':
  18774. case 'GGGG':
  18775. case 'gggg':
  18776. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  18777. case 'Y':
  18778. case 'G':
  18779. case 'g':
  18780. return parseTokenSignedNumber;
  18781. case 'YYYYYY':
  18782. case 'YYYYY':
  18783. case 'GGGGG':
  18784. case 'ggggg':
  18785. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  18786. case 'S':
  18787. if (strict) { return parseTokenOneDigit; }
  18788. /* falls through */
  18789. case 'SS':
  18790. if (strict) { return parseTokenTwoDigits; }
  18791. /* falls through */
  18792. case 'SSS':
  18793. if (strict) { return parseTokenThreeDigits; }
  18794. /* falls through */
  18795. case 'DDD':
  18796. return parseTokenOneToThreeDigits;
  18797. case 'MMM':
  18798. case 'MMMM':
  18799. case 'dd':
  18800. case 'ddd':
  18801. case 'dddd':
  18802. return parseTokenWord;
  18803. case 'a':
  18804. case 'A':
  18805. return getLangDefinition(config._l)._meridiemParse;
  18806. case 'X':
  18807. return parseTokenTimestampMs;
  18808. case 'Z':
  18809. case 'ZZ':
  18810. return parseTokenTimezone;
  18811. case 'T':
  18812. return parseTokenT;
  18813. case 'SSSS':
  18814. return parseTokenDigits;
  18815. case 'MM':
  18816. case 'DD':
  18817. case 'YY':
  18818. case 'GG':
  18819. case 'gg':
  18820. case 'HH':
  18821. case 'hh':
  18822. case 'mm':
  18823. case 'ss':
  18824. case 'ww':
  18825. case 'WW':
  18826. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  18827. case 'M':
  18828. case 'D':
  18829. case 'd':
  18830. case 'H':
  18831. case 'h':
  18832. case 'm':
  18833. case 's':
  18834. case 'w':
  18835. case 'W':
  18836. case 'e':
  18837. case 'E':
  18838. return parseTokenOneOrTwoDigits;
  18839. case 'Do':
  18840. return parseTokenOrdinal;
  18841. default :
  18842. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  18843. return a;
  18844. }
  18845. }
  18846. function timezoneMinutesFromString(string) {
  18847. string = string || "";
  18848. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  18849. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  18850. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  18851. minutes = +(parts[1] * 60) + toInt(parts[2]);
  18852. return parts[0] === '+' ? -minutes : minutes;
  18853. }
  18854. // function to convert string input to date
  18855. function addTimeToArrayFromToken(token, input, config) {
  18856. var a, datePartArray = config._a;
  18857. switch (token) {
  18858. // QUARTER
  18859. case 'Q':
  18860. if (input != null) {
  18861. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  18862. }
  18863. break;
  18864. // MONTH
  18865. case 'M' : // fall through to MM
  18866. case 'MM' :
  18867. if (input != null) {
  18868. datePartArray[MONTH] = toInt(input) - 1;
  18869. }
  18870. break;
  18871. case 'MMM' : // fall through to MMMM
  18872. case 'MMMM' :
  18873. a = getLangDefinition(config._l).monthsParse(input);
  18874. // if we didn't find a month name, mark the date as invalid.
  18875. if (a != null) {
  18876. datePartArray[MONTH] = a;
  18877. } else {
  18878. config._pf.invalidMonth = input;
  18879. }
  18880. break;
  18881. // DAY OF MONTH
  18882. case 'D' : // fall through to DD
  18883. case 'DD' :
  18884. if (input != null) {
  18885. datePartArray[DATE] = toInt(input);
  18886. }
  18887. break;
  18888. case 'Do' :
  18889. if (input != null) {
  18890. datePartArray[DATE] = toInt(parseInt(input, 10));
  18891. }
  18892. break;
  18893. // DAY OF YEAR
  18894. case 'DDD' : // fall through to DDDD
  18895. case 'DDDD' :
  18896. if (input != null) {
  18897. config._dayOfYear = toInt(input);
  18898. }
  18899. break;
  18900. // YEAR
  18901. case 'YY' :
  18902. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  18903. break;
  18904. case 'YYYY' :
  18905. case 'YYYYY' :
  18906. case 'YYYYYY' :
  18907. datePartArray[YEAR] = toInt(input);
  18908. break;
  18909. // AM / PM
  18910. case 'a' : // fall through to A
  18911. case 'A' :
  18912. config._isPm = getLangDefinition(config._l).isPM(input);
  18913. break;
  18914. // 24 HOUR
  18915. case 'H' : // fall through to hh
  18916. case 'HH' : // fall through to hh
  18917. case 'h' : // fall through to hh
  18918. case 'hh' :
  18919. datePartArray[HOUR] = toInt(input);
  18920. break;
  18921. // MINUTE
  18922. case 'm' : // fall through to mm
  18923. case 'mm' :
  18924. datePartArray[MINUTE] = toInt(input);
  18925. break;
  18926. // SECOND
  18927. case 's' : // fall through to ss
  18928. case 'ss' :
  18929. datePartArray[SECOND] = toInt(input);
  18930. break;
  18931. // MILLISECOND
  18932. case 'S' :
  18933. case 'SS' :
  18934. case 'SSS' :
  18935. case 'SSSS' :
  18936. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  18937. break;
  18938. // UNIX TIMESTAMP WITH MS
  18939. case 'X':
  18940. config._d = new Date(parseFloat(input) * 1000);
  18941. break;
  18942. // TIMEZONE
  18943. case 'Z' : // fall through to ZZ
  18944. case 'ZZ' :
  18945. config._useUTC = true;
  18946. config._tzm = timezoneMinutesFromString(input);
  18947. break;
  18948. case 'w':
  18949. case 'ww':
  18950. case 'W':
  18951. case 'WW':
  18952. case 'd':
  18953. case 'dd':
  18954. case 'ddd':
  18955. case 'dddd':
  18956. case 'e':
  18957. case 'E':
  18958. token = token.substr(0, 1);
  18959. /* falls through */
  18960. case 'gg':
  18961. case 'gggg':
  18962. case 'GG':
  18963. case 'GGGG':
  18964. case 'GGGGG':
  18965. token = token.substr(0, 2);
  18966. if (input) {
  18967. config._w = config._w || {};
  18968. config._w[token] = input;
  18969. }
  18970. break;
  18971. }
  18972. }
  18973. // convert an array to a date.
  18974. // the array should mirror the parameters below
  18975. // note: all values past the year are optional and will default to the lowest possible value.
  18976. // [year, month, day , hour, minute, second, millisecond]
  18977. function dateFromConfig(config) {
  18978. var i, date, input = [], currentDate,
  18979. yearToUse, fixYear, w, temp, lang, weekday, week;
  18980. if (config._d) {
  18981. return;
  18982. }
  18983. currentDate = currentDateArray(config);
  18984. //compute day of the year from weeks and weekdays
  18985. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  18986. fixYear = function (val) {
  18987. var intVal = parseInt(val, 10);
  18988. return val ?
  18989. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  18990. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  18991. };
  18992. w = config._w;
  18993. if (w.GG != null || w.W != null || w.E != null) {
  18994. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  18995. }
  18996. else {
  18997. lang = getLangDefinition(config._l);
  18998. weekday = w.d != null ? parseWeekday(w.d, lang) :
  18999. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  19000. week = parseInt(w.w, 10) || 1;
  19001. //if we're parsing 'd', then the low day numbers may be next week
  19002. if (w.d != null && weekday < lang._week.dow) {
  19003. week++;
  19004. }
  19005. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  19006. }
  19007. config._a[YEAR] = temp.year;
  19008. config._dayOfYear = temp.dayOfYear;
  19009. }
  19010. //if the day of the year is set, figure out what it is
  19011. if (config._dayOfYear) {
  19012. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  19013. if (config._dayOfYear > daysInYear(yearToUse)) {
  19014. config._pf._overflowDayOfYear = true;
  19015. }
  19016. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  19017. config._a[MONTH] = date.getUTCMonth();
  19018. config._a[DATE] = date.getUTCDate();
  19019. }
  19020. // Default to current date.
  19021. // * if no year, month, day of month are given, default to today
  19022. // * if day of month is given, default month and year
  19023. // * if month is given, default only year
  19024. // * if year is given, don't default anything
  19025. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  19026. config._a[i] = input[i] = currentDate[i];
  19027. }
  19028. // Zero out whatever was not defaulted, including time
  19029. for (; i < 7; i++) {
  19030. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  19031. }
  19032. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  19033. input[HOUR] += toInt((config._tzm || 0) / 60);
  19034. input[MINUTE] += toInt((config._tzm || 0) % 60);
  19035. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  19036. }
  19037. function dateFromObject(config) {
  19038. var normalizedInput;
  19039. if (config._d) {
  19040. return;
  19041. }
  19042. normalizedInput = normalizeObjectUnits(config._i);
  19043. config._a = [
  19044. normalizedInput.year,
  19045. normalizedInput.month,
  19046. normalizedInput.day,
  19047. normalizedInput.hour,
  19048. normalizedInput.minute,
  19049. normalizedInput.second,
  19050. normalizedInput.millisecond
  19051. ];
  19052. dateFromConfig(config);
  19053. }
  19054. function currentDateArray(config) {
  19055. var now = new Date();
  19056. if (config._useUTC) {
  19057. return [
  19058. now.getUTCFullYear(),
  19059. now.getUTCMonth(),
  19060. now.getUTCDate()
  19061. ];
  19062. } else {
  19063. return [now.getFullYear(), now.getMonth(), now.getDate()];
  19064. }
  19065. }
  19066. // date from string and format string
  19067. function makeDateFromStringAndFormat(config) {
  19068. config._a = [];
  19069. config._pf.empty = true;
  19070. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  19071. var lang = getLangDefinition(config._l),
  19072. string = '' + config._i,
  19073. i, parsedInput, tokens, token, skipped,
  19074. stringLength = string.length,
  19075. totalParsedInputLength = 0;
  19076. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  19077. for (i = 0; i < tokens.length; i++) {
  19078. token = tokens[i];
  19079. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  19080. if (parsedInput) {
  19081. skipped = string.substr(0, string.indexOf(parsedInput));
  19082. if (skipped.length > 0) {
  19083. config._pf.unusedInput.push(skipped);
  19084. }
  19085. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  19086. totalParsedInputLength += parsedInput.length;
  19087. }
  19088. // don't parse if it's not a known token
  19089. if (formatTokenFunctions[token]) {
  19090. if (parsedInput) {
  19091. config._pf.empty = false;
  19092. }
  19093. else {
  19094. config._pf.unusedTokens.push(token);
  19095. }
  19096. addTimeToArrayFromToken(token, parsedInput, config);
  19097. }
  19098. else if (config._strict && !parsedInput) {
  19099. config._pf.unusedTokens.push(token);
  19100. }
  19101. }
  19102. // add remaining unparsed input length to the string
  19103. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  19104. if (string.length > 0) {
  19105. config._pf.unusedInput.push(string);
  19106. }
  19107. // handle am pm
  19108. if (config._isPm && config._a[HOUR] < 12) {
  19109. config._a[HOUR] += 12;
  19110. }
  19111. // if is 12 am, change hours to 0
  19112. if (config._isPm === false && config._a[HOUR] === 12) {
  19113. config._a[HOUR] = 0;
  19114. }
  19115. dateFromConfig(config);
  19116. checkOverflow(config);
  19117. }
  19118. function unescapeFormat(s) {
  19119. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  19120. return p1 || p2 || p3 || p4;
  19121. });
  19122. }
  19123. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  19124. function regexpEscape(s) {
  19125. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  19126. }
  19127. // date from string and array of format strings
  19128. function makeDateFromStringAndArray(config) {
  19129. var tempConfig,
  19130. bestMoment,
  19131. scoreToBeat,
  19132. i,
  19133. currentScore;
  19134. if (config._f.length === 0) {
  19135. config._pf.invalidFormat = true;
  19136. config._d = new Date(NaN);
  19137. return;
  19138. }
  19139. for (i = 0; i < config._f.length; i++) {
  19140. currentScore = 0;
  19141. tempConfig = extend({}, config);
  19142. tempConfig._pf = defaultParsingFlags();
  19143. tempConfig._f = config._f[i];
  19144. makeDateFromStringAndFormat(tempConfig);
  19145. if (!isValid(tempConfig)) {
  19146. continue;
  19147. }
  19148. // if there is any input that was not parsed add a penalty for that format
  19149. currentScore += tempConfig._pf.charsLeftOver;
  19150. //or tokens
  19151. currentScore += tempConfig._pf.unusedTokens.length * 10;
  19152. tempConfig._pf.score = currentScore;
  19153. if (scoreToBeat == null || currentScore < scoreToBeat) {
  19154. scoreToBeat = currentScore;
  19155. bestMoment = tempConfig;
  19156. }
  19157. }
  19158. extend(config, bestMoment || tempConfig);
  19159. }
  19160. // date from iso format
  19161. function makeDateFromString(config) {
  19162. var i, l,
  19163. string = config._i,
  19164. match = isoRegex.exec(string);
  19165. if (match) {
  19166. config._pf.iso = true;
  19167. for (i = 0, l = isoDates.length; i < l; i++) {
  19168. if (isoDates[i][1].exec(string)) {
  19169. // match[5] should be "T" or undefined
  19170. config._f = isoDates[i][0] + (match[6] || " ");
  19171. break;
  19172. }
  19173. }
  19174. for (i = 0, l = isoTimes.length; i < l; i++) {
  19175. if (isoTimes[i][1].exec(string)) {
  19176. config._f += isoTimes[i][0];
  19177. break;
  19178. }
  19179. }
  19180. if (string.match(parseTokenTimezone)) {
  19181. config._f += "Z";
  19182. }
  19183. makeDateFromStringAndFormat(config);
  19184. }
  19185. else {
  19186. moment.createFromInputFallback(config);
  19187. }
  19188. }
  19189. function makeDateFromInput(config) {
  19190. var input = config._i,
  19191. matched = aspNetJsonRegex.exec(input);
  19192. if (input === undefined) {
  19193. config._d = new Date();
  19194. } else if (matched) {
  19195. config._d = new Date(+matched[1]);
  19196. } else if (typeof input === 'string') {
  19197. makeDateFromString(config);
  19198. } else if (isArray(input)) {
  19199. config._a = input.slice(0);
  19200. dateFromConfig(config);
  19201. } else if (isDate(input)) {
  19202. config._d = new Date(+input);
  19203. } else if (typeof(input) === 'object') {
  19204. dateFromObject(config);
  19205. } else if (typeof(input) === 'number') {
  19206. // from milliseconds
  19207. config._d = new Date(input);
  19208. } else {
  19209. moment.createFromInputFallback(config);
  19210. }
  19211. }
  19212. function makeDate(y, m, d, h, M, s, ms) {
  19213. //can't just apply() to create a date:
  19214. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  19215. var date = new Date(y, m, d, h, M, s, ms);
  19216. //the date constructor doesn't accept years < 1970
  19217. if (y < 1970) {
  19218. date.setFullYear(y);
  19219. }
  19220. return date;
  19221. }
  19222. function makeUTCDate(y) {
  19223. var date = new Date(Date.UTC.apply(null, arguments));
  19224. if (y < 1970) {
  19225. date.setUTCFullYear(y);
  19226. }
  19227. return date;
  19228. }
  19229. function parseWeekday(input, language) {
  19230. if (typeof input === 'string') {
  19231. if (!isNaN(input)) {
  19232. input = parseInt(input, 10);
  19233. }
  19234. else {
  19235. input = language.weekdaysParse(input);
  19236. if (typeof input !== 'number') {
  19237. return null;
  19238. }
  19239. }
  19240. }
  19241. return input;
  19242. }
  19243. /************************************
  19244. Relative Time
  19245. ************************************/
  19246. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  19247. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  19248. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  19249. }
  19250. function relativeTime(milliseconds, withoutSuffix, lang) {
  19251. var seconds = round(Math.abs(milliseconds) / 1000),
  19252. minutes = round(seconds / 60),
  19253. hours = round(minutes / 60),
  19254. days = round(hours / 24),
  19255. years = round(days / 365),
  19256. args = seconds < 45 && ['s', seconds] ||
  19257. minutes === 1 && ['m'] ||
  19258. minutes < 45 && ['mm', minutes] ||
  19259. hours === 1 && ['h'] ||
  19260. hours < 22 && ['hh', hours] ||
  19261. days === 1 && ['d'] ||
  19262. days <= 25 && ['dd', days] ||
  19263. days <= 45 && ['M'] ||
  19264. days < 345 && ['MM', round(days / 30)] ||
  19265. years === 1 && ['y'] || ['yy', years];
  19266. args[2] = withoutSuffix;
  19267. args[3] = milliseconds > 0;
  19268. args[4] = lang;
  19269. return substituteTimeAgo.apply({}, args);
  19270. }
  19271. /************************************
  19272. Week of Year
  19273. ************************************/
  19274. // firstDayOfWeek 0 = sun, 6 = sat
  19275. // the day of the week that starts the week
  19276. // (usually sunday or monday)
  19277. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  19278. // the first week is the week that contains the first
  19279. // of this day of the week
  19280. // (eg. ISO weeks use thursday (4))
  19281. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  19282. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  19283. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  19284. adjustedMoment;
  19285. if (daysToDayOfWeek > end) {
  19286. daysToDayOfWeek -= 7;
  19287. }
  19288. if (daysToDayOfWeek < end - 7) {
  19289. daysToDayOfWeek += 7;
  19290. }
  19291. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  19292. return {
  19293. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  19294. year: adjustedMoment.year()
  19295. };
  19296. }
  19297. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  19298. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  19299. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  19300. weekday = weekday != null ? weekday : firstDayOfWeek;
  19301. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  19302. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  19303. return {
  19304. year: dayOfYear > 0 ? year : year - 1,
  19305. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  19306. };
  19307. }
  19308. /************************************
  19309. Top Level Functions
  19310. ************************************/
  19311. function makeMoment(config) {
  19312. var input = config._i,
  19313. format = config._f;
  19314. if (input === null || (format === undefined && input === '')) {
  19315. return moment.invalid({nullInput: true});
  19316. }
  19317. if (typeof input === 'string') {
  19318. config._i = input = getLangDefinition().preparse(input);
  19319. }
  19320. if (moment.isMoment(input)) {
  19321. config = cloneMoment(input);
  19322. config._d = new Date(+input._d);
  19323. } else if (format) {
  19324. if (isArray(format)) {
  19325. makeDateFromStringAndArray(config);
  19326. } else {
  19327. makeDateFromStringAndFormat(config);
  19328. }
  19329. } else {
  19330. makeDateFromInput(config);
  19331. }
  19332. return new Moment(config);
  19333. }
  19334. moment = function (input, format, lang, strict) {
  19335. var c;
  19336. if (typeof(lang) === "boolean") {
  19337. strict = lang;
  19338. lang = undefined;
  19339. }
  19340. // object construction must be done this way.
  19341. // https://github.com/moment/moment/issues/1423
  19342. c = {};
  19343. c._isAMomentObject = true;
  19344. c._i = input;
  19345. c._f = format;
  19346. c._l = lang;
  19347. c._strict = strict;
  19348. c._isUTC = false;
  19349. c._pf = defaultParsingFlags();
  19350. return makeMoment(c);
  19351. };
  19352. moment.suppressDeprecationWarnings = false;
  19353. moment.createFromInputFallback = deprecate(
  19354. "moment construction falls back to js Date. This is " +
  19355. "discouraged and will be removed in upcoming major " +
  19356. "release. Please refer to " +
  19357. "https://github.com/moment/moment/issues/1407 for more info.",
  19358. function (config) {
  19359. config._d = new Date(config._i);
  19360. });
  19361. // creating with utc
  19362. moment.utc = function (input, format, lang, strict) {
  19363. var c;
  19364. if (typeof(lang) === "boolean") {
  19365. strict = lang;
  19366. lang = undefined;
  19367. }
  19368. // object construction must be done this way.
  19369. // https://github.com/moment/moment/issues/1423
  19370. c = {};
  19371. c._isAMomentObject = true;
  19372. c._useUTC = true;
  19373. c._isUTC = true;
  19374. c._l = lang;
  19375. c._i = input;
  19376. c._f = format;
  19377. c._strict = strict;
  19378. c._pf = defaultParsingFlags();
  19379. return makeMoment(c).utc();
  19380. };
  19381. // creating with unix timestamp (in seconds)
  19382. moment.unix = function (input) {
  19383. return moment(input * 1000);
  19384. };
  19385. // duration
  19386. moment.duration = function (input, key) {
  19387. var duration = input,
  19388. // matching against regexp is expensive, do it on demand
  19389. match = null,
  19390. sign,
  19391. ret,
  19392. parseIso;
  19393. if (moment.isDuration(input)) {
  19394. duration = {
  19395. ms: input._milliseconds,
  19396. d: input._days,
  19397. M: input._months
  19398. };
  19399. } else if (typeof input === 'number') {
  19400. duration = {};
  19401. if (key) {
  19402. duration[key] = input;
  19403. } else {
  19404. duration.milliseconds = input;
  19405. }
  19406. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  19407. sign = (match[1] === "-") ? -1 : 1;
  19408. duration = {
  19409. y: 0,
  19410. d: toInt(match[DATE]) * sign,
  19411. h: toInt(match[HOUR]) * sign,
  19412. m: toInt(match[MINUTE]) * sign,
  19413. s: toInt(match[SECOND]) * sign,
  19414. ms: toInt(match[MILLISECOND]) * sign
  19415. };
  19416. } else if (!!(match = isoDurationRegex.exec(input))) {
  19417. sign = (match[1] === "-") ? -1 : 1;
  19418. parseIso = function (inp) {
  19419. // We'd normally use ~~inp for this, but unfortunately it also
  19420. // converts floats to ints.
  19421. // inp may be undefined, so careful calling replace on it.
  19422. var res = inp && parseFloat(inp.replace(',', '.'));
  19423. // apply sign while we're at it
  19424. return (isNaN(res) ? 0 : res) * sign;
  19425. };
  19426. duration = {
  19427. y: parseIso(match[2]),
  19428. M: parseIso(match[3]),
  19429. d: parseIso(match[4]),
  19430. h: parseIso(match[5]),
  19431. m: parseIso(match[6]),
  19432. s: parseIso(match[7]),
  19433. w: parseIso(match[8])
  19434. };
  19435. }
  19436. ret = new Duration(duration);
  19437. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  19438. ret._lang = input._lang;
  19439. }
  19440. return ret;
  19441. };
  19442. // version number
  19443. moment.version = VERSION;
  19444. // default format
  19445. moment.defaultFormat = isoFormat;
  19446. // Plugins that add properties should also add the key here (null value),
  19447. // so we can properly clone ourselves.
  19448. moment.momentProperties = momentProperties;
  19449. // This function will be called whenever a moment is mutated.
  19450. // It is intended to keep the offset in sync with the timezone.
  19451. moment.updateOffset = function () {};
  19452. // This function will load languages and then set the global language. If
  19453. // no arguments are passed in, it will simply return the current global
  19454. // language key.
  19455. moment.lang = function (key, values) {
  19456. var r;
  19457. if (!key) {
  19458. return moment.fn._lang._abbr;
  19459. }
  19460. if (values) {
  19461. loadLang(normalizeLanguage(key), values);
  19462. } else if (values === null) {
  19463. unloadLang(key);
  19464. key = 'en';
  19465. } else if (!languages[key]) {
  19466. getLangDefinition(key);
  19467. }
  19468. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  19469. return r._abbr;
  19470. };
  19471. // returns language data
  19472. moment.langData = function (key) {
  19473. if (key && key._lang && key._lang._abbr) {
  19474. key = key._lang._abbr;
  19475. }
  19476. return getLangDefinition(key);
  19477. };
  19478. // compare moment object
  19479. moment.isMoment = function (obj) {
  19480. return obj instanceof Moment ||
  19481. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  19482. };
  19483. // for typechecking Duration objects
  19484. moment.isDuration = function (obj) {
  19485. return obj instanceof Duration;
  19486. };
  19487. for (i = lists.length - 1; i >= 0; --i) {
  19488. makeList(lists[i]);
  19489. }
  19490. moment.normalizeUnits = function (units) {
  19491. return normalizeUnits(units);
  19492. };
  19493. moment.invalid = function (flags) {
  19494. var m = moment.utc(NaN);
  19495. if (flags != null) {
  19496. extend(m._pf, flags);
  19497. }
  19498. else {
  19499. m._pf.userInvalidated = true;
  19500. }
  19501. return m;
  19502. };
  19503. moment.parseZone = function () {
  19504. return moment.apply(null, arguments).parseZone();
  19505. };
  19506. moment.parseTwoDigitYear = function (input) {
  19507. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  19508. };
  19509. /************************************
  19510. Moment Prototype
  19511. ************************************/
  19512. extend(moment.fn = Moment.prototype, {
  19513. clone : function () {
  19514. return moment(this);
  19515. },
  19516. valueOf : function () {
  19517. return +this._d + ((this._offset || 0) * 60000);
  19518. },
  19519. unix : function () {
  19520. return Math.floor(+this / 1000);
  19521. },
  19522. toString : function () {
  19523. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  19524. },
  19525. toDate : function () {
  19526. return this._offset ? new Date(+this) : this._d;
  19527. },
  19528. toISOString : function () {
  19529. var m = moment(this).utc();
  19530. if (0 < m.year() && m.year() <= 9999) {
  19531. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  19532. } else {
  19533. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  19534. }
  19535. },
  19536. toArray : function () {
  19537. var m = this;
  19538. return [
  19539. m.year(),
  19540. m.month(),
  19541. m.date(),
  19542. m.hours(),
  19543. m.minutes(),
  19544. m.seconds(),
  19545. m.milliseconds()
  19546. ];
  19547. },
  19548. isValid : function () {
  19549. return isValid(this);
  19550. },
  19551. isDSTShifted : function () {
  19552. if (this._a) {
  19553. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  19554. }
  19555. return false;
  19556. },
  19557. parsingFlags : function () {
  19558. return extend({}, this._pf);
  19559. },
  19560. invalidAt: function () {
  19561. return this._pf.overflow;
  19562. },
  19563. utc : function () {
  19564. return this.zone(0);
  19565. },
  19566. local : function () {
  19567. this.zone(0);
  19568. this._isUTC = false;
  19569. return this;
  19570. },
  19571. format : function (inputString) {
  19572. var output = formatMoment(this, inputString || moment.defaultFormat);
  19573. return this.lang().postformat(output);
  19574. },
  19575. add : function (input, val) {
  19576. var dur;
  19577. // switch args to support add('s', 1) and add(1, 's')
  19578. if (typeof input === 'string') {
  19579. dur = moment.duration(+val, input);
  19580. } else {
  19581. dur = moment.duration(input, val);
  19582. }
  19583. addOrSubtractDurationFromMoment(this, dur, 1);
  19584. return this;
  19585. },
  19586. subtract : function (input, val) {
  19587. var dur;
  19588. // switch args to support subtract('s', 1) and subtract(1, 's')
  19589. if (typeof input === 'string') {
  19590. dur = moment.duration(+val, input);
  19591. } else {
  19592. dur = moment.duration(input, val);
  19593. }
  19594. addOrSubtractDurationFromMoment(this, dur, -1);
  19595. return this;
  19596. },
  19597. diff : function (input, units, asFloat) {
  19598. var that = makeAs(input, this),
  19599. zoneDiff = (this.zone() - that.zone()) * 6e4,
  19600. diff, output;
  19601. units = normalizeUnits(units);
  19602. if (units === 'year' || units === 'month') {
  19603. // average number of days in the months in the given dates
  19604. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  19605. // difference in months
  19606. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  19607. // adjust by taking difference in days, average number of days
  19608. // and dst in the given months.
  19609. output += ((this - moment(this).startOf('month')) -
  19610. (that - moment(that).startOf('month'))) / diff;
  19611. // same as above but with zones, to negate all dst
  19612. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  19613. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  19614. if (units === 'year') {
  19615. output = output / 12;
  19616. }
  19617. } else {
  19618. diff = (this - that);
  19619. output = units === 'second' ? diff / 1e3 : // 1000
  19620. units === 'minute' ? diff / 6e4 : // 1000 * 60
  19621. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  19622. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  19623. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  19624. diff;
  19625. }
  19626. return asFloat ? output : absRound(output);
  19627. },
  19628. from : function (time, withoutSuffix) {
  19629. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  19630. },
  19631. fromNow : function (withoutSuffix) {
  19632. return this.from(moment(), withoutSuffix);
  19633. },
  19634. calendar : function () {
  19635. // We want to compare the start of today, vs this.
  19636. // Getting start-of-today depends on whether we're zone'd or not.
  19637. var sod = makeAs(moment(), this).startOf('day'),
  19638. diff = this.diff(sod, 'days', true),
  19639. format = diff < -6 ? 'sameElse' :
  19640. diff < -1 ? 'lastWeek' :
  19641. diff < 0 ? 'lastDay' :
  19642. diff < 1 ? 'sameDay' :
  19643. diff < 2 ? 'nextDay' :
  19644. diff < 7 ? 'nextWeek' : 'sameElse';
  19645. return this.format(this.lang().calendar(format, this));
  19646. },
  19647. isLeapYear : function () {
  19648. return isLeapYear(this.year());
  19649. },
  19650. isDST : function () {
  19651. return (this.zone() < this.clone().month(0).zone() ||
  19652. this.zone() < this.clone().month(5).zone());
  19653. },
  19654. day : function (input) {
  19655. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  19656. if (input != null) {
  19657. input = parseWeekday(input, this.lang());
  19658. return this.add({ d : input - day });
  19659. } else {
  19660. return day;
  19661. }
  19662. },
  19663. month : makeAccessor('Month', true),
  19664. startOf: function (units) {
  19665. units = normalizeUnits(units);
  19666. // the following switch intentionally omits break keywords
  19667. // to utilize falling through the cases.
  19668. switch (units) {
  19669. case 'year':
  19670. this.month(0);
  19671. /* falls through */
  19672. case 'quarter':
  19673. case 'month':
  19674. this.date(1);
  19675. /* falls through */
  19676. case 'week':
  19677. case 'isoWeek':
  19678. case 'day':
  19679. this.hours(0);
  19680. /* falls through */
  19681. case 'hour':
  19682. this.minutes(0);
  19683. /* falls through */
  19684. case 'minute':
  19685. this.seconds(0);
  19686. /* falls through */
  19687. case 'second':
  19688. this.milliseconds(0);
  19689. /* falls through */
  19690. }
  19691. // weeks are a special case
  19692. if (units === 'week') {
  19693. this.weekday(0);
  19694. } else if (units === 'isoWeek') {
  19695. this.isoWeekday(1);
  19696. }
  19697. // quarters are also special
  19698. if (units === 'quarter') {
  19699. this.month(Math.floor(this.month() / 3) * 3);
  19700. }
  19701. return this;
  19702. },
  19703. endOf: function (units) {
  19704. units = normalizeUnits(units);
  19705. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  19706. },
  19707. isAfter: function (input, units) {
  19708. units = typeof units !== 'undefined' ? units : 'millisecond';
  19709. return +this.clone().startOf(units) > +moment(input).startOf(units);
  19710. },
  19711. isBefore: function (input, units) {
  19712. units = typeof units !== 'undefined' ? units : 'millisecond';
  19713. return +this.clone().startOf(units) < +moment(input).startOf(units);
  19714. },
  19715. isSame: function (input, units) {
  19716. units = units || 'ms';
  19717. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  19718. },
  19719. min: function (other) {
  19720. other = moment.apply(null, arguments);
  19721. return other < this ? this : other;
  19722. },
  19723. max: function (other) {
  19724. other = moment.apply(null, arguments);
  19725. return other > this ? this : other;
  19726. },
  19727. // keepTime = true means only change the timezone, without affecting
  19728. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  19729. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  19730. // adjust the time as needed, to be valid.
  19731. //
  19732. // Keeping the time actually adds/subtracts (one hour)
  19733. // from the actual represented time. That is why we call updateOffset
  19734. // a second time. In case it wants us to change the offset again
  19735. // _changeInProgress == true case, then we have to adjust, because
  19736. // there is no such time in the given timezone.
  19737. zone : function (input, keepTime) {
  19738. var offset = this._offset || 0;
  19739. if (input != null) {
  19740. if (typeof input === "string") {
  19741. input = timezoneMinutesFromString(input);
  19742. }
  19743. if (Math.abs(input) < 16) {
  19744. input = input * 60;
  19745. }
  19746. this._offset = input;
  19747. this._isUTC = true;
  19748. if (offset !== input) {
  19749. if (!keepTime || this._changeInProgress) {
  19750. addOrSubtractDurationFromMoment(this,
  19751. moment.duration(offset - input, 'm'), 1, false);
  19752. } else if (!this._changeInProgress) {
  19753. this._changeInProgress = true;
  19754. moment.updateOffset(this, true);
  19755. this._changeInProgress = null;
  19756. }
  19757. }
  19758. } else {
  19759. return this._isUTC ? offset : this._d.getTimezoneOffset();
  19760. }
  19761. return this;
  19762. },
  19763. zoneAbbr : function () {
  19764. return this._isUTC ? "UTC" : "";
  19765. },
  19766. zoneName : function () {
  19767. return this._isUTC ? "Coordinated Universal Time" : "";
  19768. },
  19769. parseZone : function () {
  19770. if (this._tzm) {
  19771. this.zone(this._tzm);
  19772. } else if (typeof this._i === 'string') {
  19773. this.zone(this._i);
  19774. }
  19775. return this;
  19776. },
  19777. hasAlignedHourOffset : function (input) {
  19778. if (!input) {
  19779. input = 0;
  19780. }
  19781. else {
  19782. input = moment(input).zone();
  19783. }
  19784. return (this.zone() - input) % 60 === 0;
  19785. },
  19786. daysInMonth : function () {
  19787. return daysInMonth(this.year(), this.month());
  19788. },
  19789. dayOfYear : function (input) {
  19790. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  19791. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  19792. },
  19793. quarter : function (input) {
  19794. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  19795. },
  19796. weekYear : function (input) {
  19797. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  19798. return input == null ? year : this.add("y", (input - year));
  19799. },
  19800. isoWeekYear : function (input) {
  19801. var year = weekOfYear(this, 1, 4).year;
  19802. return input == null ? year : this.add("y", (input - year));
  19803. },
  19804. week : function (input) {
  19805. var week = this.lang().week(this);
  19806. return input == null ? week : this.add("d", (input - week) * 7);
  19807. },
  19808. isoWeek : function (input) {
  19809. var week = weekOfYear(this, 1, 4).week;
  19810. return input == null ? week : this.add("d", (input - week) * 7);
  19811. },
  19812. weekday : function (input) {
  19813. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  19814. return input == null ? weekday : this.add("d", input - weekday);
  19815. },
  19816. isoWeekday : function (input) {
  19817. // behaves the same as moment#day except
  19818. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  19819. // as a setter, sunday should belong to the previous week.
  19820. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  19821. },
  19822. isoWeeksInYear : function () {
  19823. return weeksInYear(this.year(), 1, 4);
  19824. },
  19825. weeksInYear : function () {
  19826. var weekInfo = this._lang._week;
  19827. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  19828. },
  19829. get : function (units) {
  19830. units = normalizeUnits(units);
  19831. return this[units]();
  19832. },
  19833. set : function (units, value) {
  19834. units = normalizeUnits(units);
  19835. if (typeof this[units] === 'function') {
  19836. this[units](value);
  19837. }
  19838. return this;
  19839. },
  19840. // If passed a language key, it will set the language for this
  19841. // instance. Otherwise, it will return the language configuration
  19842. // variables for this instance.
  19843. lang : function (key) {
  19844. if (key === undefined) {
  19845. return this._lang;
  19846. } else {
  19847. this._lang = getLangDefinition(key);
  19848. return this;
  19849. }
  19850. }
  19851. });
  19852. function rawMonthSetter(mom, value) {
  19853. var dayOfMonth;
  19854. // TODO: Move this out of here!
  19855. if (typeof value === 'string') {
  19856. value = mom.lang().monthsParse(value);
  19857. // TODO: Another silent failure?
  19858. if (typeof value !== 'number') {
  19859. return mom;
  19860. }
  19861. }
  19862. dayOfMonth = Math.min(mom.date(),
  19863. daysInMonth(mom.year(), value));
  19864. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  19865. return mom;
  19866. }
  19867. function rawGetter(mom, unit) {
  19868. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  19869. }
  19870. function rawSetter(mom, unit, value) {
  19871. if (unit === 'Month') {
  19872. return rawMonthSetter(mom, value);
  19873. } else {
  19874. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  19875. }
  19876. }
  19877. function makeAccessor(unit, keepTime) {
  19878. return function (value) {
  19879. if (value != null) {
  19880. rawSetter(this, unit, value);
  19881. moment.updateOffset(this, keepTime);
  19882. return this;
  19883. } else {
  19884. return rawGetter(this, unit);
  19885. }
  19886. };
  19887. }
  19888. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  19889. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  19890. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  19891. // Setting the hour should keep the time, because the user explicitly
  19892. // specified which hour he wants. So trying to maintain the same hour (in
  19893. // a new timezone) makes sense. Adding/subtracting hours does not follow
  19894. // this rule.
  19895. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  19896. // moment.fn.month is defined separately
  19897. moment.fn.date = makeAccessor('Date', true);
  19898. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  19899. moment.fn.year = makeAccessor('FullYear', true);
  19900. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  19901. // add plural methods
  19902. moment.fn.days = moment.fn.day;
  19903. moment.fn.months = moment.fn.month;
  19904. moment.fn.weeks = moment.fn.week;
  19905. moment.fn.isoWeeks = moment.fn.isoWeek;
  19906. moment.fn.quarters = moment.fn.quarter;
  19907. // add aliased format methods
  19908. moment.fn.toJSON = moment.fn.toISOString;
  19909. /************************************
  19910. Duration Prototype
  19911. ************************************/
  19912. extend(moment.duration.fn = Duration.prototype, {
  19913. _bubble : function () {
  19914. var milliseconds = this._milliseconds,
  19915. days = this._days,
  19916. months = this._months,
  19917. data = this._data,
  19918. seconds, minutes, hours, years;
  19919. // The following code bubbles up values, see the tests for
  19920. // examples of what that means.
  19921. data.milliseconds = milliseconds % 1000;
  19922. seconds = absRound(milliseconds / 1000);
  19923. data.seconds = seconds % 60;
  19924. minutes = absRound(seconds / 60);
  19925. data.minutes = minutes % 60;
  19926. hours = absRound(minutes / 60);
  19927. data.hours = hours % 24;
  19928. days += absRound(hours / 24);
  19929. data.days = days % 30;
  19930. months += absRound(days / 30);
  19931. data.months = months % 12;
  19932. years = absRound(months / 12);
  19933. data.years = years;
  19934. },
  19935. weeks : function () {
  19936. return absRound(this.days() / 7);
  19937. },
  19938. valueOf : function () {
  19939. return this._milliseconds +
  19940. this._days * 864e5 +
  19941. (this._months % 12) * 2592e6 +
  19942. toInt(this._months / 12) * 31536e6;
  19943. },
  19944. humanize : function (withSuffix) {
  19945. var difference = +this,
  19946. output = relativeTime(difference, !withSuffix, this.lang());
  19947. if (withSuffix) {
  19948. output = this.lang().pastFuture(difference, output);
  19949. }
  19950. return this.lang().postformat(output);
  19951. },
  19952. add : function (input, val) {
  19953. // supports only 2.0-style add(1, 's') or add(moment)
  19954. var dur = moment.duration(input, val);
  19955. this._milliseconds += dur._milliseconds;
  19956. this._days += dur._days;
  19957. this._months += dur._months;
  19958. this._bubble();
  19959. return this;
  19960. },
  19961. subtract : function (input, val) {
  19962. var dur = moment.duration(input, val);
  19963. this._milliseconds -= dur._milliseconds;
  19964. this._days -= dur._days;
  19965. this._months -= dur._months;
  19966. this._bubble();
  19967. return this;
  19968. },
  19969. get : function (units) {
  19970. units = normalizeUnits(units);
  19971. return this[units.toLowerCase() + 's']();
  19972. },
  19973. as : function (units) {
  19974. units = normalizeUnits(units);
  19975. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  19976. },
  19977. lang : moment.fn.lang,
  19978. toIsoString : function () {
  19979. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  19980. var years = Math.abs(this.years()),
  19981. months = Math.abs(this.months()),
  19982. days = Math.abs(this.days()),
  19983. hours = Math.abs(this.hours()),
  19984. minutes = Math.abs(this.minutes()),
  19985. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  19986. if (!this.asSeconds()) {
  19987. // this is the same as C#'s (Noda) and python (isodate)...
  19988. // but not other JS (goog.date)
  19989. return 'P0D';
  19990. }
  19991. return (this.asSeconds() < 0 ? '-' : '') +
  19992. 'P' +
  19993. (years ? years + 'Y' : '') +
  19994. (months ? months + 'M' : '') +
  19995. (days ? days + 'D' : '') +
  19996. ((hours || minutes || seconds) ? 'T' : '') +
  19997. (hours ? hours + 'H' : '') +
  19998. (minutes ? minutes + 'M' : '') +
  19999. (seconds ? seconds + 'S' : '');
  20000. }
  20001. });
  20002. function makeDurationGetter(name) {
  20003. moment.duration.fn[name] = function () {
  20004. return this._data[name];
  20005. };
  20006. }
  20007. function makeDurationAsGetter(name, factor) {
  20008. moment.duration.fn['as' + name] = function () {
  20009. return +this / factor;
  20010. };
  20011. }
  20012. for (i in unitMillisecondFactors) {
  20013. if (unitMillisecondFactors.hasOwnProperty(i)) {
  20014. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  20015. makeDurationGetter(i.toLowerCase());
  20016. }
  20017. }
  20018. makeDurationAsGetter('Weeks', 6048e5);
  20019. moment.duration.fn.asMonths = function () {
  20020. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  20021. };
  20022. /************************************
  20023. Default Lang
  20024. ************************************/
  20025. // Set default language, other languages will inherit from English.
  20026. moment.lang('en', {
  20027. ordinal : function (number) {
  20028. var b = number % 10,
  20029. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  20030. (b === 1) ? 'st' :
  20031. (b === 2) ? 'nd' :
  20032. (b === 3) ? 'rd' : 'th';
  20033. return number + output;
  20034. }
  20035. });
  20036. /* EMBED_LANGUAGES */
  20037. /************************************
  20038. Exposing Moment
  20039. ************************************/
  20040. function makeGlobal(shouldDeprecate) {
  20041. /*global ender:false */
  20042. if (typeof ender !== 'undefined') {
  20043. return;
  20044. }
  20045. oldGlobalMoment = globalScope.moment;
  20046. if (shouldDeprecate) {
  20047. globalScope.moment = deprecate(
  20048. "Accessing Moment through the global scope is " +
  20049. "deprecated, and will be removed in an upcoming " +
  20050. "release.",
  20051. moment);
  20052. } else {
  20053. globalScope.moment = moment;
  20054. }
  20055. }
  20056. // CommonJS module is defined
  20057. if (hasModule) {
  20058. module.exports = moment;
  20059. } else if (typeof define === "function" && define.amd) {
  20060. define("moment", function (require, exports, module) {
  20061. if (module.config && module.config() && module.config().noGlobal === true) {
  20062. // release the global variable
  20063. globalScope.moment = oldGlobalMoment;
  20064. }
  20065. return moment;
  20066. });
  20067. makeGlobal(true);
  20068. } else {
  20069. makeGlobal();
  20070. }
  20071. }).call(this);
  20072. },{}],5:[function(require,module,exports){
  20073. /**
  20074. * Copyright 2012 Craig Campbell
  20075. *
  20076. * Licensed under the Apache License, Version 2.0 (the "License");
  20077. * you may not use this file except in compliance with the License.
  20078. * You may obtain a copy of the License at
  20079. *
  20080. * http://www.apache.org/licenses/LICENSE-2.0
  20081. *
  20082. * Unless required by applicable law or agreed to in writing, software
  20083. * distributed under the License is distributed on an "AS IS" BASIS,
  20084. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  20085. * See the License for the specific language governing permissions and
  20086. * limitations under the License.
  20087. *
  20088. * Mousetrap is a simple keyboard shortcut library for Javascript with
  20089. * no external dependencies
  20090. *
  20091. * @version 1.1.2
  20092. * @url craig.is/killing/mice
  20093. */
  20094. /**
  20095. * mapping of special keycodes to their corresponding keys
  20096. *
  20097. * everything in this dictionary cannot use keypress events
  20098. * so it has to be here to map to the correct keycodes for
  20099. * keyup/keydown events
  20100. *
  20101. * @type {Object}
  20102. */
  20103. var _MAP = {
  20104. 8: 'backspace',
  20105. 9: 'tab',
  20106. 13: 'enter',
  20107. 16: 'shift',
  20108. 17: 'ctrl',
  20109. 18: 'alt',
  20110. 20: 'capslock',
  20111. 27: 'esc',
  20112. 32: 'space',
  20113. 33: 'pageup',
  20114. 34: 'pagedown',
  20115. 35: 'end',
  20116. 36: 'home',
  20117. 37: 'left',
  20118. 38: 'up',
  20119. 39: 'right',
  20120. 40: 'down',
  20121. 45: 'ins',
  20122. 46: 'del',
  20123. 91: 'meta',
  20124. 93: 'meta',
  20125. 224: 'meta'
  20126. },
  20127. /**
  20128. * mapping for special characters so they can support
  20129. *
  20130. * this dictionary is only used incase you want to bind a
  20131. * keyup or keydown event to one of these keys
  20132. *
  20133. * @type {Object}
  20134. */
  20135. _KEYCODE_MAP = {
  20136. 106: '*',
  20137. 107: '+',
  20138. 109: '-',
  20139. 110: '.',
  20140. 111 : '/',
  20141. 186: ';',
  20142. 187: '=',
  20143. 188: ',',
  20144. 189: '-',
  20145. 190: '.',
  20146. 191: '/',
  20147. 192: '`',
  20148. 219: '[',
  20149. 220: '\\',
  20150. 221: ']',
  20151. 222: '\''
  20152. },
  20153. /**
  20154. * this is a mapping of keys that require shift on a US keypad
  20155. * back to the non shift equivelents
  20156. *
  20157. * this is so you can use keyup events with these keys
  20158. *
  20159. * note that this will only work reliably on US keyboards
  20160. *
  20161. * @type {Object}
  20162. */
  20163. _SHIFT_MAP = {
  20164. '~': '`',
  20165. '!': '1',
  20166. '@': '2',
  20167. '#': '3',
  20168. '$': '4',
  20169. '%': '5',
  20170. '^': '6',
  20171. '&': '7',
  20172. '*': '8',
  20173. '(': '9',
  20174. ')': '0',
  20175. '_': '-',
  20176. '+': '=',
  20177. ':': ';',
  20178. '\"': '\'',
  20179. '<': ',',
  20180. '>': '.',
  20181. '?': '/',
  20182. '|': '\\'
  20183. },
  20184. /**
  20185. * this is a list of special strings you can use to map
  20186. * to modifier keys when you specify your keyboard shortcuts
  20187. *
  20188. * @type {Object}
  20189. */
  20190. _SPECIAL_ALIASES = {
  20191. 'option': 'alt',
  20192. 'command': 'meta',
  20193. 'return': 'enter',
  20194. 'escape': 'esc'
  20195. },
  20196. /**
  20197. * variable to store the flipped version of _MAP from above
  20198. * needed to check if we should use keypress or not when no action
  20199. * is specified
  20200. *
  20201. * @type {Object|undefined}
  20202. */
  20203. _REVERSE_MAP,
  20204. /**
  20205. * a list of all the callbacks setup via Mousetrap.bind()
  20206. *
  20207. * @type {Object}
  20208. */
  20209. _callbacks = {},
  20210. /**
  20211. * direct map of string combinations to callbacks used for trigger()
  20212. *
  20213. * @type {Object}
  20214. */
  20215. _direct_map = {},
  20216. /**
  20217. * keeps track of what level each sequence is at since multiple
  20218. * sequences can start out with the same sequence
  20219. *
  20220. * @type {Object}
  20221. */
  20222. _sequence_levels = {},
  20223. /**
  20224. * variable to store the setTimeout call
  20225. *
  20226. * @type {null|number}
  20227. */
  20228. _reset_timer,
  20229. /**
  20230. * temporary state where we will ignore the next keyup
  20231. *
  20232. * @type {boolean|string}
  20233. */
  20234. _ignore_next_keyup = false,
  20235. /**
  20236. * are we currently inside of a sequence?
  20237. * type of action ("keyup" or "keydown" or "keypress") or false
  20238. *
  20239. * @type {boolean|string}
  20240. */
  20241. _inside_sequence = false;
  20242. /**
  20243. * loop through the f keys, f1 to f19 and add them to the map
  20244. * programatically
  20245. */
  20246. for (var i = 1; i < 20; ++i) {
  20247. _MAP[111 + i] = 'f' + i;
  20248. }
  20249. /**
  20250. * loop through to map numbers on the numeric keypad
  20251. */
  20252. for (i = 0; i <= 9; ++i) {
  20253. _MAP[i + 96] = i;
  20254. }
  20255. /**
  20256. * cross browser add event method
  20257. *
  20258. * @param {Element|HTMLDocument} object
  20259. * @param {string} type
  20260. * @param {Function} callback
  20261. * @returns void
  20262. */
  20263. function _addEvent(object, type, callback) {
  20264. if (object.addEventListener) {
  20265. return object.addEventListener(type, callback, false);
  20266. }
  20267. object.attachEvent('on' + type, callback);
  20268. }
  20269. /**
  20270. * takes the event and returns the key character
  20271. *
  20272. * @param {Event} e
  20273. * @return {string}
  20274. */
  20275. function _characterFromEvent(e) {
  20276. // for keypress events we should return the character as is
  20277. if (e.type == 'keypress') {
  20278. return String.fromCharCode(e.which);
  20279. }
  20280. // for non keypress events the special maps are needed
  20281. if (_MAP[e.which]) {
  20282. return _MAP[e.which];
  20283. }
  20284. if (_KEYCODE_MAP[e.which]) {
  20285. return _KEYCODE_MAP[e.which];
  20286. }
  20287. // if it is not in the special map
  20288. return String.fromCharCode(e.which).toLowerCase();
  20289. }
  20290. /**
  20291. * should we stop this event before firing off callbacks
  20292. *
  20293. * @param {Event} e
  20294. * @return {boolean}
  20295. */
  20296. function _stop(e) {
  20297. var element = e.target || e.srcElement,
  20298. tag_name = element.tagName;
  20299. // if the element has the class "mousetrap" then no need to stop
  20300. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  20301. return false;
  20302. }
  20303. // stop for input, select, and textarea
  20304. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  20305. }
  20306. /**
  20307. * checks if two arrays are equal
  20308. *
  20309. * @param {Array} modifiers1
  20310. * @param {Array} modifiers2
  20311. * @returns {boolean}
  20312. */
  20313. function _modifiersMatch(modifiers1, modifiers2) {
  20314. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  20315. }
  20316. /**
  20317. * resets all sequence counters except for the ones passed in
  20318. *
  20319. * @param {Object} do_not_reset
  20320. * @returns void
  20321. */
  20322. function _resetSequences(do_not_reset) {
  20323. do_not_reset = do_not_reset || {};
  20324. var active_sequences = false,
  20325. key;
  20326. for (key in _sequence_levels) {
  20327. if (do_not_reset[key]) {
  20328. active_sequences = true;
  20329. continue;
  20330. }
  20331. _sequence_levels[key] = 0;
  20332. }
  20333. if (!active_sequences) {
  20334. _inside_sequence = false;
  20335. }
  20336. }
  20337. /**
  20338. * finds all callbacks that match based on the keycode, modifiers,
  20339. * and action
  20340. *
  20341. * @param {string} character
  20342. * @param {Array} modifiers
  20343. * @param {string} action
  20344. * @param {boolean=} remove - should we remove any matches
  20345. * @param {string=} combination
  20346. * @returns {Array}
  20347. */
  20348. function _getMatches(character, modifiers, action, remove, combination) {
  20349. var i,
  20350. callback,
  20351. matches = [];
  20352. // if there are no events related to this keycode
  20353. if (!_callbacks[character]) {
  20354. return [];
  20355. }
  20356. // if a modifier key is coming up on its own we should allow it
  20357. if (action == 'keyup' && _isModifier(character)) {
  20358. modifiers = [character];
  20359. }
  20360. // loop through all callbacks for the key that was pressed
  20361. // and see if any of them match
  20362. for (i = 0; i < _callbacks[character].length; ++i) {
  20363. callback = _callbacks[character][i];
  20364. // if this is a sequence but it is not at the right level
  20365. // then move onto the next match
  20366. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  20367. continue;
  20368. }
  20369. // if the action we are looking for doesn't match the action we got
  20370. // then we should keep going
  20371. if (action != callback.action) {
  20372. continue;
  20373. }
  20374. // if this is a keypress event that means that we need to only
  20375. // look at the character, otherwise check the modifiers as
  20376. // well
  20377. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  20378. // remove is used so if you change your mind and call bind a
  20379. // second time with a new function the first one is overwritten
  20380. if (remove && callback.combo == combination) {
  20381. _callbacks[character].splice(i, 1);
  20382. }
  20383. matches.push(callback);
  20384. }
  20385. }
  20386. return matches;
  20387. }
  20388. /**
  20389. * takes a key event and figures out what the modifiers are
  20390. *
  20391. * @param {Event} e
  20392. * @returns {Array}
  20393. */
  20394. function _eventModifiers(e) {
  20395. var modifiers = [];
  20396. if (e.shiftKey) {
  20397. modifiers.push('shift');
  20398. }
  20399. if (e.altKey) {
  20400. modifiers.push('alt');
  20401. }
  20402. if (e.ctrlKey) {
  20403. modifiers.push('ctrl');
  20404. }
  20405. if (e.metaKey) {
  20406. modifiers.push('meta');
  20407. }
  20408. return modifiers;
  20409. }
  20410. /**
  20411. * actually calls the callback function
  20412. *
  20413. * if your callback function returns false this will use the jquery
  20414. * convention - prevent default and stop propogation on the event
  20415. *
  20416. * @param {Function} callback
  20417. * @param {Event} e
  20418. * @returns void
  20419. */
  20420. function _fireCallback(callback, e) {
  20421. if (callback(e) === false) {
  20422. if (e.preventDefault) {
  20423. e.preventDefault();
  20424. }
  20425. if (e.stopPropagation) {
  20426. e.stopPropagation();
  20427. }
  20428. e.returnValue = false;
  20429. e.cancelBubble = true;
  20430. }
  20431. }
  20432. /**
  20433. * handles a character key event
  20434. *
  20435. * @param {string} character
  20436. * @param {Event} e
  20437. * @returns void
  20438. */
  20439. function _handleCharacter(character, e) {
  20440. // if this event should not happen stop here
  20441. if (_stop(e)) {
  20442. return;
  20443. }
  20444. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  20445. i,
  20446. do_not_reset = {},
  20447. processed_sequence_callback = false;
  20448. // loop through matching callbacks for this key event
  20449. for (i = 0; i < callbacks.length; ++i) {
  20450. // fire for all sequence callbacks
  20451. // this is because if for example you have multiple sequences
  20452. // bound such as "g i" and "g t" they both need to fire the
  20453. // callback for matching g cause otherwise you can only ever
  20454. // match the first one
  20455. if (callbacks[i].seq) {
  20456. processed_sequence_callback = true;
  20457. // keep a list of which sequences were matches for later
  20458. do_not_reset[callbacks[i].seq] = 1;
  20459. _fireCallback(callbacks[i].callback, e);
  20460. continue;
  20461. }
  20462. // if there were no sequence matches but we are still here
  20463. // that means this is a regular match so we should fire that
  20464. if (!processed_sequence_callback && !_inside_sequence) {
  20465. _fireCallback(callbacks[i].callback, e);
  20466. }
  20467. }
  20468. // if you are inside of a sequence and the key you are pressing
  20469. // is not a modifier key then we should reset all sequences
  20470. // that were not matched by this key event
  20471. if (e.type == _inside_sequence && !_isModifier(character)) {
  20472. _resetSequences(do_not_reset);
  20473. }
  20474. }
  20475. /**
  20476. * handles a keydown event
  20477. *
  20478. * @param {Event} e
  20479. * @returns void
  20480. */
  20481. function _handleKey(e) {
  20482. // normalize e.which for key events
  20483. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  20484. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  20485. var character = _characterFromEvent(e);
  20486. // no character found then stop
  20487. if (!character) {
  20488. return;
  20489. }
  20490. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  20491. _ignore_next_keyup = false;
  20492. return;
  20493. }
  20494. _handleCharacter(character, e);
  20495. }
  20496. /**
  20497. * determines if the keycode specified is a modifier key or not
  20498. *
  20499. * @param {string} key
  20500. * @returns {boolean}
  20501. */
  20502. function _isModifier(key) {
  20503. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  20504. }
  20505. /**
  20506. * called to set a 1 second timeout on the specified sequence
  20507. *
  20508. * this is so after each key press in the sequence you have 1 second
  20509. * to press the next key before you have to start over
  20510. *
  20511. * @returns void
  20512. */
  20513. function _resetSequenceTimer() {
  20514. clearTimeout(_reset_timer);
  20515. _reset_timer = setTimeout(_resetSequences, 1000);
  20516. }
  20517. /**
  20518. * reverses the map lookup so that we can look for specific keys
  20519. * to see what can and can't use keypress
  20520. *
  20521. * @return {Object}
  20522. */
  20523. function _getReverseMap() {
  20524. if (!_REVERSE_MAP) {
  20525. _REVERSE_MAP = {};
  20526. for (var key in _MAP) {
  20527. // pull out the numeric keypad from here cause keypress should
  20528. // be able to detect the keys from the character
  20529. if (key > 95 && key < 112) {
  20530. continue;
  20531. }
  20532. if (_MAP.hasOwnProperty(key)) {
  20533. _REVERSE_MAP[_MAP[key]] = key;
  20534. }
  20535. }
  20536. }
  20537. return _REVERSE_MAP;
  20538. }
  20539. /**
  20540. * picks the best action based on the key combination
  20541. *
  20542. * @param {string} key - character for key
  20543. * @param {Array} modifiers
  20544. * @param {string=} action passed in
  20545. */
  20546. function _pickBestAction(key, modifiers, action) {
  20547. // if no action was picked in we should try to pick the one
  20548. // that we think would work best for this key
  20549. if (!action) {
  20550. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  20551. }
  20552. // modifier keys don't work as expected with keypress,
  20553. // switch to keydown
  20554. if (action == 'keypress' && modifiers.length) {
  20555. action = 'keydown';
  20556. }
  20557. return action;
  20558. }
  20559. /**
  20560. * binds a key sequence to an event
  20561. *
  20562. * @param {string} combo - combo specified in bind call
  20563. * @param {Array} keys
  20564. * @param {Function} callback
  20565. * @param {string=} action
  20566. * @returns void
  20567. */
  20568. function _bindSequence(combo, keys, callback, action) {
  20569. // start off by adding a sequence level record for this combination
  20570. // and setting the level to 0
  20571. _sequence_levels[combo] = 0;
  20572. // if there is no action pick the best one for the first key
  20573. // in the sequence
  20574. if (!action) {
  20575. action = _pickBestAction(keys[0], []);
  20576. }
  20577. /**
  20578. * callback to increase the sequence level for this sequence and reset
  20579. * all other sequences that were active
  20580. *
  20581. * @param {Event} e
  20582. * @returns void
  20583. */
  20584. var _increaseSequence = function(e) {
  20585. _inside_sequence = action;
  20586. ++_sequence_levels[combo];
  20587. _resetSequenceTimer();
  20588. },
  20589. /**
  20590. * wraps the specified callback inside of another function in order
  20591. * to reset all sequence counters as soon as this sequence is done
  20592. *
  20593. * @param {Event} e
  20594. * @returns void
  20595. */
  20596. _callbackAndReset = function(e) {
  20597. _fireCallback(callback, e);
  20598. // we should ignore the next key up if the action is key down
  20599. // or keypress. this is so if you finish a sequence and
  20600. // release the key the final key will not trigger a keyup
  20601. if (action !== 'keyup') {
  20602. _ignore_next_keyup = _characterFromEvent(e);
  20603. }
  20604. // weird race condition if a sequence ends with the key
  20605. // another sequence begins with
  20606. setTimeout(_resetSequences, 10);
  20607. },
  20608. i;
  20609. // loop through keys one at a time and bind the appropriate callback
  20610. // function. for any key leading up to the final one it should
  20611. // increase the sequence. after the final, it should reset all sequences
  20612. for (i = 0; i < keys.length; ++i) {
  20613. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  20614. }
  20615. }
  20616. /**
  20617. * binds a single keyboard combination
  20618. *
  20619. * @param {string} combination
  20620. * @param {Function} callback
  20621. * @param {string=} action
  20622. * @param {string=} sequence_name - name of sequence if part of sequence
  20623. * @param {number=} level - what part of the sequence the command is
  20624. * @returns void
  20625. */
  20626. function _bindSingle(combination, callback, action, sequence_name, level) {
  20627. // make sure multiple spaces in a row become a single space
  20628. combination = combination.replace(/\s+/g, ' ');
  20629. var sequence = combination.split(' '),
  20630. i,
  20631. key,
  20632. keys,
  20633. modifiers = [];
  20634. // if this pattern is a sequence of keys then run through this method
  20635. // to reprocess each pattern one key at a time
  20636. if (sequence.length > 1) {
  20637. return _bindSequence(combination, sequence, callback, action);
  20638. }
  20639. // take the keys from this pattern and figure out what the actual
  20640. // pattern is all about
  20641. keys = combination === '+' ? ['+'] : combination.split('+');
  20642. for (i = 0; i < keys.length; ++i) {
  20643. key = keys[i];
  20644. // normalize key names
  20645. if (_SPECIAL_ALIASES[key]) {
  20646. key = _SPECIAL_ALIASES[key];
  20647. }
  20648. // if this is not a keypress event then we should
  20649. // be smart about using shift keys
  20650. // this will only work for US keyboards however
  20651. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  20652. key = _SHIFT_MAP[key];
  20653. modifiers.push('shift');
  20654. }
  20655. // if this key is a modifier then add it to the list of modifiers
  20656. if (_isModifier(key)) {
  20657. modifiers.push(key);
  20658. }
  20659. }
  20660. // depending on what the key combination is
  20661. // we will try to pick the best event for it
  20662. action = _pickBestAction(key, modifiers, action);
  20663. // make sure to initialize array if this is the first time
  20664. // a callback is added for this key
  20665. if (!_callbacks[key]) {
  20666. _callbacks[key] = [];
  20667. }
  20668. // remove an existing match if there is one
  20669. _getMatches(key, modifiers, action, !sequence_name, combination);
  20670. // add this call back to the array
  20671. // if it is a sequence put it at the beginning
  20672. // if not put it at the end
  20673. //
  20674. // this is important because the way these are processed expects
  20675. // the sequence ones to come first
  20676. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  20677. callback: callback,
  20678. modifiers: modifiers,
  20679. action: action,
  20680. seq: sequence_name,
  20681. level: level,
  20682. combo: combination
  20683. });
  20684. }
  20685. /**
  20686. * binds multiple combinations to the same callback
  20687. *
  20688. * @param {Array} combinations
  20689. * @param {Function} callback
  20690. * @param {string|undefined} action
  20691. * @returns void
  20692. */
  20693. function _bindMultiple(combinations, callback, action) {
  20694. for (var i = 0; i < combinations.length; ++i) {
  20695. _bindSingle(combinations[i], callback, action);
  20696. }
  20697. }
  20698. // start!
  20699. _addEvent(document, 'keypress', _handleKey);
  20700. _addEvent(document, 'keydown', _handleKey);
  20701. _addEvent(document, 'keyup', _handleKey);
  20702. var mousetrap = {
  20703. /**
  20704. * binds an event to mousetrap
  20705. *
  20706. * can be a single key, a combination of keys separated with +,
  20707. * a comma separated list of keys, an array of keys, or
  20708. * a sequence of keys separated by spaces
  20709. *
  20710. * be sure to list the modifier keys first to make sure that the
  20711. * correct key ends up getting bound (the last key in the pattern)
  20712. *
  20713. * @param {string|Array} keys
  20714. * @param {Function} callback
  20715. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  20716. * @returns void
  20717. */
  20718. bind: function(keys, callback, action) {
  20719. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  20720. _direct_map[keys + ':' + action] = callback;
  20721. return this;
  20722. },
  20723. /**
  20724. * unbinds an event to mousetrap
  20725. *
  20726. * the unbinding sets the callback function of the specified key combo
  20727. * to an empty function and deletes the corresponding key in the
  20728. * _direct_map dict.
  20729. *
  20730. * the keycombo+action has to be exactly the same as
  20731. * it was defined in the bind method
  20732. *
  20733. * TODO: actually remove this from the _callbacks dictionary instead
  20734. * of binding an empty function
  20735. *
  20736. * @param {string|Array} keys
  20737. * @param {string} action
  20738. * @returns void
  20739. */
  20740. unbind: function(keys, action) {
  20741. if (_direct_map[keys + ':' + action]) {
  20742. delete _direct_map[keys + ':' + action];
  20743. this.bind(keys, function() {}, action);
  20744. }
  20745. return this;
  20746. },
  20747. /**
  20748. * triggers an event that has already been bound
  20749. *
  20750. * @param {string} keys
  20751. * @param {string=} action
  20752. * @returns void
  20753. */
  20754. trigger: function(keys, action) {
  20755. _direct_map[keys + ':' + action]();
  20756. return this;
  20757. },
  20758. /**
  20759. * resets the library back to its initial state. this is useful
  20760. * if you want to clear out the current keyboard shortcuts and bind
  20761. * new ones - for example if you switch to another page
  20762. *
  20763. * @returns void
  20764. */
  20765. reset: function() {
  20766. _callbacks = {};
  20767. _direct_map = {};
  20768. return this;
  20769. }
  20770. };
  20771. module.exports = mousetrap;
  20772. },{}]},{},[1])
  20773. (1)
  20774. });