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.

22644 lines
670 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
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.7.5-SNAPSHOT
  8. * @date 2014-04-29
  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 = 1, 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. * The order of the items is not determined.
  1550. * @param {function} callback
  1551. * @param {Object} [options] Available options:
  1552. * {Object.<String, String>} [convert]
  1553. * {String[]} [fields] filter fields
  1554. * {function} [filter] filter items
  1555. * {String | function} [order] Order the items by
  1556. * a field name or custom sort function.
  1557. */
  1558. DataSet.prototype.forEach = function (callback, options) {
  1559. var filter = options && options.filter,
  1560. convert = options && options.convert || this.options.convert,
  1561. data = this.data,
  1562. item,
  1563. id;
  1564. if (options && options.order) {
  1565. // execute forEach on ordered list
  1566. var items = this.get(options);
  1567. for (var i = 0, len = items.length; i < len; i++) {
  1568. item = items[i];
  1569. id = item[this.fieldId];
  1570. callback(item, id);
  1571. }
  1572. }
  1573. else {
  1574. // unordered
  1575. for (id in data) {
  1576. if (data.hasOwnProperty(id)) {
  1577. item = this._getItem(id, convert);
  1578. if (!filter || filter(item)) {
  1579. callback(item, id);
  1580. }
  1581. }
  1582. }
  1583. }
  1584. };
  1585. /**
  1586. * Map every item in the dataset.
  1587. * @param {function} callback
  1588. * @param {Object} [options] Available options:
  1589. * {Object.<String, String>} [convert]
  1590. * {String[]} [fields] filter fields
  1591. * {function} [filter] filter items
  1592. * {String | function} [order] Order the items by
  1593. * a field name or custom sort function.
  1594. * @return {Object[]} mappedItems
  1595. */
  1596. DataSet.prototype.map = function (callback, options) {
  1597. var filter = options && options.filter,
  1598. convert = options && options.convert || this.options.convert,
  1599. mappedItems = [],
  1600. data = this.data,
  1601. item;
  1602. // convert and filter items
  1603. for (var id in data) {
  1604. if (data.hasOwnProperty(id)) {
  1605. item = this._getItem(id, convert);
  1606. if (!filter || filter(item)) {
  1607. mappedItems.push(callback(item, id));
  1608. }
  1609. }
  1610. }
  1611. // order items
  1612. if (options && options.order) {
  1613. this._sort(mappedItems, options.order);
  1614. }
  1615. return mappedItems;
  1616. };
  1617. /**
  1618. * Filter the fields of an item
  1619. * @param {Object} item
  1620. * @param {String[]} fields Field names
  1621. * @return {Object} filteredItem
  1622. * @private
  1623. */
  1624. DataSet.prototype._filterFields = function (item, fields) {
  1625. var filteredItem = {};
  1626. for (var field in item) {
  1627. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1628. filteredItem[field] = item[field];
  1629. }
  1630. }
  1631. return filteredItem;
  1632. };
  1633. /**
  1634. * Sort the provided array with items
  1635. * @param {Object[]} items
  1636. * @param {String | function} order A field name or custom sort function.
  1637. * @private
  1638. */
  1639. DataSet.prototype._sort = function (items, order) {
  1640. if (util.isString(order)) {
  1641. // order by provided field name
  1642. var name = order; // field name
  1643. items.sort(function (a, b) {
  1644. var av = a[name];
  1645. var bv = b[name];
  1646. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1647. });
  1648. }
  1649. else if (typeof order === 'function') {
  1650. // order by sort function
  1651. items.sort(order);
  1652. }
  1653. // TODO: extend order by an Object {field:String, direction:String}
  1654. // where direction can be 'asc' or 'desc'
  1655. else {
  1656. throw new TypeError('Order must be a function or a string');
  1657. }
  1658. };
  1659. /**
  1660. * Remove an object by pointer or by id
  1661. * @param {String | Number | Object | Array} id Object or id, or an array with
  1662. * objects or ids to be removed
  1663. * @param {String} [senderId] Optional sender id
  1664. * @return {Array} removedIds
  1665. */
  1666. DataSet.prototype.remove = function (id, senderId) {
  1667. var removedIds = [],
  1668. i, len, removedId;
  1669. if (id instanceof Array) {
  1670. for (i = 0, len = id.length; i < len; i++) {
  1671. removedId = this._remove(id[i]);
  1672. if (removedId != null) {
  1673. removedIds.push(removedId);
  1674. }
  1675. }
  1676. }
  1677. else {
  1678. removedId = this._remove(id);
  1679. if (removedId != null) {
  1680. removedIds.push(removedId);
  1681. }
  1682. }
  1683. if (removedIds.length) {
  1684. this._trigger('remove', {items: removedIds}, senderId);
  1685. }
  1686. return removedIds;
  1687. };
  1688. /**
  1689. * Remove an item by its id
  1690. * @param {Number | String | Object} id id or item
  1691. * @returns {Number | String | null} id
  1692. * @private
  1693. */
  1694. DataSet.prototype._remove = function (id) {
  1695. if (util.isNumber(id) || util.isString(id)) {
  1696. if (this.data[id]) {
  1697. delete this.data[id];
  1698. delete this.internalIds[id];
  1699. return id;
  1700. }
  1701. }
  1702. else if (id instanceof Object) {
  1703. var itemId = id[this.fieldId];
  1704. if (itemId && this.data[itemId]) {
  1705. delete this.data[itemId];
  1706. delete this.internalIds[itemId];
  1707. return itemId;
  1708. }
  1709. }
  1710. return null;
  1711. };
  1712. /**
  1713. * Clear the data
  1714. * @param {String} [senderId] Optional sender id
  1715. * @return {Array} removedIds The ids of all removed items
  1716. */
  1717. DataSet.prototype.clear = function (senderId) {
  1718. var ids = Object.keys(this.data);
  1719. this.data = {};
  1720. this.internalIds = {};
  1721. this._trigger('remove', {items: ids}, senderId);
  1722. return ids;
  1723. };
  1724. /**
  1725. * Find the item with maximum value of a specified field
  1726. * @param {String} field
  1727. * @return {Object | null} item Item containing max value, or null if no items
  1728. */
  1729. DataSet.prototype.max = function (field) {
  1730. var data = this.data,
  1731. max = null,
  1732. maxField = null;
  1733. for (var id in data) {
  1734. if (data.hasOwnProperty(id)) {
  1735. var item = data[id];
  1736. var itemField = item[field];
  1737. if (itemField != null && (!max || itemField > maxField)) {
  1738. max = item;
  1739. maxField = itemField;
  1740. }
  1741. }
  1742. }
  1743. return max;
  1744. };
  1745. /**
  1746. * Find the item with minimum value of a specified field
  1747. * @param {String} field
  1748. * @return {Object | null} item Item containing max value, or null if no items
  1749. */
  1750. DataSet.prototype.min = function (field) {
  1751. var data = this.data,
  1752. min = null,
  1753. minField = null;
  1754. for (var id in data) {
  1755. if (data.hasOwnProperty(id)) {
  1756. var item = data[id];
  1757. var itemField = item[field];
  1758. if (itemField != null && (!min || itemField < minField)) {
  1759. min = item;
  1760. minField = itemField;
  1761. }
  1762. }
  1763. }
  1764. return min;
  1765. };
  1766. /**
  1767. * Find all distinct values of a specified field
  1768. * @param {String} field
  1769. * @return {Array} values Array containing all distinct values. If the data
  1770. * items do not contain the specified field, an array
  1771. * containing a single value undefined is returned.
  1772. * The returned array is unordered.
  1773. */
  1774. DataSet.prototype.distinct = function (field) {
  1775. var data = this.data,
  1776. values = [],
  1777. fieldType = this.options.convert[field],
  1778. count = 0;
  1779. for (var prop in data) {
  1780. if (data.hasOwnProperty(prop)) {
  1781. var item = data[prop];
  1782. var value = util.convert(item[field], fieldType);
  1783. var exists = false;
  1784. for (var i = 0; i < count; i++) {
  1785. if (values[i] == value) {
  1786. exists = true;
  1787. break;
  1788. }
  1789. }
  1790. if (!exists) {
  1791. values[count] = value;
  1792. count++;
  1793. }
  1794. }
  1795. }
  1796. return values;
  1797. };
  1798. /**
  1799. * Add a single item. Will fail when an item with the same id already exists.
  1800. * @param {Object} item
  1801. * @return {String} id
  1802. * @private
  1803. */
  1804. DataSet.prototype._addItem = function (item) {
  1805. var id = item[this.fieldId];
  1806. if (id != undefined) {
  1807. // check whether this id is already taken
  1808. if (this.data[id]) {
  1809. // item already exists
  1810. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1811. }
  1812. }
  1813. else {
  1814. // generate an id
  1815. id = util.randomUUID();
  1816. item[this.fieldId] = id;
  1817. this.internalIds[id] = item;
  1818. }
  1819. var d = {};
  1820. for (var field in item) {
  1821. if (item.hasOwnProperty(field)) {
  1822. var fieldType = this.convert[field]; // type may be undefined
  1823. d[field] = util.convert(item[field], fieldType);
  1824. }
  1825. }
  1826. this.data[id] = d;
  1827. return id;
  1828. };
  1829. /**
  1830. * Get an item. Fields can be converted to a specific type
  1831. * @param {String} id
  1832. * @param {Object.<String, String>} [convert] field types to convert
  1833. * @return {Object | null} item
  1834. * @private
  1835. */
  1836. DataSet.prototype._getItem = function (id, convert) {
  1837. var field, value;
  1838. // get the item from the dataset
  1839. var raw = this.data[id];
  1840. if (!raw) {
  1841. return null;
  1842. }
  1843. // convert the items field types
  1844. var converted = {},
  1845. fieldId = this.fieldId,
  1846. internalIds = this.internalIds;
  1847. if (convert) {
  1848. for (field in raw) {
  1849. if (raw.hasOwnProperty(field)) {
  1850. value = raw[field];
  1851. // output all fields, except internal ids
  1852. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1853. converted[field] = util.convert(value, convert[field]);
  1854. }
  1855. }
  1856. }
  1857. }
  1858. else {
  1859. // no field types specified, no converting needed
  1860. for (field in raw) {
  1861. if (raw.hasOwnProperty(field)) {
  1862. value = raw[field];
  1863. // output all fields, except internal ids
  1864. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1865. converted[field] = value;
  1866. }
  1867. }
  1868. }
  1869. }
  1870. return converted;
  1871. };
  1872. /**
  1873. * Update a single item: merge with existing item.
  1874. * Will fail when the item has no id, or when there does not exist an item
  1875. * with the same id.
  1876. * @param {Object} item
  1877. * @return {String} id
  1878. * @private
  1879. */
  1880. DataSet.prototype._updateItem = function (item) {
  1881. var id = item[this.fieldId];
  1882. if (id == undefined) {
  1883. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1884. }
  1885. var d = this.data[id];
  1886. if (!d) {
  1887. // item doesn't exist
  1888. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1889. }
  1890. // merge with current item
  1891. for (var field in item) {
  1892. if (item.hasOwnProperty(field)) {
  1893. var fieldType = this.convert[field]; // type may be undefined
  1894. d[field] = util.convert(item[field], fieldType);
  1895. }
  1896. }
  1897. return id;
  1898. };
  1899. /**
  1900. * check if an id is an internal or external id
  1901. * @param id
  1902. * @returns {boolean}
  1903. * @private
  1904. */
  1905. DataSet.prototype.isInternalId = function(id) {
  1906. return (id in this.internalIds);
  1907. };
  1908. /**
  1909. * Get an array with the column names of a Google DataTable
  1910. * @param {DataTable} dataTable
  1911. * @return {String[]} columnNames
  1912. * @private
  1913. */
  1914. DataSet.prototype._getColumnNames = function (dataTable) {
  1915. var columns = [];
  1916. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1917. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1918. }
  1919. return columns;
  1920. };
  1921. /**
  1922. * Append an item as a row to the dataTable
  1923. * @param dataTable
  1924. * @param columns
  1925. * @param item
  1926. * @private
  1927. */
  1928. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1929. var row = dataTable.addRow();
  1930. for (var col = 0, cols = columns.length; col < cols; col++) {
  1931. var field = columns[col];
  1932. dataTable.setValue(row, col, item[field]);
  1933. }
  1934. };
  1935. /**
  1936. * DataView
  1937. *
  1938. * a dataview offers a filtered view on a dataset or an other dataview.
  1939. *
  1940. * @param {DataSet | DataView} data
  1941. * @param {Object} [options] Available options: see method get
  1942. *
  1943. * @constructor DataView
  1944. */
  1945. function DataView (data, options) {
  1946. this.id = util.randomUUID();
  1947. this.data = null;
  1948. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1949. this.options = options || {};
  1950. this.fieldId = 'id'; // name of the field containing id
  1951. this.subscribers = {}; // event subscribers
  1952. var me = this;
  1953. this.listener = function () {
  1954. me._onEvent.apply(me, arguments);
  1955. };
  1956. this.setData(data);
  1957. }
  1958. // TODO: implement a function .config() to dynamically update things like configured filter
  1959. // and trigger changes accordingly
  1960. /**
  1961. * Set a data source for the view
  1962. * @param {DataSet | DataView} data
  1963. */
  1964. DataView.prototype.setData = function (data) {
  1965. var ids, dataItems, i, len;
  1966. if (this.data) {
  1967. // unsubscribe from current dataset
  1968. if (this.data.unsubscribe) {
  1969. this.data.unsubscribe('*', this.listener);
  1970. }
  1971. // trigger a remove of all items in memory
  1972. ids = [];
  1973. for (var id in this.ids) {
  1974. if (this.ids.hasOwnProperty(id)) {
  1975. ids.push(id);
  1976. }
  1977. }
  1978. this.ids = {};
  1979. this._trigger('remove', {items: ids});
  1980. }
  1981. this.data = data;
  1982. if (this.data) {
  1983. // update fieldId
  1984. this.fieldId = this.options.fieldId ||
  1985. (this.data && this.data.options && this.data.options.fieldId) ||
  1986. 'id';
  1987. // trigger an add of all added items
  1988. ids = this.data.getIds({filter: this.options && this.options.filter});
  1989. for (i = 0, len = ids.length; i < len; i++) {
  1990. id = ids[i];
  1991. this.ids[id] = true;
  1992. }
  1993. this._trigger('add', {items: ids});
  1994. // subscribe to new dataset
  1995. if (this.data.on) {
  1996. this.data.on('*', this.listener);
  1997. }
  1998. }
  1999. };
  2000. /**
  2001. * Get data from the data view
  2002. *
  2003. * Usage:
  2004. *
  2005. * get()
  2006. * get(options: Object)
  2007. * get(options: Object, data: Array | DataTable)
  2008. *
  2009. * get(id: Number)
  2010. * get(id: Number, options: Object)
  2011. * get(id: Number, options: Object, data: Array | DataTable)
  2012. *
  2013. * get(ids: Number[])
  2014. * get(ids: Number[], options: Object)
  2015. * get(ids: Number[], options: Object, data: Array | DataTable)
  2016. *
  2017. * Where:
  2018. *
  2019. * {Number | String} id The id of an item
  2020. * {Number[] | String{}} ids An array with ids of items
  2021. * {Object} options An Object with options. Available options:
  2022. * {String} [type] Type of data to be returned. Can
  2023. * be 'DataTable' or 'Array' (default)
  2024. * {Object.<String, String>} [convert]
  2025. * {String[]} [fields] field names to be returned
  2026. * {function} [filter] filter items
  2027. * {String | function} [order] Order the items by
  2028. * a field name or custom sort function.
  2029. * {Array | DataTable} [data] If provided, items will be appended to this
  2030. * array or table. Required in case of Google
  2031. * DataTable.
  2032. * @param args
  2033. */
  2034. DataView.prototype.get = function (args) {
  2035. var me = this;
  2036. // parse the arguments
  2037. var ids, options, data;
  2038. var firstType = util.getType(arguments[0]);
  2039. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2040. // get(id(s) [, options] [, data])
  2041. ids = arguments[0]; // can be a single id or an array with ids
  2042. options = arguments[1];
  2043. data = arguments[2];
  2044. }
  2045. else {
  2046. // get([, options] [, data])
  2047. options = arguments[0];
  2048. data = arguments[1];
  2049. }
  2050. // extend the options with the default options and provided options
  2051. var viewOptions = util.extend({}, this.options, options);
  2052. // create a combined filter method when needed
  2053. if (this.options.filter && options && options.filter) {
  2054. viewOptions.filter = function (item) {
  2055. return me.options.filter(item) && options.filter(item);
  2056. }
  2057. }
  2058. // build up the call to the linked data set
  2059. var getArguments = [];
  2060. if (ids != undefined) {
  2061. getArguments.push(ids);
  2062. }
  2063. getArguments.push(viewOptions);
  2064. getArguments.push(data);
  2065. return this.data && this.data.get.apply(this.data, getArguments);
  2066. };
  2067. /**
  2068. * Get ids of all items or from a filtered set of items.
  2069. * @param {Object} [options] An Object with options. Available options:
  2070. * {function} [filter] filter items
  2071. * {String | function} [order] Order the items by
  2072. * a field name or custom sort function.
  2073. * @return {Array} ids
  2074. */
  2075. DataView.prototype.getIds = function (options) {
  2076. var ids;
  2077. if (this.data) {
  2078. var defaultFilter = this.options.filter;
  2079. var filter;
  2080. if (options && options.filter) {
  2081. if (defaultFilter) {
  2082. filter = function (item) {
  2083. return defaultFilter(item) && options.filter(item);
  2084. }
  2085. }
  2086. else {
  2087. filter = options.filter;
  2088. }
  2089. }
  2090. else {
  2091. filter = defaultFilter;
  2092. }
  2093. ids = this.data.getIds({
  2094. filter: filter,
  2095. order: options && options.order
  2096. });
  2097. }
  2098. else {
  2099. ids = [];
  2100. }
  2101. return ids;
  2102. };
  2103. /**
  2104. * Event listener. Will propagate all events from the connected data set to
  2105. * the subscribers of the DataView, but will filter the items and only trigger
  2106. * when there are changes in the filtered data set.
  2107. * @param {String} event
  2108. * @param {Object | null} params
  2109. * @param {String} senderId
  2110. * @private
  2111. */
  2112. DataView.prototype._onEvent = function (event, params, senderId) {
  2113. var i, len, id, item,
  2114. ids = params && params.items,
  2115. data = this.data,
  2116. added = [],
  2117. updated = [],
  2118. removed = [];
  2119. if (ids && data) {
  2120. switch (event) {
  2121. case 'add':
  2122. // filter the ids of the added items
  2123. for (i = 0, len = ids.length; i < len; i++) {
  2124. id = ids[i];
  2125. item = this.get(id);
  2126. if (item) {
  2127. this.ids[id] = true;
  2128. added.push(id);
  2129. }
  2130. }
  2131. break;
  2132. case 'update':
  2133. // determine the event from the views viewpoint: an updated
  2134. // item can be added, updated, or removed from this view.
  2135. for (i = 0, len = ids.length; i < len; i++) {
  2136. id = ids[i];
  2137. item = this.get(id);
  2138. if (item) {
  2139. if (this.ids[id]) {
  2140. updated.push(id);
  2141. }
  2142. else {
  2143. this.ids[id] = true;
  2144. added.push(id);
  2145. }
  2146. }
  2147. else {
  2148. if (this.ids[id]) {
  2149. delete this.ids[id];
  2150. removed.push(id);
  2151. }
  2152. else {
  2153. // nothing interesting for me :-(
  2154. }
  2155. }
  2156. }
  2157. break;
  2158. case 'remove':
  2159. // filter the ids of the removed items
  2160. for (i = 0, len = ids.length; i < len; i++) {
  2161. id = ids[i];
  2162. if (this.ids[id]) {
  2163. delete this.ids[id];
  2164. removed.push(id);
  2165. }
  2166. }
  2167. break;
  2168. }
  2169. if (added.length) {
  2170. this._trigger('add', {items: added}, senderId);
  2171. }
  2172. if (updated.length) {
  2173. this._trigger('update', {items: updated}, senderId);
  2174. }
  2175. if (removed.length) {
  2176. this._trigger('remove', {items: removed}, senderId);
  2177. }
  2178. }
  2179. };
  2180. // copy subscription functionality from DataSet
  2181. DataView.prototype.on = DataSet.prototype.on;
  2182. DataView.prototype.off = DataSet.prototype.off;
  2183. DataView.prototype._trigger = DataSet.prototype._trigger;
  2184. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2185. DataView.prototype.subscribe = DataView.prototype.on;
  2186. DataView.prototype.unsubscribe = DataView.prototype.off;
  2187. /**
  2188. * @constructor TimeStep
  2189. * The class TimeStep is an iterator for dates. You provide a start date and an
  2190. * end date. The class itself determines the best scale (step size) based on the
  2191. * provided start Date, end Date, and minimumStep.
  2192. *
  2193. * If minimumStep is provided, the step size is chosen as close as possible
  2194. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2195. * provided, the scale is set to 1 DAY.
  2196. * The minimumStep should correspond with the onscreen size of about 6 characters
  2197. *
  2198. * Alternatively, you can set a scale by hand.
  2199. * After creation, you can initialize the class by executing first(). Then you
  2200. * can iterate from the start date to the end date via next(). You can check if
  2201. * the end date is reached with the function hasNext(). After each step, you can
  2202. * retrieve the current date via getCurrent().
  2203. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2204. * days, to years.
  2205. *
  2206. * Version: 1.2
  2207. *
  2208. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2209. * or new Date(2010, 9, 21, 23, 45, 00)
  2210. * @param {Date} [end] The end date
  2211. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2212. */
  2213. TimeStep = function(start, end, minimumStep) {
  2214. // variables
  2215. this.current = new Date();
  2216. this._start = new Date();
  2217. this._end = new Date();
  2218. this.autoScale = true;
  2219. this.scale = TimeStep.SCALE.DAY;
  2220. this.step = 1;
  2221. // initialize the range
  2222. this.setRange(start, end, minimumStep);
  2223. };
  2224. /// enum scale
  2225. TimeStep.SCALE = {
  2226. MILLISECOND: 1,
  2227. SECOND: 2,
  2228. MINUTE: 3,
  2229. HOUR: 4,
  2230. DAY: 5,
  2231. WEEKDAY: 6,
  2232. MONTH: 7,
  2233. YEAR: 8
  2234. };
  2235. /**
  2236. * Set a new range
  2237. * If minimumStep is provided, the step size is chosen as close as possible
  2238. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2239. * provided, the scale is set to 1 DAY.
  2240. * The minimumStep should correspond with the onscreen size of about 6 characters
  2241. * @param {Date} [start] The start date and time.
  2242. * @param {Date} [end] The end date and time.
  2243. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2244. */
  2245. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2246. if (!(start instanceof Date) || !(end instanceof Date)) {
  2247. throw "No legal start or end date in method setRange";
  2248. }
  2249. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2250. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2251. if (this.autoScale) {
  2252. this.setMinimumStep(minimumStep);
  2253. }
  2254. };
  2255. /**
  2256. * Set the range iterator to the start date.
  2257. */
  2258. TimeStep.prototype.first = function() {
  2259. this.current = new Date(this._start.valueOf());
  2260. this.roundToMinor();
  2261. };
  2262. /**
  2263. * Round the current date to the first minor date value
  2264. * This must be executed once when the current date is set to start Date
  2265. */
  2266. TimeStep.prototype.roundToMinor = function() {
  2267. // round to floor
  2268. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2269. //noinspection FallthroughInSwitchStatementJS
  2270. switch (this.scale) {
  2271. case TimeStep.SCALE.YEAR:
  2272. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2273. this.current.setMonth(0);
  2274. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2275. case TimeStep.SCALE.DAY: // intentional fall through
  2276. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2277. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2278. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2279. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2280. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2281. }
  2282. if (this.step != 1) {
  2283. // round down to the first minor value that is a multiple of the current step size
  2284. switch (this.scale) {
  2285. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2286. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2287. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2288. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2289. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2290. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2291. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2292. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2293. default: break;
  2294. }
  2295. }
  2296. };
  2297. /**
  2298. * Check if the there is a next step
  2299. * @return {boolean} true if the current date has not passed the end date
  2300. */
  2301. TimeStep.prototype.hasNext = function () {
  2302. return (this.current.valueOf() <= this._end.valueOf());
  2303. };
  2304. /**
  2305. * Do the next step
  2306. */
  2307. TimeStep.prototype.next = function() {
  2308. var prev = this.current.valueOf();
  2309. // Two cases, needed to prevent issues with switching daylight savings
  2310. // (end of March and end of October)
  2311. if (this.current.getMonth() < 6) {
  2312. switch (this.scale) {
  2313. case TimeStep.SCALE.MILLISECOND:
  2314. this.current = new Date(this.current.valueOf() + this.step); break;
  2315. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2316. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2317. case TimeStep.SCALE.HOUR:
  2318. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2319. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2320. var h = this.current.getHours();
  2321. this.current.setHours(h - (h % this.step));
  2322. break;
  2323. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2324. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2325. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2326. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2327. default: break;
  2328. }
  2329. }
  2330. else {
  2331. switch (this.scale) {
  2332. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2333. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2334. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2335. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2336. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2337. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2338. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2339. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2340. default: break;
  2341. }
  2342. }
  2343. if (this.step != 1) {
  2344. // round down to the correct major value
  2345. switch (this.scale) {
  2346. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2347. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2348. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2349. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2350. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2351. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2352. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2353. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2354. default: break;
  2355. }
  2356. }
  2357. // safety mechanism: if current time is still unchanged, move to the end
  2358. if (this.current.valueOf() == prev) {
  2359. this.current = new Date(this._end.valueOf());
  2360. }
  2361. };
  2362. /**
  2363. * Get the current datetime
  2364. * @return {Date} current The current date
  2365. */
  2366. TimeStep.prototype.getCurrent = function() {
  2367. return this.current;
  2368. };
  2369. /**
  2370. * Set a custom scale. Autoscaling will be disabled.
  2371. * For example setScale(SCALE.MINUTES, 5) will result
  2372. * in minor steps of 5 minutes, and major steps of an hour.
  2373. *
  2374. * @param {TimeStep.SCALE} newScale
  2375. * A scale. Choose from SCALE.MILLISECOND,
  2376. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2377. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2378. * SCALE.YEAR.
  2379. * @param {Number} newStep A step size, by default 1. Choose for
  2380. * example 1, 2, 5, or 10.
  2381. */
  2382. TimeStep.prototype.setScale = function(newScale, newStep) {
  2383. this.scale = newScale;
  2384. if (newStep > 0) {
  2385. this.step = newStep;
  2386. }
  2387. this.autoScale = false;
  2388. };
  2389. /**
  2390. * Enable or disable autoscaling
  2391. * @param {boolean} enable If true, autoascaling is set true
  2392. */
  2393. TimeStep.prototype.setAutoScale = function (enable) {
  2394. this.autoScale = enable;
  2395. };
  2396. /**
  2397. * Automatically determine the scale that bests fits the provided minimum step
  2398. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2399. */
  2400. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2401. if (minimumStep == undefined) {
  2402. return;
  2403. }
  2404. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2405. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2406. var stepDay = (1000 * 60 * 60 * 24);
  2407. var stepHour = (1000 * 60 * 60);
  2408. var stepMinute = (1000 * 60);
  2409. var stepSecond = (1000);
  2410. var stepMillisecond= (1);
  2411. // find the smallest step that is larger than the provided minimumStep
  2412. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2413. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2414. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2415. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2416. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2417. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2418. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2419. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2420. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2421. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2422. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2423. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2424. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2425. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2426. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2427. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2428. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2429. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2430. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2431. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2432. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2433. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2434. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2435. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2436. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2437. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2438. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2439. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2440. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2441. };
  2442. /**
  2443. * Snap a date to a rounded value.
  2444. * The snap intervals are dependent on the current scale and step.
  2445. * @param {Date} date the date to be snapped.
  2446. * @return {Date} snappedDate
  2447. */
  2448. TimeStep.prototype.snap = function(date) {
  2449. var clone = new Date(date.valueOf());
  2450. if (this.scale == TimeStep.SCALE.YEAR) {
  2451. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2452. clone.setFullYear(Math.round(year / this.step) * this.step);
  2453. clone.setMonth(0);
  2454. clone.setDate(0);
  2455. clone.setHours(0);
  2456. clone.setMinutes(0);
  2457. clone.setSeconds(0);
  2458. clone.setMilliseconds(0);
  2459. }
  2460. else if (this.scale == TimeStep.SCALE.MONTH) {
  2461. if (clone.getDate() > 15) {
  2462. clone.setDate(1);
  2463. clone.setMonth(clone.getMonth() + 1);
  2464. // important: first set Date to 1, after that change the month.
  2465. }
  2466. else {
  2467. clone.setDate(1);
  2468. }
  2469. clone.setHours(0);
  2470. clone.setMinutes(0);
  2471. clone.setSeconds(0);
  2472. clone.setMilliseconds(0);
  2473. }
  2474. else if (this.scale == TimeStep.SCALE.DAY ||
  2475. this.scale == TimeStep.SCALE.WEEKDAY) {
  2476. //noinspection FallthroughInSwitchStatementJS
  2477. switch (this.step) {
  2478. case 5:
  2479. case 2:
  2480. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2481. default:
  2482. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2483. }
  2484. clone.setMinutes(0);
  2485. clone.setSeconds(0);
  2486. clone.setMilliseconds(0);
  2487. }
  2488. else if (this.scale == TimeStep.SCALE.HOUR) {
  2489. switch (this.step) {
  2490. case 4:
  2491. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2492. default:
  2493. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2494. }
  2495. clone.setSeconds(0);
  2496. clone.setMilliseconds(0);
  2497. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2498. //noinspection FallthroughInSwitchStatementJS
  2499. switch (this.step) {
  2500. case 15:
  2501. case 10:
  2502. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2503. clone.setSeconds(0);
  2504. break;
  2505. case 5:
  2506. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2507. default:
  2508. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2509. }
  2510. clone.setMilliseconds(0);
  2511. }
  2512. else if (this.scale == TimeStep.SCALE.SECOND) {
  2513. //noinspection FallthroughInSwitchStatementJS
  2514. switch (this.step) {
  2515. case 15:
  2516. case 10:
  2517. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2518. clone.setMilliseconds(0);
  2519. break;
  2520. case 5:
  2521. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2522. default:
  2523. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2524. }
  2525. }
  2526. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2527. var step = this.step > 5 ? this.step / 2 : 1;
  2528. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2529. }
  2530. return clone;
  2531. };
  2532. /**
  2533. * Check if the current value is a major value (for example when the step
  2534. * is DAY, a major value is each first day of the MONTH)
  2535. * @return {boolean} true if current date is major, else false.
  2536. */
  2537. TimeStep.prototype.isMajor = function() {
  2538. switch (this.scale) {
  2539. case TimeStep.SCALE.MILLISECOND:
  2540. return (this.current.getMilliseconds() == 0);
  2541. case TimeStep.SCALE.SECOND:
  2542. return (this.current.getSeconds() == 0);
  2543. case TimeStep.SCALE.MINUTE:
  2544. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2545. // Note: this is no bug. Major label is equal for both minute and hour scale
  2546. case TimeStep.SCALE.HOUR:
  2547. return (this.current.getHours() == 0);
  2548. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2549. case TimeStep.SCALE.DAY:
  2550. return (this.current.getDate() == 1);
  2551. case TimeStep.SCALE.MONTH:
  2552. return (this.current.getMonth() == 0);
  2553. case TimeStep.SCALE.YEAR:
  2554. return false;
  2555. default:
  2556. return false;
  2557. }
  2558. };
  2559. /**
  2560. * Returns formatted text for the minor axislabel, depending on the current
  2561. * date and the scale. For example when scale is MINUTE, the current time is
  2562. * formatted as "hh:mm".
  2563. * @param {Date} [date] custom date. if not provided, current date is taken
  2564. */
  2565. TimeStep.prototype.getLabelMinor = function(date) {
  2566. if (date == undefined) {
  2567. date = this.current;
  2568. }
  2569. switch (this.scale) {
  2570. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2571. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2572. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2573. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2574. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2575. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2576. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2577. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2578. default: return '';
  2579. }
  2580. };
  2581. /**
  2582. * Returns formatted text for the major axis label, depending on the current
  2583. * date and the scale. For example when scale is MINUTE, the major scale is
  2584. * hours, and the hour will be formatted as "hh".
  2585. * @param {Date} [date] custom date. if not provided, current date is taken
  2586. */
  2587. TimeStep.prototype.getLabelMajor = function(date) {
  2588. if (date == undefined) {
  2589. date = this.current;
  2590. }
  2591. //noinspection FallthroughInSwitchStatementJS
  2592. switch (this.scale) {
  2593. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2594. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2595. case TimeStep.SCALE.MINUTE:
  2596. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2597. case TimeStep.SCALE.WEEKDAY:
  2598. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2599. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2600. case TimeStep.SCALE.YEAR: return '';
  2601. default: return '';
  2602. }
  2603. };
  2604. // TODO: turn Stack into a Mixin?
  2605. /**
  2606. * @constructor Stack
  2607. * Stacks items on top of each other.
  2608. * @param {Object} [options]
  2609. */
  2610. function Stack (options) {
  2611. this.options = options || {};
  2612. this.defaultOptions = {
  2613. order: function (a, b) {
  2614. // Order: ranges over non-ranges, ranged ordered by width,
  2615. // and non-ranges ordered by start.
  2616. if (a instanceof ItemRange) {
  2617. if (b instanceof ItemRange) {
  2618. var aInt = (a.data.end - a.data.start);
  2619. var bInt = (b.data.end - b.data.start);
  2620. return (aInt - bInt) || (a.data.start - b.data.start);
  2621. }
  2622. else {
  2623. return -1;
  2624. }
  2625. }
  2626. else {
  2627. if (b instanceof ItemRange) {
  2628. return 1;
  2629. }
  2630. else {
  2631. return (a.data.start - b.data.start);
  2632. }
  2633. }
  2634. },
  2635. margin: {
  2636. item: 10,
  2637. axis: 20
  2638. }
  2639. };
  2640. }
  2641. /**
  2642. * Set options for the stack
  2643. * @param {Object} options Available options:
  2644. * {Number} [margin.item=10]
  2645. * {Number} [margin.axis=20]
  2646. * {function} [order] Stacking order
  2647. */
  2648. Stack.prototype.setOptions = function setOptions (options) {
  2649. util.extend(this.options, options);
  2650. };
  2651. /**
  2652. * Order an array with items using a predefined order function for items
  2653. * @param {Item[]} items
  2654. */
  2655. Stack.prototype.order = function order(items) {
  2656. //order the items
  2657. var order = this.options.order || this.defaultOptions.order;
  2658. if (!(typeof order === 'function')) {
  2659. throw new Error('Option order must be a function');
  2660. }
  2661. items.sort(order);
  2662. };
  2663. /**
  2664. * Order items by their start data
  2665. * @param {Item[]} items
  2666. */
  2667. Stack.prototype.orderByStart = function orderByStart(items) {
  2668. items.sort(function (a, b) {
  2669. return a.data.start - b.data.start;
  2670. });
  2671. };
  2672. /**
  2673. * Order items by their end date. If they have no end date, their start date
  2674. * is used.
  2675. * @param {Item[]} items
  2676. */
  2677. Stack.prototype.orderByEnd = function orderByEnd(items) {
  2678. items.sort(function (a, b) {
  2679. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  2680. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  2681. return aTime - bTime;
  2682. });
  2683. };
  2684. /**
  2685. * Adjust vertical positions of the events such that they don't overlap each
  2686. * other.
  2687. * @param {Item[]} items All visible items
  2688. * @param {boolean} [force=false] If true, all items will be re-stacked.
  2689. * If false (default), only items having a
  2690. * top===null will be re-stacked
  2691. * @private
  2692. */
  2693. Stack.prototype.stack = function stack (items, force) {
  2694. var i,
  2695. iMax,
  2696. options = this.options,
  2697. marginItem,
  2698. marginAxis;
  2699. if (options.margin && options.margin.item !== undefined) {
  2700. marginItem = options.margin.item;
  2701. }
  2702. else {
  2703. marginItem = this.defaultOptions.margin.item
  2704. }
  2705. if (options.margin && options.margin.axis !== undefined) {
  2706. marginAxis = options.margin.axis;
  2707. }
  2708. else {
  2709. marginAxis = this.defaultOptions.margin.axis
  2710. }
  2711. if (force) {
  2712. // reset top position of all items
  2713. for (i = 0, iMax = items.length; i < iMax; i++) {
  2714. items[i].top = null;
  2715. }
  2716. }
  2717. // calculate new, non-overlapping positions
  2718. for (i = 0, iMax = items.length; i < iMax; i++) {
  2719. var item = items[i];
  2720. if (item.top === null) {
  2721. // initialize top position
  2722. item.top = marginAxis;
  2723. do {
  2724. // TODO: optimize checking for overlap. when there is a gap without items,
  2725. // you only need to check for items from the next item on, not from zero
  2726. var collidingItem = null;
  2727. for (var j = 0, jj = items.length; j < jj; j++) {
  2728. var other = items[j];
  2729. if (other.top !== null && other !== item && this.collision(item, other, marginItem)) {
  2730. collidingItem = other;
  2731. break;
  2732. }
  2733. }
  2734. if (collidingItem != null) {
  2735. // There is a collision. Reposition the event above the colliding element
  2736. item.top = collidingItem.top + collidingItem.height + marginItem;
  2737. }
  2738. } while (collidingItem);
  2739. }
  2740. }
  2741. };
  2742. /**
  2743. * Test if the two provided items collide
  2744. * The items must have parameters left, width, top, and height.
  2745. * @param {Component} a The first item
  2746. * @param {Component} b The second item
  2747. * @param {Number} margin A minimum required margin.
  2748. * If margin is provided, the two items will be
  2749. * marked colliding when they overlap or
  2750. * when the margin between the two is smaller than
  2751. * the requested margin.
  2752. * @return {boolean} true if a and b collide, else false
  2753. */
  2754. Stack.prototype.collision = function collision (a, b, margin) {
  2755. return ((a.left - margin) < (b.left + b.width) &&
  2756. (a.left + a.width + margin) > b.left &&
  2757. (a.top - margin) < (b.top + b.height) &&
  2758. (a.top + a.height + margin) > b.top);
  2759. };
  2760. /**
  2761. * @constructor Range
  2762. * A Range controls a numeric range with a start and end value.
  2763. * The Range adjusts the range based on mouse events or programmatic changes,
  2764. * and triggers events when the range is changing or has been changed.
  2765. * @param {RootPanel} root Root panel, used to subscribe to events
  2766. * @param {Panel} parent Parent panel, used to attach to the DOM
  2767. * @param {Object} [options] See description at Range.setOptions
  2768. */
  2769. function Range(root, parent, options) {
  2770. this.id = util.randomUUID();
  2771. this.start = null; // Number
  2772. this.end = null; // Number
  2773. this.root = root;
  2774. this.parent = parent;
  2775. this.options = options || {};
  2776. // drag listeners for dragging
  2777. this.root.on('dragstart', this._onDragStart.bind(this));
  2778. this.root.on('drag', this._onDrag.bind(this));
  2779. this.root.on('dragend', this._onDragEnd.bind(this));
  2780. // ignore dragging when holding
  2781. this.root.on('hold', this._onHold.bind(this));
  2782. // mouse wheel for zooming
  2783. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  2784. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  2785. // pinch to zoom
  2786. this.root.on('touch', this._onTouch.bind(this));
  2787. this.root.on('pinch', this._onPinch.bind(this));
  2788. this.setOptions(options);
  2789. }
  2790. // turn Range into an event emitter
  2791. Emitter(Range.prototype);
  2792. /**
  2793. * Set options for the range controller
  2794. * @param {Object} options Available options:
  2795. * {Number} min Minimum value for start
  2796. * {Number} max Maximum value for end
  2797. * {Number} zoomMin Set a minimum value for
  2798. * (end - start).
  2799. * {Number} zoomMax Set a maximum value for
  2800. * (end - start).
  2801. */
  2802. Range.prototype.setOptions = function (options) {
  2803. util.extend(this.options, options);
  2804. // re-apply range with new limitations
  2805. if (this.start !== null && this.end !== null) {
  2806. this.setRange(this.start, this.end);
  2807. }
  2808. };
  2809. /**
  2810. * Test whether direction has a valid value
  2811. * @param {String} direction 'horizontal' or 'vertical'
  2812. */
  2813. function validateDirection (direction) {
  2814. if (direction != 'horizontal' && direction != 'vertical') {
  2815. throw new TypeError('Unknown direction "' + direction + '". ' +
  2816. 'Choose "horizontal" or "vertical".');
  2817. }
  2818. }
  2819. /**
  2820. * Set a new start and end range
  2821. * @param {Number} [start]
  2822. * @param {Number} [end]
  2823. */
  2824. Range.prototype.setRange = function(start, end) {
  2825. var changed = this._applyRange(start, end);
  2826. if (changed) {
  2827. var params = {
  2828. start: new Date(this.start),
  2829. end: new Date(this.end)
  2830. };
  2831. this.emit('rangechange', params);
  2832. this.emit('rangechanged', params);
  2833. }
  2834. };
  2835. /**
  2836. * Set a new start and end range. This method is the same as setRange, but
  2837. * does not trigger a range change and range changed event, and it returns
  2838. * true when the range is changed
  2839. * @param {Number} [start]
  2840. * @param {Number} [end]
  2841. * @return {Boolean} changed
  2842. * @private
  2843. */
  2844. Range.prototype._applyRange = function(start, end) {
  2845. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2846. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2847. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2848. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2849. diff;
  2850. // check for valid number
  2851. if (isNaN(newStart) || newStart === null) {
  2852. throw new Error('Invalid start "' + start + '"');
  2853. }
  2854. if (isNaN(newEnd) || newEnd === null) {
  2855. throw new Error('Invalid end "' + end + '"');
  2856. }
  2857. // prevent start < end
  2858. if (newEnd < newStart) {
  2859. newEnd = newStart;
  2860. }
  2861. // prevent start < min
  2862. if (min !== null) {
  2863. if (newStart < min) {
  2864. diff = (min - newStart);
  2865. newStart += diff;
  2866. newEnd += diff;
  2867. // prevent end > max
  2868. if (max != null) {
  2869. if (newEnd > max) {
  2870. newEnd = max;
  2871. }
  2872. }
  2873. }
  2874. }
  2875. // prevent end > max
  2876. if (max !== null) {
  2877. if (newEnd > max) {
  2878. diff = (newEnd - max);
  2879. newStart -= diff;
  2880. newEnd -= diff;
  2881. // prevent start < min
  2882. if (min != null) {
  2883. if (newStart < min) {
  2884. newStart = min;
  2885. }
  2886. }
  2887. }
  2888. }
  2889. // prevent (end-start) < zoomMin
  2890. if (this.options.zoomMin !== null) {
  2891. var zoomMin = parseFloat(this.options.zoomMin);
  2892. if (zoomMin < 0) {
  2893. zoomMin = 0;
  2894. }
  2895. if ((newEnd - newStart) < zoomMin) {
  2896. if ((this.end - this.start) === zoomMin) {
  2897. // ignore this action, we are already zoomed to the minimum
  2898. newStart = this.start;
  2899. newEnd = this.end;
  2900. }
  2901. else {
  2902. // zoom to the minimum
  2903. diff = (zoomMin - (newEnd - newStart));
  2904. newStart -= diff / 2;
  2905. newEnd += diff / 2;
  2906. }
  2907. }
  2908. }
  2909. // prevent (end-start) > zoomMax
  2910. if (this.options.zoomMax !== null) {
  2911. var zoomMax = parseFloat(this.options.zoomMax);
  2912. if (zoomMax < 0) {
  2913. zoomMax = 0;
  2914. }
  2915. if ((newEnd - newStart) > zoomMax) {
  2916. if ((this.end - this.start) === zoomMax) {
  2917. // ignore this action, we are already zoomed to the maximum
  2918. newStart = this.start;
  2919. newEnd = this.end;
  2920. }
  2921. else {
  2922. // zoom to the maximum
  2923. diff = ((newEnd - newStart) - zoomMax);
  2924. newStart += diff / 2;
  2925. newEnd -= diff / 2;
  2926. }
  2927. }
  2928. }
  2929. var changed = (this.start != newStart || this.end != newEnd);
  2930. this.start = newStart;
  2931. this.end = newEnd;
  2932. return changed;
  2933. };
  2934. /**
  2935. * Retrieve the current range.
  2936. * @return {Object} An object with start and end properties
  2937. */
  2938. Range.prototype.getRange = function() {
  2939. return {
  2940. start: this.start,
  2941. end: this.end
  2942. };
  2943. };
  2944. /**
  2945. * Calculate the conversion offset and scale for current range, based on
  2946. * the provided width
  2947. * @param {Number} width
  2948. * @returns {{offset: number, scale: number}} conversion
  2949. */
  2950. Range.prototype.conversion = function (width) {
  2951. return Range.conversion(this.start, this.end, width);
  2952. };
  2953. /**
  2954. * Static method to calculate the conversion offset and scale for a range,
  2955. * based on the provided start, end, and width
  2956. * @param {Number} start
  2957. * @param {Number} end
  2958. * @param {Number} width
  2959. * @returns {{offset: number, scale: number}} conversion
  2960. */
  2961. Range.conversion = function (start, end, width) {
  2962. if (width != 0 && (end - start != 0)) {
  2963. return {
  2964. offset: start,
  2965. scale: width / (end - start)
  2966. }
  2967. }
  2968. else {
  2969. return {
  2970. offset: 0,
  2971. scale: 1
  2972. };
  2973. }
  2974. };
  2975. // global (private) object to store drag params
  2976. var touchParams = {};
  2977. /**
  2978. * Start dragging horizontally or vertically
  2979. * @param {Event} event
  2980. * @private
  2981. */
  2982. Range.prototype._onDragStart = function(event) {
  2983. // refuse to drag when we where pinching to prevent the timeline make a jump
  2984. // when releasing the fingers in opposite order from the touch screen
  2985. if (touchParams.ignore) return;
  2986. // TODO: reckon with option movable
  2987. touchParams.start = this.start;
  2988. touchParams.end = this.end;
  2989. var frame = this.parent.frame;
  2990. if (frame) {
  2991. frame.style.cursor = 'move';
  2992. }
  2993. };
  2994. /**
  2995. * Perform dragging operating.
  2996. * @param {Event} event
  2997. * @private
  2998. */
  2999. Range.prototype._onDrag = function (event) {
  3000. var direction = this.options.direction;
  3001. validateDirection(direction);
  3002. // TODO: reckon with option movable
  3003. // refuse to drag when we where pinching to prevent the timeline make a jump
  3004. // when releasing the fingers in opposite order from the touch screen
  3005. if (touchParams.ignore) return;
  3006. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3007. interval = (touchParams.end - touchParams.start),
  3008. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  3009. diffRange = -delta / width * interval;
  3010. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3011. this.emit('rangechange', {
  3012. start: new Date(this.start),
  3013. end: new Date(this.end)
  3014. });
  3015. };
  3016. /**
  3017. * Stop dragging operating.
  3018. * @param {event} event
  3019. * @private
  3020. */
  3021. Range.prototype._onDragEnd = function (event) {
  3022. // refuse to drag when we where pinching to prevent the timeline make a jump
  3023. // when releasing the fingers in opposite order from the touch screen
  3024. if (touchParams.ignore) return;
  3025. // TODO: reckon with option movable
  3026. if (this.parent.frame) {
  3027. this.parent.frame.style.cursor = 'auto';
  3028. }
  3029. // fire a rangechanged event
  3030. this.emit('rangechanged', {
  3031. start: new Date(this.start),
  3032. end: new Date(this.end)
  3033. });
  3034. };
  3035. /**
  3036. * Event handler for mouse wheel event, used to zoom
  3037. * Code from http://adomas.org/javascript-mouse-wheel/
  3038. * @param {Event} event
  3039. * @private
  3040. */
  3041. Range.prototype._onMouseWheel = function(event) {
  3042. // TODO: reckon with option zoomable
  3043. // retrieve delta
  3044. var delta = 0;
  3045. if (event.wheelDelta) { /* IE/Opera. */
  3046. delta = event.wheelDelta / 120;
  3047. } else if (event.detail) { /* Mozilla case. */
  3048. // In Mozilla, sign of delta is different than in IE.
  3049. // Also, delta is multiple of 3.
  3050. delta = -event.detail / 3;
  3051. }
  3052. // If delta is nonzero, handle it.
  3053. // Basically, delta is now positive if wheel was scrolled up,
  3054. // and negative, if wheel was scrolled down.
  3055. if (delta) {
  3056. // perform the zoom action. Delta is normally 1 or -1
  3057. // adjust a negative delta such that zooming in with delta 0.1
  3058. // equals zooming out with a delta -0.1
  3059. var scale;
  3060. if (delta < 0) {
  3061. scale = 1 - (delta / 5);
  3062. }
  3063. else {
  3064. scale = 1 / (1 + (delta / 5)) ;
  3065. }
  3066. // calculate center, the date to zoom around
  3067. var gesture = util.fakeGesture(this, event),
  3068. pointer = getPointer(gesture.center, this.parent.frame),
  3069. pointerDate = this._pointerToDate(pointer);
  3070. this.zoom(scale, pointerDate);
  3071. }
  3072. // Prevent default actions caused by mouse wheel
  3073. // (else the page and timeline both zoom and scroll)
  3074. event.preventDefault();
  3075. };
  3076. /**
  3077. * Start of a touch gesture
  3078. * @private
  3079. */
  3080. Range.prototype._onTouch = function (event) {
  3081. touchParams.start = this.start;
  3082. touchParams.end = this.end;
  3083. touchParams.ignore = false;
  3084. touchParams.center = null;
  3085. // don't move the range when dragging a selected event
  3086. // TODO: it's not so neat to have to know about the state of the ItemSet
  3087. var item = ItemSet.itemFromTarget(event);
  3088. if (item && item.selected && this.options.editable) {
  3089. touchParams.ignore = true;
  3090. }
  3091. };
  3092. /**
  3093. * On start of a hold gesture
  3094. * @private
  3095. */
  3096. Range.prototype._onHold = function () {
  3097. touchParams.ignore = true;
  3098. };
  3099. /**
  3100. * Handle pinch event
  3101. * @param {Event} event
  3102. * @private
  3103. */
  3104. Range.prototype._onPinch = function (event) {
  3105. var direction = this.options.direction;
  3106. touchParams.ignore = true;
  3107. // TODO: reckon with option zoomable
  3108. if (event.gesture.touches.length > 1) {
  3109. if (!touchParams.center) {
  3110. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  3111. }
  3112. var scale = 1 / event.gesture.scale,
  3113. initDate = this._pointerToDate(touchParams.center),
  3114. center = getPointer(event.gesture.center, this.parent.frame),
  3115. date = this._pointerToDate(this.parent, center),
  3116. delta = date - initDate; // TODO: utilize delta
  3117. // calculate new start and end
  3118. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3119. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3120. // apply new range
  3121. this.setRange(newStart, newEnd);
  3122. }
  3123. };
  3124. /**
  3125. * Helper function to calculate the center date for zooming
  3126. * @param {{x: Number, y: Number}} pointer
  3127. * @return {number} date
  3128. * @private
  3129. */
  3130. Range.prototype._pointerToDate = function (pointer) {
  3131. var conversion;
  3132. var direction = this.options.direction;
  3133. validateDirection(direction);
  3134. if (direction == 'horizontal') {
  3135. var width = this.parent.width;
  3136. conversion = this.conversion(width);
  3137. return pointer.x / conversion.scale + conversion.offset;
  3138. }
  3139. else {
  3140. var height = this.parent.height;
  3141. conversion = this.conversion(height);
  3142. return pointer.y / conversion.scale + conversion.offset;
  3143. }
  3144. };
  3145. /**
  3146. * Get the pointer location relative to the location of the dom element
  3147. * @param {{pageX: Number, pageY: Number}} touch
  3148. * @param {Element} element HTML DOM element
  3149. * @return {{x: Number, y: Number}} pointer
  3150. * @private
  3151. */
  3152. function getPointer (touch, element) {
  3153. return {
  3154. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3155. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3156. };
  3157. }
  3158. /**
  3159. * Zoom the range the given scale in or out. Start and end date will
  3160. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3161. * date around which to zoom.
  3162. * For example, try scale = 0.9 or 1.1
  3163. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3164. * values below 1 will zoom in.
  3165. * @param {Number} [center] Value representing a date around which will
  3166. * be zoomed.
  3167. */
  3168. Range.prototype.zoom = function(scale, center) {
  3169. // if centerDate is not provided, take it half between start Date and end Date
  3170. if (center == null) {
  3171. center = (this.start + this.end) / 2;
  3172. }
  3173. // calculate new start and end
  3174. var newStart = center + (this.start - center) * scale;
  3175. var newEnd = center + (this.end - center) * scale;
  3176. this.setRange(newStart, newEnd);
  3177. };
  3178. /**
  3179. * Move the range with a given delta to the left or right. Start and end
  3180. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3181. * @param {Number} delta Moving amount. Positive value will move right,
  3182. * negative value will move left
  3183. */
  3184. Range.prototype.move = function(delta) {
  3185. // zoom start Date and end Date relative to the centerDate
  3186. var diff = (this.end - this.start);
  3187. // apply new values
  3188. var newStart = this.start + diff * delta;
  3189. var newEnd = this.end + diff * delta;
  3190. // TODO: reckon with min and max range
  3191. this.start = newStart;
  3192. this.end = newEnd;
  3193. };
  3194. /**
  3195. * Move the range to a new center point
  3196. * @param {Number} moveTo New center point of the range
  3197. */
  3198. Range.prototype.moveTo = function(moveTo) {
  3199. var center = (this.start + this.end) / 2;
  3200. var diff = center - moveTo;
  3201. // calculate new start and end
  3202. var newStart = this.start - diff;
  3203. var newEnd = this.end - diff;
  3204. this.setRange(newStart, newEnd);
  3205. };
  3206. /**
  3207. * Prototype for visual components
  3208. */
  3209. function Component () {
  3210. this.id = null;
  3211. this.parent = null;
  3212. this.childs = null;
  3213. this.options = null;
  3214. this.top = 0;
  3215. this.left = 0;
  3216. this.width = 0;
  3217. this.height = 0;
  3218. }
  3219. // Turn the Component into an event emitter
  3220. Emitter(Component.prototype);
  3221. /**
  3222. * Set parameters for the frame. Parameters will be merged in current parameter
  3223. * set.
  3224. * @param {Object} options Available parameters:
  3225. * {String | function} [className]
  3226. * {String | Number | function} [left]
  3227. * {String | Number | function} [top]
  3228. * {String | Number | function} [width]
  3229. * {String | Number | function} [height]
  3230. */
  3231. Component.prototype.setOptions = function setOptions(options) {
  3232. if (options) {
  3233. util.extend(this.options, options);
  3234. this.repaint();
  3235. }
  3236. };
  3237. /**
  3238. * Get an option value by name
  3239. * The function will first check this.options object, and else will check
  3240. * this.defaultOptions.
  3241. * @param {String} name
  3242. * @return {*} value
  3243. */
  3244. Component.prototype.getOption = function getOption(name) {
  3245. var value;
  3246. if (this.options) {
  3247. value = this.options[name];
  3248. }
  3249. if (value === undefined && this.defaultOptions) {
  3250. value = this.defaultOptions[name];
  3251. }
  3252. return value;
  3253. };
  3254. /**
  3255. * Get the frame element of the component, the outer HTML DOM element.
  3256. * @returns {HTMLElement | null} frame
  3257. */
  3258. Component.prototype.getFrame = function getFrame() {
  3259. // should be implemented by the component
  3260. return null;
  3261. };
  3262. /**
  3263. * Repaint the component
  3264. * @return {boolean} Returns true if the component is resized
  3265. */
  3266. Component.prototype.repaint = function repaint() {
  3267. // should be implemented by the component
  3268. return false;
  3269. };
  3270. /**
  3271. * Test whether the component is resized since the last time _isResized() was
  3272. * called.
  3273. * @return {Boolean} Returns true if the component is resized
  3274. * @private
  3275. */
  3276. Component.prototype._isResized = function _isResized() {
  3277. var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
  3278. this._previousWidth = this.width;
  3279. this._previousHeight = this.height;
  3280. return resized;
  3281. };
  3282. /**
  3283. * A panel can contain components
  3284. * @param {Object} [options] Available parameters:
  3285. * {String | Number | function} [left]
  3286. * {String | Number | function} [top]
  3287. * {String | Number | function} [width]
  3288. * {String | Number | function} [height]
  3289. * {String | function} [className]
  3290. * @constructor Panel
  3291. * @extends Component
  3292. */
  3293. function Panel(options) {
  3294. this.id = util.randomUUID();
  3295. this.parent = null;
  3296. this.childs = [];
  3297. this.options = options || {};
  3298. // create frame
  3299. this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
  3300. }
  3301. Panel.prototype = new Component();
  3302. /**
  3303. * Set options. Will extend the current options.
  3304. * @param {Object} [options] Available parameters:
  3305. * {String | function} [className]
  3306. * {String | Number | function} [left]
  3307. * {String | Number | function} [top]
  3308. * {String | Number | function} [width]
  3309. * {String | Number | function} [height]
  3310. */
  3311. Panel.prototype.setOptions = Component.prototype.setOptions;
  3312. /**
  3313. * Get the outer frame of the panel
  3314. * @returns {HTMLElement} frame
  3315. */
  3316. Panel.prototype.getFrame = function () {
  3317. return this.frame;
  3318. };
  3319. /**
  3320. * Append a child to the panel
  3321. * @param {Component} child
  3322. */
  3323. Panel.prototype.appendChild = function (child) {
  3324. this.childs.push(child);
  3325. child.parent = this;
  3326. // attach to the DOM
  3327. var frame = child.getFrame();
  3328. if (frame) {
  3329. if (frame.parentNode) {
  3330. frame.parentNode.removeChild(frame);
  3331. }
  3332. this.frame.appendChild(frame);
  3333. }
  3334. };
  3335. /**
  3336. * Insert a child to the panel
  3337. * @param {Component} child
  3338. * @param {Component} beforeChild
  3339. */
  3340. Panel.prototype.insertBefore = function (child, beforeChild) {
  3341. var index = this.childs.indexOf(beforeChild);
  3342. if (index != -1) {
  3343. this.childs.splice(index, 0, child);
  3344. child.parent = this;
  3345. // attach to the DOM
  3346. var frame = child.getFrame();
  3347. if (frame) {
  3348. if (frame.parentNode) {
  3349. frame.parentNode.removeChild(frame);
  3350. }
  3351. var beforeFrame = beforeChild.getFrame();
  3352. if (beforeFrame) {
  3353. this.frame.insertBefore(frame, beforeFrame);
  3354. }
  3355. else {
  3356. this.frame.appendChild(frame);
  3357. }
  3358. }
  3359. }
  3360. };
  3361. /**
  3362. * Remove a child from the panel
  3363. * @param {Component} child
  3364. */
  3365. Panel.prototype.removeChild = function (child) {
  3366. var index = this.childs.indexOf(child);
  3367. if (index != -1) {
  3368. this.childs.splice(index, 1);
  3369. child.parent = null;
  3370. // remove from the DOM
  3371. var frame = child.getFrame();
  3372. if (frame && frame.parentNode) {
  3373. this.frame.removeChild(frame);
  3374. }
  3375. }
  3376. };
  3377. /**
  3378. * Test whether the panel contains given child
  3379. * @param {Component} child
  3380. */
  3381. Panel.prototype.hasChild = function (child) {
  3382. var index = this.childs.indexOf(child);
  3383. return (index != -1);
  3384. };
  3385. /**
  3386. * Repaint the component
  3387. * @return {boolean} Returns true if the component was resized since previous repaint
  3388. */
  3389. Panel.prototype.repaint = function () {
  3390. var asString = util.option.asString,
  3391. options = this.options,
  3392. frame = this.getFrame();
  3393. // update className
  3394. frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
  3395. // repaint the child components
  3396. var childsResized = this._repaintChilds();
  3397. // update frame size
  3398. this._updateSize();
  3399. return this._isResized() || childsResized;
  3400. };
  3401. /**
  3402. * Repaint all childs of the panel
  3403. * @return {boolean} Returns true if the component is resized
  3404. * @private
  3405. */
  3406. Panel.prototype._repaintChilds = function () {
  3407. var resized = false;
  3408. for (var i = 0, ii = this.childs.length; i < ii; i++) {
  3409. resized = this.childs[i].repaint() || resized;
  3410. }
  3411. return resized;
  3412. };
  3413. /**
  3414. * Apply the size from options to the panel, and recalculate it's actual size.
  3415. * @private
  3416. */
  3417. Panel.prototype._updateSize = function () {
  3418. // apply size
  3419. this.frame.style.top = util.option.asSize(this.options.top);
  3420. this.frame.style.bottom = util.option.asSize(this.options.bottom);
  3421. this.frame.style.left = util.option.asSize(this.options.left);
  3422. this.frame.style.right = util.option.asSize(this.options.right);
  3423. this.frame.style.width = util.option.asSize(this.options.width, '100%');
  3424. this.frame.style.height = util.option.asSize(this.options.height, '');
  3425. // get actual size
  3426. this.top = this.frame.offsetTop;
  3427. this.left = this.frame.offsetLeft;
  3428. this.width = this.frame.offsetWidth;
  3429. this.height = this.frame.offsetHeight;
  3430. };
  3431. /**
  3432. * A root panel can hold components. The root panel must be initialized with
  3433. * a DOM element as container.
  3434. * @param {HTMLElement} container
  3435. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3436. * @constructor RootPanel
  3437. * @extends Panel
  3438. */
  3439. function RootPanel(container, options) {
  3440. this.id = util.randomUUID();
  3441. this.container = container;
  3442. this.options = options || {};
  3443. this.defaultOptions = {
  3444. autoResize: true
  3445. };
  3446. // create the HTML DOM
  3447. this._create();
  3448. // attach the root panel to the provided container
  3449. if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
  3450. this.container.appendChild(this.getFrame());
  3451. this._initWatch();
  3452. }
  3453. RootPanel.prototype = new Panel();
  3454. /**
  3455. * Create the HTML DOM for the root panel
  3456. */
  3457. RootPanel.prototype._create = function _create() {
  3458. // create frame
  3459. this.frame = document.createElement('div');
  3460. // create event listeners for all interesting events, these events will be
  3461. // emitted via emitter
  3462. this.hammer = Hammer(this.frame, {
  3463. prevent_default: true
  3464. });
  3465. this.listeners = {};
  3466. var me = this;
  3467. var events = [
  3468. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3469. 'dragstart', 'drag', 'dragend',
  3470. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3471. ];
  3472. events.forEach(function (event) {
  3473. var listener = function () {
  3474. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3475. me.emit.apply(me, args);
  3476. };
  3477. me.hammer.on(event, listener);
  3478. me.listeners[event] = listener;
  3479. });
  3480. };
  3481. /**
  3482. * Set options. Will extend the current options.
  3483. * @param {Object} [options] Available parameters:
  3484. * {String | function} [className]
  3485. * {String | Number | function} [left]
  3486. * {String | Number | function} [top]
  3487. * {String | Number | function} [width]
  3488. * {String | Number | function} [height]
  3489. * {Boolean | function} [autoResize]
  3490. */
  3491. RootPanel.prototype.setOptions = function setOptions(options) {
  3492. if (options) {
  3493. util.extend(this.options, options);
  3494. this.repaint();
  3495. this._initWatch();
  3496. }
  3497. };
  3498. /**
  3499. * Get the frame of the root panel
  3500. */
  3501. RootPanel.prototype.getFrame = function getFrame() {
  3502. return this.frame;
  3503. };
  3504. /**
  3505. * Repaint the root panel
  3506. */
  3507. RootPanel.prototype.repaint = function repaint() {
  3508. // update class name
  3509. var options = this.options;
  3510. var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
  3511. if (options.className) className += ' ' + util.option.asString(className);
  3512. this.frame.className = className;
  3513. // repaint the child components
  3514. var childsResized = this._repaintChilds();
  3515. // update frame size
  3516. this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
  3517. this._updateSize();
  3518. // if the root panel or any of its childs is resized, repaint again,
  3519. // as other components may need to be resized accordingly
  3520. var resized = this._isResized() || childsResized;
  3521. if (resized) {
  3522. setTimeout(this.repaint.bind(this), 0);
  3523. }
  3524. };
  3525. /**
  3526. * Initialize watching when option autoResize is true
  3527. * @private
  3528. */
  3529. RootPanel.prototype._initWatch = function _initWatch() {
  3530. var autoResize = this.getOption('autoResize');
  3531. if (autoResize) {
  3532. this._watch();
  3533. }
  3534. else {
  3535. this._unwatch();
  3536. }
  3537. };
  3538. /**
  3539. * Watch for changes in the size of the frame. On resize, the Panel will
  3540. * automatically redraw itself.
  3541. * @private
  3542. */
  3543. RootPanel.prototype._watch = function _watch() {
  3544. var me = this;
  3545. this._unwatch();
  3546. var checkSize = function checkSize() {
  3547. var autoResize = me.getOption('autoResize');
  3548. if (!autoResize) {
  3549. // stop watching when the option autoResize is changed to false
  3550. me._unwatch();
  3551. return;
  3552. }
  3553. if (me.frame) {
  3554. // check whether the frame is resized
  3555. if ((me.frame.clientWidth != me.lastWidth) ||
  3556. (me.frame.clientHeight != me.lastHeight)) {
  3557. me.lastWidth = me.frame.clientWidth;
  3558. me.lastHeight = me.frame.clientHeight;
  3559. me.repaint();
  3560. // TODO: emit a resize event instead?
  3561. }
  3562. }
  3563. };
  3564. // TODO: automatically cleanup the event listener when the frame is deleted
  3565. util.addEventListener(window, 'resize', checkSize);
  3566. this.watchTimer = setInterval(checkSize, 1000);
  3567. };
  3568. /**
  3569. * Stop watching for a resize of the frame.
  3570. * @private
  3571. */
  3572. RootPanel.prototype._unwatch = function _unwatch() {
  3573. if (this.watchTimer) {
  3574. clearInterval(this.watchTimer);
  3575. this.watchTimer = undefined;
  3576. }
  3577. // TODO: remove event listener on window.resize
  3578. };
  3579. /**
  3580. * A horizontal time axis
  3581. * @param {Object} [options] See TimeAxis.setOptions for the available
  3582. * options.
  3583. * @constructor TimeAxis
  3584. * @extends Component
  3585. */
  3586. function TimeAxis (options) {
  3587. this.id = util.randomUUID();
  3588. this.dom = {
  3589. majorLines: [],
  3590. majorTexts: [],
  3591. minorLines: [],
  3592. minorTexts: [],
  3593. redundant: {
  3594. majorLines: [],
  3595. majorTexts: [],
  3596. minorLines: [],
  3597. minorTexts: []
  3598. }
  3599. };
  3600. this.props = {
  3601. range: {
  3602. start: 0,
  3603. end: 0,
  3604. minimumStep: 0
  3605. },
  3606. lineTop: 0
  3607. };
  3608. this.options = options || {};
  3609. this.defaultOptions = {
  3610. orientation: 'bottom', // supported: 'top', 'bottom'
  3611. // TODO: implement timeaxis orientations 'left' and 'right'
  3612. showMinorLabels: true,
  3613. showMajorLabels: true
  3614. };
  3615. this.range = null;
  3616. // create the HTML DOM
  3617. this._create();
  3618. }
  3619. TimeAxis.prototype = new Component();
  3620. // TODO: comment options
  3621. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3622. /**
  3623. * Create the HTML DOM for the TimeAxis
  3624. */
  3625. TimeAxis.prototype._create = function _create() {
  3626. this.frame = document.createElement('div');
  3627. };
  3628. /**
  3629. * Set a range (start and end)
  3630. * @param {Range | Object} range A Range or an object containing start and end.
  3631. */
  3632. TimeAxis.prototype.setRange = function (range) {
  3633. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3634. throw new TypeError('Range must be an instance of Range, ' +
  3635. 'or an object containing start and end.');
  3636. }
  3637. this.range = range;
  3638. };
  3639. /**
  3640. * Get the outer frame of the time axis
  3641. * @return {HTMLElement} frame
  3642. */
  3643. TimeAxis.prototype.getFrame = function getFrame() {
  3644. return this.frame;
  3645. };
  3646. /**
  3647. * Repaint the component
  3648. * @return {boolean} Returns true if the component is resized
  3649. */
  3650. TimeAxis.prototype.repaint = function () {
  3651. var asSize = util.option.asSize,
  3652. options = this.options,
  3653. props = this.props,
  3654. frame = this.frame;
  3655. // update classname
  3656. frame.className = 'timeaxis'; // TODO: add className from options if defined
  3657. var parent = frame.parentNode;
  3658. if (parent) {
  3659. // calculate character width and height
  3660. this._calculateCharSize();
  3661. // TODO: recalculate sizes only needed when parent is resized or options is changed
  3662. var orientation = this.getOption('orientation'),
  3663. showMinorLabels = this.getOption('showMinorLabels'),
  3664. showMajorLabels = this.getOption('showMajorLabels');
  3665. // determine the width and height of the elemens for the axis
  3666. var parentHeight = this.parent.height;
  3667. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3668. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3669. this.height = props.minorLabelHeight + props.majorLabelHeight;
  3670. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  3671. props.minorLineHeight = parentHeight + props.minorLabelHeight;
  3672. props.minorLineWidth = 1; // TODO: really calculate width
  3673. props.majorLineHeight = parentHeight + this.height;
  3674. props.majorLineWidth = 1; // TODO: really calculate width
  3675. // take frame offline while updating (is almost twice as fast)
  3676. var beforeChild = frame.nextSibling;
  3677. parent.removeChild(frame);
  3678. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  3679. if (orientation == 'top') {
  3680. frame.style.top = '0';
  3681. frame.style.left = '0';
  3682. frame.style.bottom = '';
  3683. frame.style.width = asSize(options.width, '100%');
  3684. frame.style.height = this.height + 'px';
  3685. }
  3686. else { // bottom
  3687. frame.style.top = '';
  3688. frame.style.bottom = '0';
  3689. frame.style.left = '0';
  3690. frame.style.width = asSize(options.width, '100%');
  3691. frame.style.height = this.height + 'px';
  3692. }
  3693. this._repaintLabels();
  3694. this._repaintLine();
  3695. // put frame online again
  3696. if (beforeChild) {
  3697. parent.insertBefore(frame, beforeChild);
  3698. }
  3699. else {
  3700. parent.appendChild(frame)
  3701. }
  3702. }
  3703. return this._isResized();
  3704. };
  3705. /**
  3706. * Repaint major and minor text labels and vertical grid lines
  3707. * @private
  3708. */
  3709. TimeAxis.prototype._repaintLabels = function () {
  3710. var orientation = this.getOption('orientation');
  3711. // calculate range and step
  3712. var start = util.convert(this.range.start, 'Number'),
  3713. end = util.convert(this.range.end, 'Number'),
  3714. minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 5).valueOf()
  3715. -this.options.toTime(0).valueOf();
  3716. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  3717. this.step = step;
  3718. // Move all DOM elements to a "redundant" list, where they
  3719. // can be picked for re-use, and clear the lists with lines and texts.
  3720. // At the end of the function _repaintLabels, left over elements will be cleaned up
  3721. var dom = this.dom;
  3722. dom.redundant.majorLines = dom.majorLines;
  3723. dom.redundant.majorTexts = dom.majorTexts;
  3724. dom.redundant.minorLines = dom.minorLines;
  3725. dom.redundant.minorTexts = dom.minorTexts;
  3726. dom.majorLines = [];
  3727. dom.majorTexts = [];
  3728. dom.minorLines = [];
  3729. dom.minorTexts = [];
  3730. step.first();
  3731. var xFirstMajorLabel = undefined;
  3732. var max = 0;
  3733. while (step.hasNext() && max < 1000) {
  3734. max++;
  3735. var cur = step.getCurrent(),
  3736. x = this.options.toScreen(cur),
  3737. isMajor = step.isMajor();
  3738. // TODO: lines must have a width, such that we can create css backgrounds
  3739. if (this.getOption('showMinorLabels')) {
  3740. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  3741. }
  3742. if (isMajor && this.getOption('showMajorLabels')) {
  3743. if (x > 0) {
  3744. if (xFirstMajorLabel == undefined) {
  3745. xFirstMajorLabel = x;
  3746. }
  3747. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  3748. }
  3749. this._repaintMajorLine(x, orientation);
  3750. }
  3751. else {
  3752. this._repaintMinorLine(x, orientation);
  3753. }
  3754. step.next();
  3755. }
  3756. // create a major label on the left when needed
  3757. if (this.getOption('showMajorLabels')) {
  3758. var leftTime = this.options.toTime(0),
  3759. leftText = step.getLabelMajor(leftTime),
  3760. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  3761. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3762. this._repaintMajorText(0, leftText, orientation);
  3763. }
  3764. }
  3765. // Cleanup leftover DOM elements from the redundant list
  3766. util.forEach(this.dom.redundant, function (arr) {
  3767. while (arr.length) {
  3768. var elem = arr.pop();
  3769. if (elem && elem.parentNode) {
  3770. elem.parentNode.removeChild(elem);
  3771. }
  3772. }
  3773. });
  3774. };
  3775. /**
  3776. * Create a minor label for the axis at position x
  3777. * @param {Number} x
  3778. * @param {String} text
  3779. * @param {String} orientation "top" or "bottom" (default)
  3780. * @private
  3781. */
  3782. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  3783. // reuse redundant label
  3784. var label = this.dom.redundant.minorTexts.shift();
  3785. if (!label) {
  3786. // create new label
  3787. var content = document.createTextNode('');
  3788. label = document.createElement('div');
  3789. label.appendChild(content);
  3790. label.className = 'text minor';
  3791. this.frame.appendChild(label);
  3792. }
  3793. this.dom.minorTexts.push(label);
  3794. label.childNodes[0].nodeValue = text;
  3795. if (orientation == 'top') {
  3796. label.style.top = this.props.majorLabelHeight + 'px';
  3797. label.style.bottom = '';
  3798. }
  3799. else {
  3800. label.style.top = '';
  3801. label.style.bottom = this.props.majorLabelHeight + 'px';
  3802. }
  3803. label.style.left = x + 'px';
  3804. //label.title = title; // TODO: this is a heavy operation
  3805. };
  3806. /**
  3807. * Create a Major label for the axis at position x
  3808. * @param {Number} x
  3809. * @param {String} text
  3810. * @param {String} orientation "top" or "bottom" (default)
  3811. * @private
  3812. */
  3813. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  3814. // reuse redundant label
  3815. var label = this.dom.redundant.majorTexts.shift();
  3816. if (!label) {
  3817. // create label
  3818. var content = document.createTextNode(text);
  3819. label = document.createElement('div');
  3820. label.className = 'text major';
  3821. label.appendChild(content);
  3822. this.frame.appendChild(label);
  3823. }
  3824. this.dom.majorTexts.push(label);
  3825. label.childNodes[0].nodeValue = text;
  3826. //label.title = title; // TODO: this is a heavy operation
  3827. if (orientation == 'top') {
  3828. label.style.top = '0px';
  3829. label.style.bottom = '';
  3830. }
  3831. else {
  3832. label.style.top = '';
  3833. label.style.bottom = '0px';
  3834. }
  3835. label.style.left = x + 'px';
  3836. };
  3837. /**
  3838. * Create a minor line for the axis at position x
  3839. * @param {Number} x
  3840. * @param {String} orientation "top" or "bottom" (default)
  3841. * @private
  3842. */
  3843. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  3844. // reuse redundant line
  3845. var line = this.dom.redundant.minorLines.shift();
  3846. if (!line) {
  3847. // create vertical line
  3848. line = document.createElement('div');
  3849. line.className = 'grid vertical minor';
  3850. this.frame.appendChild(line);
  3851. }
  3852. this.dom.minorLines.push(line);
  3853. var props = this.props;
  3854. if (orientation == 'top') {
  3855. line.style.top = this.props.majorLabelHeight + 'px';
  3856. line.style.bottom = '';
  3857. }
  3858. else {
  3859. line.style.top = '';
  3860. line.style.bottom = this.props.majorLabelHeight + 'px';
  3861. }
  3862. line.style.height = props.minorLineHeight + 'px';
  3863. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3864. };
  3865. /**
  3866. * Create a Major line for the axis at position x
  3867. * @param {Number} x
  3868. * @param {String} orientation "top" or "bottom" (default)
  3869. * @private
  3870. */
  3871. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  3872. // reuse redundant line
  3873. var line = this.dom.redundant.majorLines.shift();
  3874. if (!line) {
  3875. // create vertical line
  3876. line = document.createElement('DIV');
  3877. line.className = 'grid vertical major';
  3878. this.frame.appendChild(line);
  3879. }
  3880. this.dom.majorLines.push(line);
  3881. var props = this.props;
  3882. if (orientation == 'top') {
  3883. line.style.top = '0px';
  3884. line.style.bottom = '';
  3885. }
  3886. else {
  3887. line.style.top = '';
  3888. line.style.bottom = '0px';
  3889. }
  3890. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3891. line.style.height = props.majorLineHeight + 'px';
  3892. };
  3893. /**
  3894. * Repaint the horizontal line for the axis
  3895. * @private
  3896. */
  3897. TimeAxis.prototype._repaintLine = function() {
  3898. var line = this.dom.line,
  3899. frame = this.frame,
  3900. orientation = this.getOption('orientation');
  3901. // line before all axis elements
  3902. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3903. if (line) {
  3904. // put this line at the end of all childs
  3905. frame.removeChild(line);
  3906. frame.appendChild(line);
  3907. }
  3908. else {
  3909. // create the axis line
  3910. line = document.createElement('div');
  3911. line.className = 'grid horizontal major';
  3912. frame.appendChild(line);
  3913. this.dom.line = line;
  3914. }
  3915. if (orientation == 'top') {
  3916. line.style.top = this.height + 'px';
  3917. line.style.bottom = '';
  3918. }
  3919. else {
  3920. line.style.top = '';
  3921. line.style.bottom = this.height + 'px';
  3922. }
  3923. }
  3924. else {
  3925. if (line && line.parentNode) {
  3926. line.parentNode.removeChild(line);
  3927. delete this.dom.line;
  3928. }
  3929. }
  3930. };
  3931. /**
  3932. * Determine the size of text on the axis (both major and minor axis).
  3933. * The size is calculated only once and then cached in this.props.
  3934. * @private
  3935. */
  3936. TimeAxis.prototype._calculateCharSize = function () {
  3937. // determine the char width and height on the minor axis
  3938. if (!('minorCharHeight' in this.props)) {
  3939. var textMinor = document.createTextNode('0');
  3940. var measureCharMinor = document.createElement('DIV');
  3941. measureCharMinor.className = 'text minor measure';
  3942. measureCharMinor.appendChild(textMinor);
  3943. this.frame.appendChild(measureCharMinor);
  3944. this.props.minorCharHeight = measureCharMinor.clientHeight;
  3945. this.props.minorCharWidth = measureCharMinor.clientWidth;
  3946. this.frame.removeChild(measureCharMinor);
  3947. }
  3948. if (!('majorCharHeight' in this.props)) {
  3949. var textMajor = document.createTextNode('0');
  3950. var measureCharMajor = document.createElement('DIV');
  3951. measureCharMajor.className = 'text major measure';
  3952. measureCharMajor.appendChild(textMajor);
  3953. this.frame.appendChild(measureCharMajor);
  3954. this.props.majorCharHeight = measureCharMajor.clientHeight;
  3955. this.props.majorCharWidth = measureCharMajor.clientWidth;
  3956. this.frame.removeChild(measureCharMajor);
  3957. }
  3958. };
  3959. /**
  3960. * Snap a date to a rounded value.
  3961. * The snap intervals are dependent on the current scale and step.
  3962. * @param {Date} date the date to be snapped.
  3963. * @return {Date} snappedDate
  3964. */
  3965. TimeAxis.prototype.snap = function snap (date) {
  3966. return this.step.snap(date);
  3967. };
  3968. /**
  3969. * A current time bar
  3970. * @param {Range} range
  3971. * @param {Object} [options] Available parameters:
  3972. * {Boolean} [showCurrentTime]
  3973. * @constructor CurrentTime
  3974. * @extends Component
  3975. */
  3976. function CurrentTime (range, options) {
  3977. this.id = util.randomUUID();
  3978. this.range = range;
  3979. this.options = options || {};
  3980. this.defaultOptions = {
  3981. showCurrentTime: false
  3982. };
  3983. this._create();
  3984. }
  3985. CurrentTime.prototype = new Component();
  3986. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  3987. /**
  3988. * Create the HTML DOM for the current time bar
  3989. * @private
  3990. */
  3991. CurrentTime.prototype._create = function _create () {
  3992. var bar = document.createElement('div');
  3993. bar.className = 'currenttime';
  3994. bar.style.position = 'absolute';
  3995. bar.style.top = '0px';
  3996. bar.style.height = '100%';
  3997. this.bar = bar;
  3998. };
  3999. /**
  4000. * Get the frame element of the current time bar
  4001. * @returns {HTMLElement} frame
  4002. */
  4003. CurrentTime.prototype.getFrame = function getFrame() {
  4004. return this.bar;
  4005. };
  4006. /**
  4007. * Repaint the component
  4008. * @return {boolean} Returns true if the component is resized
  4009. */
  4010. CurrentTime.prototype.repaint = function repaint() {
  4011. var parent = this.parent;
  4012. var now = new Date();
  4013. var x = this.options.toScreen(now);
  4014. this.bar.style.left = x + 'px';
  4015. this.bar.title = 'Current time: ' + now;
  4016. return false;
  4017. };
  4018. /**
  4019. * Start auto refreshing the current time bar
  4020. */
  4021. CurrentTime.prototype.start = function start() {
  4022. var me = this;
  4023. function update () {
  4024. me.stop();
  4025. // determine interval to refresh
  4026. var scale = me.range.conversion(me.parent.width).scale;
  4027. var interval = 1 / scale / 10;
  4028. if (interval < 30) interval = 30;
  4029. if (interval > 1000) interval = 1000;
  4030. me.repaint();
  4031. // start a timer to adjust for the new time
  4032. me.currentTimeTimer = setTimeout(update, interval);
  4033. }
  4034. update();
  4035. };
  4036. /**
  4037. * Stop auto refreshing the current time bar
  4038. */
  4039. CurrentTime.prototype.stop = function stop() {
  4040. if (this.currentTimeTimer !== undefined) {
  4041. clearTimeout(this.currentTimeTimer);
  4042. delete this.currentTimeTimer;
  4043. }
  4044. };
  4045. /**
  4046. * A custom time bar
  4047. * @param {Object} [options] Available parameters:
  4048. * {Boolean} [showCustomTime]
  4049. * @constructor CustomTime
  4050. * @extends Component
  4051. */
  4052. function CustomTime (options) {
  4053. this.id = util.randomUUID();
  4054. this.options = options || {};
  4055. this.defaultOptions = {
  4056. showCustomTime: false
  4057. };
  4058. this.customTime = new Date();
  4059. this.eventParams = {}; // stores state parameters while dragging the bar
  4060. // create the DOM
  4061. this._create();
  4062. }
  4063. CustomTime.prototype = new Component();
  4064. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4065. /**
  4066. * Create the DOM for the custom time
  4067. * @private
  4068. */
  4069. CustomTime.prototype._create = function _create () {
  4070. var bar = document.createElement('div');
  4071. bar.className = 'customtime';
  4072. bar.style.position = 'absolute';
  4073. bar.style.top = '0px';
  4074. bar.style.height = '100%';
  4075. this.bar = bar;
  4076. var drag = document.createElement('div');
  4077. drag.style.position = 'relative';
  4078. drag.style.top = '0px';
  4079. drag.style.left = '-10px';
  4080. drag.style.height = '100%';
  4081. drag.style.width = '20px';
  4082. bar.appendChild(drag);
  4083. // attach event listeners
  4084. this.hammer = Hammer(bar, {
  4085. prevent_default: true
  4086. });
  4087. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4088. this.hammer.on('drag', this._onDrag.bind(this));
  4089. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4090. };
  4091. /**
  4092. * Get the frame element of the custom time bar
  4093. * @returns {HTMLElement} frame
  4094. */
  4095. CustomTime.prototype.getFrame = function getFrame() {
  4096. return this.bar;
  4097. };
  4098. /**
  4099. * Repaint the component
  4100. * @return {boolean} Returns true if the component is resized
  4101. */
  4102. CustomTime.prototype.repaint = function () {
  4103. var x = this.options.toScreen(this.customTime);
  4104. this.bar.style.left = x + 'px';
  4105. this.bar.title = 'Time: ' + this.customTime;
  4106. return false;
  4107. };
  4108. /**
  4109. * Set custom time.
  4110. * @param {Date} time
  4111. */
  4112. CustomTime.prototype.setCustomTime = function(time) {
  4113. this.customTime = new Date(time.valueOf());
  4114. this.repaint();
  4115. };
  4116. /**
  4117. * Retrieve the current custom time.
  4118. * @return {Date} customTime
  4119. */
  4120. CustomTime.prototype.getCustomTime = function() {
  4121. return new Date(this.customTime.valueOf());
  4122. };
  4123. /**
  4124. * Start moving horizontally
  4125. * @param {Event} event
  4126. * @private
  4127. */
  4128. CustomTime.prototype._onDragStart = function(event) {
  4129. this.eventParams.dragging = true;
  4130. this.eventParams.customTime = this.customTime;
  4131. event.stopPropagation();
  4132. event.preventDefault();
  4133. };
  4134. /**
  4135. * Perform moving operating.
  4136. * @param {Event} event
  4137. * @private
  4138. */
  4139. CustomTime.prototype._onDrag = function (event) {
  4140. if (!this.eventParams.dragging) return;
  4141. var deltaX = event.gesture.deltaX,
  4142. x = this.options.toScreen(this.eventParams.customTime) + deltaX,
  4143. time = this.options.toTime(x);
  4144. this.setCustomTime(time);
  4145. // fire a timechange event
  4146. this.emit('timechange', {
  4147. time: new Date(this.customTime.valueOf())
  4148. });
  4149. event.stopPropagation();
  4150. event.preventDefault();
  4151. };
  4152. /**
  4153. * Stop moving operating.
  4154. * @param {event} event
  4155. * @private
  4156. */
  4157. CustomTime.prototype._onDragEnd = function (event) {
  4158. if (!this.eventParams.dragging) return;
  4159. // fire a timechanged event
  4160. this.emit('timechanged', {
  4161. time: new Date(this.customTime.valueOf())
  4162. });
  4163. event.stopPropagation();
  4164. event.preventDefault();
  4165. };
  4166. /**
  4167. * An ItemSet holds a set of items and ranges which can be displayed in a
  4168. * range. The width is determined by the parent of the ItemSet, and the height
  4169. * is determined by the size of the items.
  4170. * @param {Panel} backgroundPanel Panel which can be used to display the
  4171. * vertical lines of box items.
  4172. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  4173. * can be displayed.
  4174. * @param {Object} [options] See ItemSet.setOptions for the available options.
  4175. * @constructor ItemSet
  4176. * @extends Panel
  4177. */
  4178. function ItemSet(backgroundPanel, axisPanel, options) {
  4179. this.id = util.randomUUID();
  4180. // one options object is shared by this itemset and all its items
  4181. this.options = options || {};
  4182. this.backgroundPanel = backgroundPanel;
  4183. this.axisPanel = axisPanel;
  4184. this.itemOptions = Object.create(this.options);
  4185. this.dom = {};
  4186. this.hammer = null;
  4187. var me = this;
  4188. this.itemsData = null; // DataSet
  4189. this.range = null; // Range or Object {start: number, end: number}
  4190. // data change listeners
  4191. this.listeners = {
  4192. 'add': function (event, params, senderId) {
  4193. if (senderId != me.id) me._onAdd(params.items);
  4194. },
  4195. 'update': function (event, params, senderId) {
  4196. if (senderId != me.id) me._onUpdate(params.items);
  4197. },
  4198. 'remove': function (event, params, senderId) {
  4199. if (senderId != me.id) me._onRemove(params.items);
  4200. }
  4201. };
  4202. this.items = {}; // object with an Item for every data item
  4203. this.orderedItems = {
  4204. byStart: [],
  4205. byEnd: []
  4206. };
  4207. // this.systemLoaded = false;
  4208. this.visibleItems = []; // visible, ordered items
  4209. this.selection = []; // list with the ids of all selected nodes
  4210. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4211. this.stack = new Stack(Object.create(this.options));
  4212. this.stackDirty = true; // if true, all items will be restacked on next repaint
  4213. this.touchParams = {}; // stores properties while dragging
  4214. // create the HTML DOM
  4215. this._create();
  4216. }
  4217. ItemSet.prototype = new Panel();
  4218. // available item types will be registered here
  4219. ItemSet.types = {
  4220. box: ItemBox,
  4221. range: ItemRange,
  4222. rangeoverflow: ItemRangeOverflow,
  4223. point: ItemPoint
  4224. };
  4225. /**
  4226. * Create the HTML DOM for the ItemSet
  4227. */
  4228. ItemSet.prototype._create = function _create(){
  4229. var frame = document.createElement('div');
  4230. frame['timeline-itemset'] = this;
  4231. this.frame = frame;
  4232. // create background panel
  4233. var background = document.createElement('div');
  4234. background.className = 'background';
  4235. this.backgroundPanel.frame.appendChild(background);
  4236. this.dom.background = background;
  4237. // create foreground panel
  4238. var foreground = document.createElement('div');
  4239. foreground.className = 'foreground';
  4240. frame.appendChild(foreground);
  4241. this.dom.foreground = foreground;
  4242. // create axis panel
  4243. var axis = document.createElement('div');
  4244. axis.className = 'axis';
  4245. this.dom.axis = axis;
  4246. this.axisPanel.frame.appendChild(axis);
  4247. // attach event listeners
  4248. // TODO: use event listeners from the rootpanel to improve performance?
  4249. this.hammer = Hammer(frame, {
  4250. prevent_default: true
  4251. });
  4252. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4253. this.hammer.on('drag', this._onDrag.bind(this));
  4254. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4255. };
  4256. /**
  4257. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4258. * @param {Object} [options] The following options are available:
  4259. * {String | function} [className]
  4260. * class name for the itemset
  4261. * {String} [type]
  4262. * Default type for the items. Choose from 'box'
  4263. * (default), 'point', or 'range'. The default
  4264. * Style can be overwritten by individual items.
  4265. * {String} align
  4266. * Alignment for the items, only applicable for
  4267. * ItemBox. Choose 'center' (default), 'left', or
  4268. * 'right'.
  4269. * {String} orientation
  4270. * Orientation of the item set. Choose 'top' or
  4271. * 'bottom' (default).
  4272. * {Number} margin.axis
  4273. * Margin between the axis and the items in pixels.
  4274. * Default is 20.
  4275. * {Number} margin.item
  4276. * Margin between items in pixels. Default is 10.
  4277. * {Number} padding
  4278. * Padding of the contents of an item in pixels.
  4279. * Must correspond with the items css. Default is 5.
  4280. * {Function} snap
  4281. * Function to let items snap to nice dates when
  4282. * dragging items.
  4283. */
  4284. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4285. /**
  4286. * Hide the component from the DOM
  4287. */
  4288. ItemSet.prototype.hide = function hide() {
  4289. // remove the axis with dots
  4290. if (this.dom.axis.parentNode) {
  4291. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4292. }
  4293. // remove the background with vertical lines
  4294. if (this.dom.background.parentNode) {
  4295. this.dom.background.parentNode.removeChild(this.dom.background);
  4296. }
  4297. };
  4298. /**
  4299. * Show the component in the DOM (when not already visible).
  4300. * @return {Boolean} changed
  4301. */
  4302. ItemSet.prototype.show = function show() {
  4303. // show axis with dots
  4304. if (!this.dom.axis.parentNode) {
  4305. this.axisPanel.frame.appendChild(this.dom.axis);
  4306. }
  4307. // show background with vertical lines
  4308. if (!this.dom.background.parentNode) {
  4309. this.backgroundPanel.frame.appendChild(this.dom.background);
  4310. }
  4311. };
  4312. /**
  4313. * Set range (start and end).
  4314. * @param {Range | Object} range A Range or an object containing start and end.
  4315. */
  4316. ItemSet.prototype.setRange = function setRange(range) {
  4317. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4318. throw new TypeError('Range must be an instance of Range, ' +
  4319. 'or an object containing start and end.');
  4320. }
  4321. this.range = range;
  4322. };
  4323. /**
  4324. * Set selected items by their id. Replaces the current selection
  4325. * Unknown id's are silently ignored.
  4326. * @param {Array} [ids] An array with zero or more id's of the items to be
  4327. * selected. If ids is an empty array, all items will be
  4328. * unselected.
  4329. */
  4330. ItemSet.prototype.setSelection = function setSelection(ids) {
  4331. var i, ii, id, item;
  4332. if (ids) {
  4333. if (!Array.isArray(ids)) {
  4334. throw new TypeError('Array expected');
  4335. }
  4336. // unselect currently selected items
  4337. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4338. id = this.selection[i];
  4339. item = this.items[id];
  4340. if (item) item.unselect();
  4341. }
  4342. // select items
  4343. this.selection = [];
  4344. for (i = 0, ii = ids.length; i < ii; i++) {
  4345. id = ids[i];
  4346. item = this.items[id];
  4347. if (item) {
  4348. this.selection.push(id);
  4349. item.select();
  4350. }
  4351. }
  4352. }
  4353. };
  4354. /**
  4355. * Get the selected items by their id
  4356. * @return {Array} ids The ids of the selected items
  4357. */
  4358. ItemSet.prototype.getSelection = function getSelection() {
  4359. return this.selection.concat([]);
  4360. };
  4361. /**
  4362. * Deselect a selected item
  4363. * @param {String | Number} id
  4364. * @private
  4365. */
  4366. ItemSet.prototype._deselect = function _deselect(id) {
  4367. var selection = this.selection;
  4368. for (var i = 0, ii = selection.length; i < ii; i++) {
  4369. if (selection[i] == id) { // non-strict comparison!
  4370. selection.splice(i, 1);
  4371. break;
  4372. }
  4373. }
  4374. };
  4375. /**
  4376. * Return the item sets frame
  4377. * @returns {HTMLElement} frame
  4378. */
  4379. ItemSet.prototype.getFrame = function getFrame() {
  4380. return this.frame;
  4381. };
  4382. /**
  4383. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  4384. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  4385. * 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
  4386. * if the time we selected (start or end) is within the current range).
  4387. *
  4388. * 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
  4389. * 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,
  4390. * either the start OR end time has to be in the range.
  4391. *
  4392. * @param {Boolean} byEnd
  4393. * @returns {number}
  4394. * @private
  4395. */
  4396. ItemSet.prototype._binarySearch = function _binarySearch(byEnd) {
  4397. var array = []
  4398. var byTime = byEnd ? "end" : "start";
  4399. if (byEnd == true) {array = this.orderedItems.byEnd; }
  4400. else {array = this.orderedItems.byStart;}
  4401. var interval = this.range.end - this.range.start;
  4402. var found = false;
  4403. var low = 0;
  4404. var high = array.length;
  4405. var guess = Math.floor(0.5*(high+low));
  4406. var newGuess;
  4407. if (high == 0) {guess = -1;}
  4408. else if (high == 1) {
  4409. if ((array[guess].data[byTime] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end)) {
  4410. guess = 0;
  4411. }
  4412. else {
  4413. guess = -1;
  4414. }
  4415. }
  4416. else {
  4417. high -= 1;
  4418. while (found == false) {
  4419. if ((array[guess].data[byTime] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end)) {
  4420. found = true;
  4421. }
  4422. else {
  4423. if (array[guess].data[byTime] < this.range.start - interval) { // it is too small --> increase low
  4424. low = Math.floor(0.5*(high+low));
  4425. }
  4426. else { // it is too big --> decrease high
  4427. high = Math.floor(0.5*(high+low));
  4428. }
  4429. newGuess = Math.floor(0.5*(high+low));
  4430. // not in list;
  4431. if (guess == newGuess) {
  4432. guess = -1;
  4433. found = true;
  4434. }
  4435. else {
  4436. guess = newGuess;
  4437. }
  4438. }
  4439. }
  4440. }
  4441. return guess;
  4442. }
  4443. /**
  4444. * this function checks if an item is invisible. If it is NOT we make it visible and add it to the global visible items. If it is, return true.
  4445. *
  4446. * @param {itemRange | itemPoint | itemBox} item
  4447. * @returns {boolean}
  4448. * @private
  4449. */
  4450. ItemSet.prototype._checkIfInvisible = function _checkIfInvisible(item) {
  4451. if (item.isVisible(this.range)) {
  4452. if (!item.displayed) item.show();
  4453. item.repositionX();
  4454. if (this.visibleItems.indexOf(item) == -1) {
  4455. this.visibleItems.push(item);
  4456. }
  4457. return false;
  4458. }
  4459. else {
  4460. return true;
  4461. }
  4462. };
  4463. /**
  4464. * this function is very similar to the _checkIfInvisible() but it does not return booleans, hides the item if it should not be seen and always adds to the visibleItems.
  4465. * this one is for brute forcing and hiding.
  4466. *
  4467. * @param {itemRange | itemPoint | itemBox} item
  4468. * @param {array} visibleItems
  4469. * @private
  4470. */
  4471. ItemSet.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems) {
  4472. if (item.isVisible(this.range)) {
  4473. if (!item.displayed) item.show();
  4474. // reposition item horizontally
  4475. item.repositionX();
  4476. visibleItems.push(item);
  4477. }
  4478. else {
  4479. if (item.displayed) item.hide();
  4480. }
  4481. };
  4482. /**
  4483. * Repaint the component
  4484. * @return {boolean} Returns true if the component is resized
  4485. */
  4486. ItemSet.prototype.repaint = function repaint() {
  4487. var asSize = util.option.asSize,
  4488. asString = util.option.asString,
  4489. options = this.options,
  4490. orientation = this.getOption('orientation'),
  4491. frame = this.frame;
  4492. // update className
  4493. frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  4494. // check whether zoomed (in that case we need to re-stack everything)
  4495. var visibleInterval = this.range.end - this.range.start;
  4496. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  4497. this.lastVisibleInterval = visibleInterval;
  4498. this.lastWidth = this.width;
  4499. var newVisibleItems = [];
  4500. var item;
  4501. var orderedItems = this.orderedItems;
  4502. // first check if the items that were in view previously are still in view.
  4503. // this handles the case for the ItemRange that is both before and after the current one.
  4504. if (this.visibleItems.length > 0) {
  4505. for (var i = 0; i < this.visibleItems.length; i++) {
  4506. this._checkIfVisible(this.visibleItems[i],newVisibleItems);
  4507. }
  4508. }
  4509. this.visibleItems = newVisibleItems;
  4510. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  4511. if (this.visibleItems.length == 0) {var initialPosByStart = this._binarySearch(false);}
  4512. else {var initialPosByStart = orderedItems.byStart.indexOf(this.visibleItems[0]);}
  4513. // use visible search to find a visible ItemRange (only based on endTime)
  4514. var initialPosByEnd = this._binarySearch(true);
  4515. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  4516. if (initialPosByStart != -1) {
  4517. for (var i = initialPosByStart; i >= 0; i--) {
  4518. if (this._checkIfInvisible(orderedItems.byStart[i])) {break;}
  4519. }
  4520. for (var i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  4521. if (this._checkIfInvisible(orderedItems.byStart[i])) {break;}
  4522. }
  4523. }
  4524. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  4525. if (initialPosByEnd != -1) {
  4526. for (var i = initialPosByEnd; i >= 0; i--) {
  4527. if (this._checkIfInvisible(orderedItems.byEnd[i])) {break;}
  4528. }
  4529. for (var i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  4530. if (this._checkIfInvisible(orderedItems.byEnd[i])) {break;}
  4531. }
  4532. }
  4533. // reposition visible items vertically
  4534. //this.stack.order(this.visibleItems); // TODO: improve ordering
  4535. var force = this.stackDirty || zoomed; // force re-stacking of all items if true
  4536. this.stack.stack(this.visibleItems, force);
  4537. this.stackDirty = false;
  4538. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  4539. this.visibleItems[i].repositionY();
  4540. }
  4541. // recalculate the height of the itemset
  4542. var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
  4543. marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
  4544. height;
  4545. // determine the height from the stacked items
  4546. var visibleItems = this.visibleItems;
  4547. if (visibleItems.length) {
  4548. var min = visibleItems[0].top;
  4549. var max = visibleItems[0].top + visibleItems[0].height;
  4550. util.forEach(visibleItems, function (item) {
  4551. min = Math.min(min, item.top);
  4552. max = Math.max(max, (item.top + item.height));
  4553. });
  4554. height = (max - min) + marginAxis + marginItem;
  4555. }
  4556. else {
  4557. height = marginAxis + marginItem;
  4558. }
  4559. // reposition frame
  4560. frame.style.left = asSize(options.left, '');
  4561. frame.style.right = asSize(options.right, '');
  4562. frame.style.top = asSize((orientation == 'top') ? '0' : '');
  4563. frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4564. frame.style.width = asSize(options.width, '100%');
  4565. frame.style.height = asSize(height);
  4566. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  4567. // calculate actual size and position
  4568. this.top = frame.offsetTop;
  4569. this.left = frame.offsetLeft;
  4570. this.width = frame.offsetWidth;
  4571. this.height = height;
  4572. // reposition axis
  4573. this.dom.axis.style.left = asSize(options.left, '0');
  4574. this.dom.axis.style.right = asSize(options.right, '');
  4575. this.dom.axis.style.width = asSize(options.width, '100%');
  4576. this.dom.axis.style.height = asSize(0);
  4577. this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
  4578. this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4579. return this._isResized();
  4580. };
  4581. /**
  4582. * Get the foreground container element
  4583. * @return {HTMLElement} foreground
  4584. */
  4585. ItemSet.prototype.getForeground = function getForeground() {
  4586. return this.dom.foreground;
  4587. };
  4588. /**
  4589. * Get the background container element
  4590. * @return {HTMLElement} background
  4591. */
  4592. ItemSet.prototype.getBackground = function getBackground() {
  4593. return this.dom.background;
  4594. };
  4595. /**
  4596. * Get the axis container element
  4597. * @return {HTMLElement} axis
  4598. */
  4599. ItemSet.prototype.getAxis = function getAxis() {
  4600. return this.dom.axis;
  4601. };
  4602. /**
  4603. * Set items
  4604. * @param {vis.DataSet | null} items
  4605. */
  4606. ItemSet.prototype.setItems = function setItems(items) {
  4607. var me = this,
  4608. ids,
  4609. oldItemsData = this.itemsData;
  4610. // replace the dataset
  4611. if (!items) {
  4612. this.itemsData = null;
  4613. }
  4614. else if (items instanceof DataSet || items instanceof DataView) {
  4615. this.itemsData = items;
  4616. }
  4617. else {
  4618. throw new TypeError('Data must be an instance of DataSet');
  4619. }
  4620. if (oldItemsData) {
  4621. // unsubscribe from old dataset
  4622. util.forEach(this.listeners, function (callback, event) {
  4623. oldItemsData.unsubscribe(event, callback);
  4624. });
  4625. // remove all drawn items
  4626. ids = oldItemsData.getIds();
  4627. this._onRemove(ids);
  4628. }
  4629. if (this.itemsData) {
  4630. // subscribe to new dataset
  4631. var id = this.id;
  4632. util.forEach(this.listeners, function (callback, event) {
  4633. me.itemsData.on(event, callback, id);
  4634. });
  4635. // draw all new items
  4636. ids = this.itemsData.getIds();
  4637. this._onAdd(ids);
  4638. }
  4639. };
  4640. /**
  4641. * Get the current items items
  4642. * @returns {vis.DataSet | null}
  4643. */
  4644. ItemSet.prototype.getItems = function getItems() {
  4645. return this.itemsData;
  4646. };
  4647. /**
  4648. * Remove an item by its id
  4649. * @param {String | Number} id
  4650. */
  4651. ItemSet.prototype.removeItem = function removeItem (id) {
  4652. var item = this.itemsData.get(id),
  4653. dataset = this._myDataSet();
  4654. if (item) {
  4655. // confirm deletion
  4656. this.options.onRemove(item, function (item) {
  4657. if (item) {
  4658. // remove by id here, it is possible that an item has no id defined
  4659. // itself, so better not delete by the item itself
  4660. dataset.remove(id);
  4661. }
  4662. });
  4663. }
  4664. };
  4665. /**
  4666. * Handle updated items
  4667. * @param {Number[]} ids
  4668. * @private
  4669. */
  4670. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4671. var me = this,
  4672. items = this.items,
  4673. itemOptions = this.itemOptions;
  4674. ids.forEach(function (id) {
  4675. var itemData = me.itemsData.get(id),
  4676. item = items[id],
  4677. type = itemData.type ||
  4678. (itemData.start && itemData.end && 'range') ||
  4679. me.options.type ||
  4680. 'box';
  4681. var constructor = ItemSet.types[type];
  4682. if (item) {
  4683. // update item
  4684. if (!constructor || !(item instanceof constructor)) {
  4685. // item type has changed, hide and delete the item
  4686. item.hide();
  4687. item = null;
  4688. }
  4689. else {
  4690. item.data = itemData; // TODO: create a method item.setData ?
  4691. }
  4692. }
  4693. if (!item) {
  4694. // create item
  4695. if (constructor) {
  4696. item = new constructor(me, itemData, me.options, itemOptions);
  4697. item.id = id;
  4698. }
  4699. else {
  4700. throw new TypeError('Unknown item type "' + type + '"');
  4701. }
  4702. }
  4703. me.items[id] = item;
  4704. if (type == 'range') {
  4705. me._checkIfVisible(item,this.visibleItems);
  4706. }
  4707. });
  4708. this._order();
  4709. // this.systemLoaded = false;
  4710. this.stackDirty = true; // force re-stacking of all items next repaint
  4711. this.emit('change');
  4712. };
  4713. /**
  4714. * Handle added items
  4715. * @param {Number[]} ids
  4716. * @private
  4717. */
  4718. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  4719. /**
  4720. * Handle removed items
  4721. * @param {Number[]} ids
  4722. * @private
  4723. */
  4724. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4725. var count = 0;
  4726. var me = this;
  4727. ids.forEach(function (id) {
  4728. var item = me.items[id];
  4729. if (item) {
  4730. count++;
  4731. item.hide();
  4732. delete me.items[id];
  4733. // remove from visible items
  4734. var index = me.visibleItems.indexOf(me.item);
  4735. me.visibleItems.splice(index,1);
  4736. // remove from selection
  4737. index = me.selection.indexOf(id);
  4738. if (index != -1) me.selection.splice(index, 1);
  4739. }
  4740. });
  4741. if (count) {
  4742. // update order
  4743. this._order();
  4744. this.stackDirty = true; // force re-stacking of all items next repaint
  4745. this.emit('change');
  4746. }
  4747. };
  4748. /**
  4749. * Order the items
  4750. * @private
  4751. */
  4752. ItemSet.prototype._order = function _order() {
  4753. var array = util.toArray(this.items);
  4754. this.orderedItems.byStart = array;
  4755. this.orderedItems.byEnd = this._constructByEndArray(array);
  4756. //this.orderedItems.byEnd = [].concat(array); // this copies the array
  4757. // reorder the items
  4758. this.stack.orderByStart(this.orderedItems.byStart);
  4759. this.stack.orderByEnd(this.orderedItems.byEnd);
  4760. };
  4761. ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
  4762. var endArray = [];
  4763. for (var i = 0; i < array.length; i++) {
  4764. if (array[i] instanceof ItemRange) {
  4765. endArray.push(array[i]);
  4766. }
  4767. }
  4768. return endArray;
  4769. };
  4770. /**
  4771. * Start dragging the selected events
  4772. * @param {Event} event
  4773. * @private
  4774. */
  4775. ItemSet.prototype._onDragStart = function (event) {
  4776. if (!this.options.editable) {
  4777. return;
  4778. }
  4779. var item = ItemSet.itemFromTarget(event),
  4780. me = this;
  4781. if (item && item.selected) {
  4782. var dragLeftItem = event.target.dragLeftItem;
  4783. var dragRightItem = event.target.dragRightItem;
  4784. if (dragLeftItem) {
  4785. this.touchParams.itemProps = [{
  4786. item: dragLeftItem,
  4787. start: item.data.start.valueOf()
  4788. }];
  4789. }
  4790. else if (dragRightItem) {
  4791. this.touchParams.itemProps = [{
  4792. item: dragRightItem,
  4793. end: item.data.end.valueOf()
  4794. }];
  4795. }
  4796. else {
  4797. this.touchParams.itemProps = this.getSelection().map(function (id) {
  4798. var item = me.items[id];
  4799. var props = {
  4800. item: item
  4801. };
  4802. if ('start' in item.data) {
  4803. props.start = item.data.start.valueOf()
  4804. }
  4805. if ('end' in item.data) {
  4806. props.end = item.data.end.valueOf()
  4807. }
  4808. return props;
  4809. });
  4810. }
  4811. event.stopPropagation();
  4812. }
  4813. };
  4814. /**
  4815. * Drag selected items
  4816. * @param {Event} event
  4817. * @private
  4818. */
  4819. ItemSet.prototype._onDrag = function (event) {
  4820. if (this.touchParams.itemProps) {
  4821. var snap = this.options.snap || null,
  4822. deltaX = event.gesture.deltaX,
  4823. scale = (this.width / (this.range.end - this.range.start)),
  4824. offset = deltaX / scale;
  4825. // move
  4826. this.touchParams.itemProps.forEach(function (props) {
  4827. if ('start' in props) {
  4828. var start = new Date(props.start + offset);
  4829. props.item.data.start = snap ? snap(start) : start;
  4830. }
  4831. if ('end' in props) {
  4832. var end = new Date(props.end + offset);
  4833. props.item.data.end = snap ? snap(end) : end;
  4834. }
  4835. });
  4836. // TODO: implement onMoving handler
  4837. // TODO: implement dragging from one group to another
  4838. this.stackDirty = true; // force re-stacking of all items next repaint
  4839. this.emit('change');
  4840. event.stopPropagation();
  4841. }
  4842. };
  4843. /**
  4844. * End of dragging selected items
  4845. * @param {Event} event
  4846. * @private
  4847. */
  4848. ItemSet.prototype._onDragEnd = function (event) {
  4849. if (this.touchParams.itemProps) {
  4850. // prepare a change set for the changed items
  4851. var changes = [],
  4852. me = this,
  4853. dataset = this._myDataSet();
  4854. this.touchParams.itemProps.forEach(function (props) {
  4855. var id = props.item.id,
  4856. item = me.itemsData.get(id);
  4857. var changed = false;
  4858. if ('start' in props.item.data) {
  4859. changed = (props.start != props.item.data.start.valueOf());
  4860. item.start = util.convert(props.item.data.start, dataset.convert['start']);
  4861. }
  4862. if ('end' in props.item.data) {
  4863. changed = changed || (props.end != props.item.data.end.valueOf());
  4864. item.end = util.convert(props.item.data.end, dataset.convert['end']);
  4865. }
  4866. // only apply changes when start or end is actually changed
  4867. if (changed) {
  4868. me.options.onMove(item, function (item) {
  4869. if (item) {
  4870. // apply changes
  4871. item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
  4872. changes.push(item);
  4873. }
  4874. else {
  4875. // restore original values
  4876. if ('start' in props) props.item.data.start = props.start;
  4877. if ('end' in props) props.item.data.end = props.end;
  4878. me.stackDirty = true; // force re-stacking of all items next repaint
  4879. me.emit('change');
  4880. }
  4881. });
  4882. }
  4883. });
  4884. this.touchParams.itemProps = null;
  4885. // apply the changes to the data (if there are changes)
  4886. if (changes.length) {
  4887. dataset.update(changes);
  4888. }
  4889. event.stopPropagation();
  4890. }
  4891. };
  4892. /**
  4893. * Find an item from an event target:
  4894. * searches for the attribute 'timeline-item' in the event target's element tree
  4895. * @param {Event} event
  4896. * @return {Item | null} item
  4897. */
  4898. ItemSet.itemFromTarget = function itemFromTarget (event) {
  4899. var target = event.target;
  4900. while (target) {
  4901. if (target.hasOwnProperty('timeline-item')) {
  4902. return target['timeline-item'];
  4903. }
  4904. target = target.parentNode;
  4905. }
  4906. return null;
  4907. };
  4908. /**
  4909. * Find the ItemSet from an event target:
  4910. * searches for the attribute 'timeline-itemset' in the event target's element tree
  4911. * @param {Event} event
  4912. * @return {ItemSet | null} item
  4913. */
  4914. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  4915. var target = event.target;
  4916. while (target) {
  4917. if (target.hasOwnProperty('timeline-itemset')) {
  4918. return target['timeline-itemset'];
  4919. }
  4920. target = target.parentNode;
  4921. }
  4922. return null;
  4923. };
  4924. /**
  4925. * Find the DataSet to which this ItemSet is connected
  4926. * @returns {null | DataSet} dataset
  4927. * @private
  4928. */
  4929. ItemSet.prototype._myDataSet = function _myDataSet() {
  4930. // find the root DataSet
  4931. var dataset = this.itemsData;
  4932. while (dataset instanceof DataView) {
  4933. dataset = dataset.data;
  4934. }
  4935. return dataset;
  4936. };
  4937. /**
  4938. * @constructor Item
  4939. * @param {ItemSet} parent
  4940. * @param {Object} data Object containing (optional) parameters type,
  4941. * start, end, content, group, className.
  4942. * @param {Object} [options] Options to set initial property values
  4943. * @param {Object} [defaultOptions] default options
  4944. * // TODO: describe available options
  4945. */
  4946. function Item (parent, data, options, defaultOptions) {
  4947. this.parent = parent;
  4948. this.data = data;
  4949. this.dom = null;
  4950. this.options = options || {};
  4951. this.defaultOptions = defaultOptions || {};
  4952. this.selected = false;
  4953. this.displayed = false;
  4954. this.dirty = true;
  4955. this.top = null;
  4956. this.left = null;
  4957. this.width = null;
  4958. this.height = null;
  4959. }
  4960. /**
  4961. * Select current item
  4962. */
  4963. Item.prototype.select = function select() {
  4964. this.selected = true;
  4965. if (this.displayed) this.repaint();
  4966. };
  4967. /**
  4968. * Unselect current item
  4969. */
  4970. Item.prototype.unselect = function unselect() {
  4971. this.selected = false;
  4972. if (this.displayed) this.repaint();
  4973. };
  4974. /**
  4975. * Show the Item in the DOM (when not already visible)
  4976. * @return {Boolean} changed
  4977. */
  4978. Item.prototype.show = function show() {
  4979. return false;
  4980. };
  4981. /**
  4982. * Hide the Item from the DOM (when visible)
  4983. * @return {Boolean} changed
  4984. */
  4985. Item.prototype.hide = function hide() {
  4986. return false;
  4987. };
  4988. /**
  4989. * Repaint the item
  4990. */
  4991. Item.prototype.repaint = function repaint() {
  4992. // should be implemented by the item
  4993. };
  4994. /**
  4995. * Reposition the Item horizontally
  4996. */
  4997. Item.prototype.repositionX = function repositionX() {
  4998. // should be implemented by the item
  4999. };
  5000. /**
  5001. * Reposition the Item vertically
  5002. */
  5003. Item.prototype.repositionY = function repositionY() {
  5004. // should be implemented by the item
  5005. };
  5006. /**
  5007. * Repaint a delete button on the top right of the item when the item is selected
  5008. * @param {HTMLElement} anchor
  5009. * @private
  5010. */
  5011. Item.prototype._repaintDeleteButton = function (anchor) {
  5012. if (this.selected && this.options.editable && !this.dom.deleteButton) {
  5013. // create and show button
  5014. var parent = this.parent;
  5015. var id = this.id;
  5016. var deleteButton = document.createElement('div');
  5017. deleteButton.className = 'delete';
  5018. deleteButton.title = 'Delete this item';
  5019. Hammer(deleteButton, {
  5020. preventDefault: true
  5021. }).on('tap', function (event) {
  5022. parent.removeItem(id);
  5023. event.stopPropagation();
  5024. });
  5025. anchor.appendChild(deleteButton);
  5026. this.dom.deleteButton = deleteButton;
  5027. }
  5028. else if (!this.selected && this.dom.deleteButton) {
  5029. // remove button
  5030. if (this.dom.deleteButton.parentNode) {
  5031. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  5032. }
  5033. this.dom.deleteButton = null;
  5034. }
  5035. };
  5036. /**
  5037. * @constructor ItemBox
  5038. * @extends Item
  5039. * @param {ItemSet} parent
  5040. * @param {Object} data Object containing parameters start
  5041. * content, className.
  5042. * @param {Object} [options] Options to set initial property values
  5043. * @param {Object} [defaultOptions] default options
  5044. * // TODO: describe available options
  5045. */
  5046. function ItemBox (parent, data, options, defaultOptions) {
  5047. this.props = {
  5048. dot: {
  5049. width: 0,
  5050. height: 0
  5051. },
  5052. line: {
  5053. width: 0,
  5054. height: 0
  5055. }
  5056. };
  5057. // validate data
  5058. if (data) {
  5059. if (data.start == undefined) {
  5060. throw new Error('Property "start" missing in item ' + data);
  5061. }
  5062. }
  5063. Item.call(this, parent, data, options, defaultOptions);
  5064. }
  5065. ItemBox.prototype = new Item (null, null);
  5066. /**
  5067. * Check whether this item is visible inside given range
  5068. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5069. * @returns {boolean} True if visible
  5070. */
  5071. ItemBox.prototype.isVisible = function isVisible (range) {
  5072. // determine visibility
  5073. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  5074. var interval = (range.end - range.start) / 4;
  5075. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  5076. };
  5077. /**
  5078. * Repaint the item
  5079. */
  5080. ItemBox.prototype.repaint = function repaint() {
  5081. var dom = this.dom;
  5082. if (!dom) {
  5083. // create DOM
  5084. this.dom = {};
  5085. dom = this.dom;
  5086. // create main box
  5087. dom.box = document.createElement('DIV');
  5088. // contents box (inside the background box). used for making margins
  5089. dom.content = document.createElement('DIV');
  5090. dom.content.className = 'content';
  5091. dom.box.appendChild(dom.content);
  5092. // line to axis
  5093. dom.line = document.createElement('DIV');
  5094. dom.line.className = 'line';
  5095. // dot on axis
  5096. dom.dot = document.createElement('DIV');
  5097. dom.dot.className = 'dot';
  5098. // attach this item as attribute
  5099. dom.box['timeline-item'] = this;
  5100. }
  5101. // append DOM to parent DOM
  5102. if (!this.parent) {
  5103. throw new Error('Cannot repaint item: no parent attached');
  5104. }
  5105. if (!dom.box.parentNode) {
  5106. var foreground = this.parent.getForeground();
  5107. if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5108. foreground.appendChild(dom.box);
  5109. }
  5110. if (!dom.line.parentNode) {
  5111. var background = this.parent.getBackground();
  5112. if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
  5113. background.appendChild(dom.line);
  5114. }
  5115. if (!dom.dot.parentNode) {
  5116. var axis = this.parent.getAxis();
  5117. if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
  5118. axis.appendChild(dom.dot);
  5119. }
  5120. this.displayed = true;
  5121. // update contents
  5122. if (this.data.content != this.content) {
  5123. this.content = this.data.content;
  5124. if (this.content instanceof Element) {
  5125. dom.content.innerHTML = '';
  5126. dom.content.appendChild(this.content);
  5127. }
  5128. else if (this.data.content != undefined) {
  5129. dom.content.innerHTML = this.content;
  5130. }
  5131. else {
  5132. throw new Error('Property "content" missing in item ' + this.data.id);
  5133. }
  5134. this.dirty = true;
  5135. }
  5136. // update class
  5137. var className = (this.data.className? ' ' + this.data.className : '') +
  5138. (this.selected ? ' selected' : '');
  5139. if (this.className != className) {
  5140. this.className = className;
  5141. dom.box.className = 'item box' + className;
  5142. dom.line.className = 'item line' + className;
  5143. dom.dot.className = 'item dot' + className;
  5144. this.dirty = true;
  5145. }
  5146. // recalculate size
  5147. if (this.dirty) {
  5148. this.props.dot.height = dom.dot.offsetHeight;
  5149. this.props.dot.width = dom.dot.offsetWidth;
  5150. this.props.line.width = dom.line.offsetWidth;
  5151. this.width = dom.box.offsetWidth;
  5152. this.height = dom.box.offsetHeight;
  5153. this.dirty = false;
  5154. }
  5155. this._repaintDeleteButton(dom.box);
  5156. };
  5157. /**
  5158. * Show the item in the DOM (when not already displayed). The items DOM will
  5159. * be created when needed.
  5160. */
  5161. ItemBox.prototype.show = function show() {
  5162. if (!this.displayed) {
  5163. this.repaint();
  5164. }
  5165. };
  5166. /**
  5167. * Hide the item from the DOM (when visible)
  5168. */
  5169. ItemBox.prototype.hide = function hide() {
  5170. if (this.displayed) {
  5171. var dom = this.dom;
  5172. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  5173. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  5174. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  5175. this.top = null;
  5176. this.left = null;
  5177. this.displayed = false;
  5178. }
  5179. };
  5180. /**
  5181. * Reposition the item horizontally
  5182. * @Override
  5183. */
  5184. ItemBox.prototype.repositionX = function repositionX() {
  5185. var start = this.defaultOptions.toScreen(this.data.start),
  5186. align = this.options.align || this.defaultOptions.align,
  5187. left,
  5188. box = this.dom.box,
  5189. line = this.dom.line,
  5190. dot = this.dom.dot;
  5191. // calculate left position of the box
  5192. if (align == 'right') {
  5193. this.left = start - this.width;
  5194. }
  5195. else if (align == 'left') {
  5196. this.left = start;
  5197. }
  5198. else {
  5199. // default or 'center'
  5200. this.left = start - this.width / 2;
  5201. }
  5202. // reposition box
  5203. box.style.left = this.left + 'px';
  5204. // reposition line
  5205. line.style.left = (start - this.props.line.width / 2) + 'px';
  5206. // reposition dot
  5207. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  5208. };
  5209. /**
  5210. * Reposition the item vertically
  5211. * @Override
  5212. */
  5213. ItemBox.prototype.repositionY = function repositionY () {
  5214. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5215. box = this.dom.box,
  5216. line = this.dom.line,
  5217. dot = this.dom.dot;
  5218. if (orientation == 'top') {
  5219. box.style.top = (this.top || 0) + 'px';
  5220. box.style.bottom = '';
  5221. line.style.top = '0';
  5222. line.style.bottom = '';
  5223. line.style.height = (this.parent.top + this.top + 1) + 'px';
  5224. }
  5225. else { // orientation 'bottom'
  5226. box.style.top = '';
  5227. box.style.bottom = (this.top || 0) + 'px';
  5228. line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
  5229. line.style.bottom = '0';
  5230. line.style.height = '';
  5231. }
  5232. dot.style.top = (-this.props.dot.height / 2) + 'px';
  5233. };
  5234. /**
  5235. * @constructor ItemPoint
  5236. * @extends Item
  5237. * @param {ItemSet} parent
  5238. * @param {Object} data Object containing parameters start
  5239. * content, className.
  5240. * @param {Object} [options] Options to set initial property values
  5241. * @param {Object} [defaultOptions] default options
  5242. * // TODO: describe available options
  5243. */
  5244. function ItemPoint (parent, data, options, defaultOptions) {
  5245. this.props = {
  5246. dot: {
  5247. top: 0,
  5248. width: 0,
  5249. height: 0
  5250. },
  5251. content: {
  5252. height: 0,
  5253. marginLeft: 0
  5254. }
  5255. };
  5256. // validate data
  5257. if (data) {
  5258. if (data.start == undefined) {
  5259. throw new Error('Property "start" missing in item ' + data);
  5260. }
  5261. }
  5262. Item.call(this, parent, data, options, defaultOptions);
  5263. }
  5264. ItemPoint.prototype = new Item (null, null);
  5265. /**
  5266. * Check whether this item is visible inside given range
  5267. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5268. * @returns {boolean} True if visible
  5269. */
  5270. ItemPoint.prototype.isVisible = function isVisible (range) {
  5271. // determine visibility
  5272. var interval = (range.end - range.start);
  5273. return (this.data.start > range.start - interval) && (this.data.start < range.end);
  5274. }
  5275. /**
  5276. * Repaint the item
  5277. */
  5278. ItemPoint.prototype.repaint = function repaint() {
  5279. var dom = this.dom;
  5280. if (!dom) {
  5281. // create DOM
  5282. this.dom = {};
  5283. dom = this.dom;
  5284. // background box
  5285. dom.point = document.createElement('div');
  5286. // className is updated in repaint()
  5287. // contents box, right from the dot
  5288. dom.content = document.createElement('div');
  5289. dom.content.className = 'content';
  5290. dom.point.appendChild(dom.content);
  5291. // dot at start
  5292. dom.dot = document.createElement('div');
  5293. dom.dot.className = 'dot';
  5294. dom.point.appendChild(dom.dot);
  5295. // attach this item as attribute
  5296. dom.point['timeline-item'] = this;
  5297. }
  5298. // append DOM to parent DOM
  5299. if (!this.parent) {
  5300. throw new Error('Cannot repaint item: no parent attached');
  5301. }
  5302. if (!dom.point.parentNode) {
  5303. var foreground = this.parent.getForeground();
  5304. if (!foreground) {
  5305. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5306. }
  5307. foreground.appendChild(dom.point);
  5308. }
  5309. this.displayed = true;
  5310. // update contents
  5311. if (this.data.content != this.content) {
  5312. this.content = this.data.content;
  5313. if (this.content instanceof Element) {
  5314. dom.content.innerHTML = '';
  5315. dom.content.appendChild(this.content);
  5316. }
  5317. else if (this.data.content != undefined) {
  5318. dom.content.innerHTML = this.content;
  5319. }
  5320. else {
  5321. throw new Error('Property "content" missing in item ' + this.data.id);
  5322. }
  5323. this.dirty = true;
  5324. }
  5325. // update class
  5326. var className = (this.data.className? ' ' + this.data.className : '') +
  5327. (this.selected ? ' selected' : '');
  5328. if (this.className != className) {
  5329. this.className = className;
  5330. dom.point.className = 'item point' + className;
  5331. this.dirty = true;
  5332. }
  5333. // recalculate size
  5334. if (this.dirty) {
  5335. this.width = dom.point.offsetWidth;
  5336. this.height = dom.point.offsetHeight;
  5337. this.props.dot.width = dom.dot.offsetWidth;
  5338. this.props.dot.height = dom.dot.offsetHeight;
  5339. this.props.content.height = dom.content.offsetHeight;
  5340. // resize contents
  5341. dom.content.style.marginLeft = 1.5 * this.props.dot.width + 'px';
  5342. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  5343. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  5344. this.dirty = false;
  5345. }
  5346. this._repaintDeleteButton(dom.point);
  5347. };
  5348. /**
  5349. * Show the item in the DOM (when not already visible). The items DOM will
  5350. * be created when needed.
  5351. */
  5352. ItemPoint.prototype.show = function show() {
  5353. if (!this.displayed) {
  5354. this.repaint();
  5355. }
  5356. };
  5357. /**
  5358. * Hide the item from the DOM (when visible)
  5359. */
  5360. ItemPoint.prototype.hide = function hide() {
  5361. if (this.displayed) {
  5362. if (this.dom.point.parentNode) {
  5363. this.dom.point.parentNode.removeChild(this.dom.point);
  5364. }
  5365. this.top = null;
  5366. this.left = null;
  5367. this.displayed = false;
  5368. }
  5369. };
  5370. /**
  5371. * Reposition the item horizontally
  5372. * @Override
  5373. */
  5374. ItemPoint.prototype.repositionX = function repositionX() {
  5375. var start = this.defaultOptions.toScreen(this.data.start);
  5376. this.left = start - this.props.dot.width / 2;
  5377. // reposition point
  5378. this.dom.point.style.left = this.left + 'px';
  5379. };
  5380. /**
  5381. * Reposition the item vertically
  5382. * @Override
  5383. */
  5384. ItemPoint.prototype.repositionY = function repositionY () {
  5385. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5386. point = this.dom.point;
  5387. if (orientation == 'top') {
  5388. point.style.top = this.top + 'px';
  5389. point.style.bottom = '';
  5390. }
  5391. else {
  5392. point.style.top = '';
  5393. point.style.bottom = this.top + 'px';
  5394. }
  5395. }
  5396. /**
  5397. * @constructor ItemRange
  5398. * @extends Item
  5399. * @param {ItemSet} parent
  5400. * @param {Object} data Object containing parameters start, end
  5401. * content, className.
  5402. * @param {Object} [options] Options to set initial property values
  5403. * @param {Object} [defaultOptions] default options
  5404. * // TODO: describe available options
  5405. */
  5406. function ItemRange (parent, data, options, defaultOptions) {
  5407. this.props = {
  5408. content: {
  5409. width: 0
  5410. }
  5411. };
  5412. // validate data
  5413. if (data) {
  5414. if (data.start == undefined) {
  5415. throw new Error('Property "start" missing in item ' + data.id);
  5416. }
  5417. if (data.end == undefined) {
  5418. throw new Error('Property "end" missing in item ' + data.id);
  5419. }
  5420. }
  5421. Item.call(this, parent, data, options, defaultOptions);
  5422. }
  5423. ItemRange.prototype = new Item (null, null);
  5424. ItemRange.prototype.baseClassName = 'item range';
  5425. /**
  5426. * Check whether this item is visible inside given range
  5427. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5428. * @returns {boolean} True if visible
  5429. */
  5430. ItemRange.prototype.isVisible = function isVisible (range) {
  5431. // determine visibility
  5432. return (this.data.start < range.end) && (this.data.end > range.start);
  5433. };
  5434. /**
  5435. * Repaint the item
  5436. */
  5437. ItemRange.prototype.repaint = function repaint() {
  5438. var dom = this.dom;
  5439. if (!dom) {
  5440. // create DOM
  5441. this.dom = {};
  5442. dom = this.dom;
  5443. // background box
  5444. dom.box = document.createElement('div');
  5445. // className is updated in repaint()
  5446. // contents box
  5447. dom.content = document.createElement('div');
  5448. dom.content.className = 'content';
  5449. dom.box.appendChild(dom.content);
  5450. // attach this item as attribute
  5451. dom.box['timeline-item'] = this;
  5452. }
  5453. // append DOM to parent DOM
  5454. if (!this.parent) {
  5455. throw new Error('Cannot repaint item: no parent attached');
  5456. }
  5457. if (!dom.box.parentNode) {
  5458. var foreground = this.parent.getForeground();
  5459. if (!foreground) {
  5460. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5461. }
  5462. foreground.appendChild(dom.box);
  5463. }
  5464. this.displayed = true;
  5465. // update contents
  5466. if (this.data.content != this.content) {
  5467. this.content = this.data.content;
  5468. if (this.content instanceof Element) {
  5469. dom.content.innerHTML = '';
  5470. dom.content.appendChild(this.content);
  5471. }
  5472. else if (this.data.content != undefined) {
  5473. dom.content.innerHTML = this.content;
  5474. }
  5475. else {
  5476. throw new Error('Property "content" missing in item ' + this.data.id);
  5477. }
  5478. this.dirty = true;
  5479. }
  5480. // update class
  5481. var className = (this.data.className ? (' ' + this.data.className) : '') +
  5482. (this.selected ? ' selected' : '');
  5483. if (this.className != className) {
  5484. this.className = className;
  5485. dom.box.className = this.baseClassName + className;
  5486. this.dirty = true;
  5487. }
  5488. // recalculate size
  5489. if (this.dirty) {
  5490. this.props.content.width = this.dom.content.offsetWidth;
  5491. this.height = this.dom.box.offsetHeight;
  5492. this.dirty = false;
  5493. }
  5494. this._repaintDeleteButton(dom.box);
  5495. this._repaintDragLeft();
  5496. this._repaintDragRight();
  5497. };
  5498. /**
  5499. * Show the item in the DOM (when not already visible). The items DOM will
  5500. * be created when needed.
  5501. */
  5502. ItemRange.prototype.show = function show() {
  5503. if (!this.displayed) {
  5504. this.repaint();
  5505. }
  5506. };
  5507. /**
  5508. * Hide the item from the DOM (when visible)
  5509. * @return {Boolean} changed
  5510. */
  5511. ItemRange.prototype.hide = function hide() {
  5512. if (this.displayed) {
  5513. var box = this.dom.box;
  5514. if (box.parentNode) {
  5515. box.parentNode.removeChild(box);
  5516. }
  5517. this.top = null;
  5518. this.left = null;
  5519. this.displayed = false;
  5520. }
  5521. };
  5522. /**
  5523. * Reposition the item horizontally
  5524. * @Override
  5525. */
  5526. ItemRange.prototype.repositionX = function repositionX() {
  5527. var props = this.props,
  5528. parentWidth = this.parent.width,
  5529. start = this.defaultOptions.toScreen(this.data.start),
  5530. end = this.defaultOptions.toScreen(this.data.end),
  5531. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5532. contentLeft;
  5533. // limit the width of the this, as browsers cannot draw very wide divs
  5534. if (start < -parentWidth) {
  5535. start = -parentWidth;
  5536. }
  5537. if (end > 2 * parentWidth) {
  5538. end = 2 * parentWidth;
  5539. }
  5540. // when range exceeds left of the window, position the contents at the left of the visible area
  5541. if (start < 0) {
  5542. contentLeft = Math.min(-start,
  5543. (end - start - props.content.width - 2 * padding));
  5544. // TODO: remove the need for options.padding. it's terrible.
  5545. }
  5546. else {
  5547. contentLeft = 0;
  5548. }
  5549. this.left = start;
  5550. this.width = Math.max(end - start, 1);
  5551. this.dom.box.style.left = this.left + 'px';
  5552. this.dom.box.style.width = this.width + 'px';
  5553. this.dom.content.style.left = contentLeft + 'px';
  5554. };
  5555. /**
  5556. * Reposition the item vertically
  5557. * @Override
  5558. */
  5559. ItemRange.prototype.repositionY = function repositionY() {
  5560. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5561. box = this.dom.box;
  5562. if (orientation == 'top') {
  5563. box.style.top = this.top + 'px';
  5564. box.style.bottom = '';
  5565. }
  5566. else {
  5567. box.style.top = '';
  5568. box.style.bottom = this.top + 'px';
  5569. }
  5570. };
  5571. /**
  5572. * Repaint a drag area on the left side of the range when the range is selected
  5573. * @private
  5574. */
  5575. ItemRange.prototype._repaintDragLeft = function () {
  5576. if (this.selected && this.options.editable && !this.dom.dragLeft) {
  5577. // create and show drag area
  5578. var dragLeft = document.createElement('div');
  5579. dragLeft.className = 'drag-left';
  5580. dragLeft.dragLeftItem = this;
  5581. // TODO: this should be redundant?
  5582. Hammer(dragLeft, {
  5583. preventDefault: true
  5584. }).on('drag', function () {
  5585. //console.log('drag left')
  5586. });
  5587. this.dom.box.appendChild(dragLeft);
  5588. this.dom.dragLeft = dragLeft;
  5589. }
  5590. else if (!this.selected && this.dom.dragLeft) {
  5591. // delete drag area
  5592. if (this.dom.dragLeft.parentNode) {
  5593. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  5594. }
  5595. this.dom.dragLeft = null;
  5596. }
  5597. };
  5598. /**
  5599. * Repaint a drag area on the right side of the range when the range is selected
  5600. * @private
  5601. */
  5602. ItemRange.prototype._repaintDragRight = function () {
  5603. if (this.selected && this.options.editable && !this.dom.dragRight) {
  5604. // create and show drag area
  5605. var dragRight = document.createElement('div');
  5606. dragRight.className = 'drag-right';
  5607. dragRight.dragRightItem = this;
  5608. // TODO: this should be redundant?
  5609. Hammer(dragRight, {
  5610. preventDefault: true
  5611. }).on('drag', function () {
  5612. //console.log('drag right')
  5613. });
  5614. this.dom.box.appendChild(dragRight);
  5615. this.dom.dragRight = dragRight;
  5616. }
  5617. else if (!this.selected && this.dom.dragRight) {
  5618. // delete drag area
  5619. if (this.dom.dragRight.parentNode) {
  5620. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  5621. }
  5622. this.dom.dragRight = null;
  5623. }
  5624. };
  5625. /**
  5626. * @constructor ItemRangeOverflow
  5627. * @extends ItemRange
  5628. * @param {ItemSet} parent
  5629. * @param {Object} data Object containing parameters start, end
  5630. * content, className.
  5631. * @param {Object} [options] Options to set initial property values
  5632. * @param {Object} [defaultOptions] default options
  5633. * // TODO: describe available options
  5634. */
  5635. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5636. this.props = {
  5637. content: {
  5638. left: 0,
  5639. width: 0
  5640. }
  5641. };
  5642. ItemRange.call(this, parent, data, options, defaultOptions);
  5643. }
  5644. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5645. ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
  5646. /**
  5647. * Reposition the item horizontally
  5648. * @Override
  5649. */
  5650. ItemRangeOverflow.prototype.repositionX = function repositionX() {
  5651. var parentWidth = this.parent.width,
  5652. start = this.defaultOptions.toScreen(this.data.start),
  5653. end = this.defaultOptions.toScreen(this.data.end),
  5654. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5655. contentLeft;
  5656. // limit the width of the this, as browsers cannot draw very wide divs
  5657. if (start < -parentWidth) {
  5658. start = -parentWidth;
  5659. }
  5660. if (end > 2 * parentWidth) {
  5661. end = 2 * parentWidth;
  5662. }
  5663. // when range exceeds left of the window, position the contents at the left of the visible area
  5664. contentLeft = Math.max(-start, 0);
  5665. this.left = start;
  5666. var boxWidth = Math.max(end - start, 1);
  5667. this.width = (this.props.content.width < boxWidth) ?
  5668. boxWidth :
  5669. start + contentLeft + this.props.content.width;
  5670. this.dom.box.style.left = this.left + 'px';
  5671. this.dom.box.style.width = boxWidth + 'px';
  5672. this.dom.content.style.left = contentLeft + 'px';
  5673. };
  5674. /**
  5675. * @constructor Group
  5676. * @param {Panel} groupPanel
  5677. * @param {Panel} labelPanel
  5678. * @param {Panel} backgroundPanel
  5679. * @param {Panel} axisPanel
  5680. * @param {Number | String} groupId
  5681. * @param {Object} [options] Options to set initial property values
  5682. * // TODO: describe available options
  5683. * @extends Component
  5684. */
  5685. function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
  5686. this.id = util.randomUUID();
  5687. this.groupPanel = groupPanel;
  5688. this.labelPanel = labelPanel;
  5689. this.backgroundPanel = backgroundPanel;
  5690. this.axisPanel = axisPanel;
  5691. this.groupId = groupId;
  5692. this.itemSet = null; // ItemSet
  5693. this.options = options || {};
  5694. this.options.top = 0;
  5695. this.props = {
  5696. label: {
  5697. width: 0,
  5698. height: 0
  5699. }
  5700. };
  5701. this.dom = {};
  5702. this.top = 0;
  5703. this.left = 0;
  5704. this.width = 0;
  5705. this.height = 0;
  5706. this._create();
  5707. }
  5708. Group.prototype = new Component();
  5709. // TODO: comment
  5710. Group.prototype.setOptions = Component.prototype.setOptions;
  5711. /**
  5712. * Create DOM elements for the group
  5713. * @private
  5714. */
  5715. Group.prototype._create = function() {
  5716. var label = document.createElement('div');
  5717. label.className = 'vlabel';
  5718. this.dom.label = label;
  5719. var inner = document.createElement('div');
  5720. inner.className = 'inner';
  5721. label.appendChild(inner);
  5722. this.dom.inner = inner;
  5723. };
  5724. /**
  5725. * Set the group data for this group
  5726. * @param {Object} data Group data, can contain properties content and className
  5727. */
  5728. Group.prototype.setData = function setData(data) {
  5729. // update contents
  5730. var content = data && data.content;
  5731. if (content instanceof Element) {
  5732. this.dom.inner.appendChild(content);
  5733. }
  5734. else if (content != undefined) {
  5735. this.dom.inner.innerHTML = content;
  5736. }
  5737. else {
  5738. this.dom.inner.innerHTML = this.groupId;
  5739. }
  5740. // update className
  5741. var className = data && data.className;
  5742. if (className) {
  5743. util.addClassName(this.dom.label, className);
  5744. }
  5745. };
  5746. /**
  5747. * Set item set for the group. The group will create a view on the itemSet,
  5748. * filtered by the groups id.
  5749. * @param {DataSet | DataView} itemsData
  5750. */
  5751. Group.prototype.setItems = function setItems(itemsData) {
  5752. if (this.itemSet) {
  5753. // remove current item set
  5754. this.itemSet.setItems();
  5755. this.itemSet.hide();
  5756. this.groupPanel.frame.removeChild(this.itemSet.getFrame());
  5757. this.itemSet = null;
  5758. }
  5759. if (itemsData) {
  5760. var groupId = this.groupId;
  5761. var me = this;
  5762. var itemSetOptions = util.extend(this.options, {
  5763. height: function () {
  5764. // FIXME: setting height doesn't yet work
  5765. return Math.max(me.props.label.height, me.itemSet.height);
  5766. }
  5767. });
  5768. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
  5769. this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
  5770. this.itemSet.parent = this;
  5771. this.groupPanel.frame.appendChild(this.itemSet.getFrame());
  5772. if (this.range) this.itemSet.setRange(this.range);
  5773. this.view = new DataView(itemsData, {
  5774. filter: function (item) {
  5775. return item.group == groupId;
  5776. }
  5777. });
  5778. this.itemSet.setItems(this.view);
  5779. }
  5780. };
  5781. /**
  5782. * hide the group, detach from DOM if needed
  5783. */
  5784. Group.prototype.show = function show() {
  5785. if (!this.dom.label.parentNode) {
  5786. this.labelPanel.frame.appendChild(this.dom.label);
  5787. }
  5788. var itemSetFrame = this.itemSet && this.itemSet.getFrame();
  5789. if (itemSetFrame) {
  5790. if (itemSetFrame.parentNode) {
  5791. itemSetFrame.parentNode.removeChild(itemSetFrame);
  5792. }
  5793. this.groupPanel.frame.appendChild(itemSetFrame);
  5794. this.itemSet.show();
  5795. }
  5796. };
  5797. /**
  5798. * hide the group, detach from DOM if needed
  5799. */
  5800. Group.prototype.hide = function hide() {
  5801. if (this.dom.label.parentNode) {
  5802. this.dom.label.parentNode.removeChild(this.dom.label);
  5803. }
  5804. if (this.itemSet) {
  5805. this.itemSet.hide();
  5806. }
  5807. var itemSetFrame = this.itemset && this.itemSet.getFrame();
  5808. if (itemSetFrame && itemSetFrame.parentNode) {
  5809. itemSetFrame.parentNode.removeChild(itemSetFrame);
  5810. }
  5811. };
  5812. /**
  5813. * Set range (start and end).
  5814. * @param {Range | Object} range A Range or an object containing start and end.
  5815. */
  5816. Group.prototype.setRange = function (range) {
  5817. this.range = range;
  5818. if (this.itemSet) this.itemSet.setRange(range);
  5819. };
  5820. /**
  5821. * Set selected items by their id. Replaces the current selection.
  5822. * Unknown id's are silently ignored.
  5823. * @param {Array} [ids] An array with zero or more id's of the items to be
  5824. * selected. If ids is an empty array, all items will be
  5825. * unselected.
  5826. */
  5827. Group.prototype.setSelection = function setSelection(ids) {
  5828. if (this.itemSet) this.itemSet.setSelection(ids);
  5829. };
  5830. /**
  5831. * Get the selected items by their id
  5832. * @return {Array} ids The ids of the selected items
  5833. */
  5834. Group.prototype.getSelection = function getSelection() {
  5835. return this.itemSet ? this.itemSet.getSelection() : [];
  5836. };
  5837. /**
  5838. * Repaint the group
  5839. * @return {boolean} Returns true if the component is resized
  5840. */
  5841. Group.prototype.repaint = function repaint() {
  5842. var resized = false;
  5843. this.show();
  5844. if (this.itemSet) {
  5845. resized = this.itemSet.repaint() || resized;
  5846. }
  5847. // calculate inner size of the label
  5848. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  5849. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  5850. this.height = this.itemSet ? this.itemSet.height : 0;
  5851. this.dom.label.style.height = this.height + 'px';
  5852. return resized;
  5853. };
  5854. /**
  5855. * An GroupSet holds a set of groups
  5856. * @param {Panel} contentPanel Panel where the ItemSets will be created
  5857. * @param {Panel} labelPanel Panel where the labels will be created
  5858. * @param {Panel} backgroundPanel Panel where the vertical lines of box
  5859. * items are created
  5860. * @param {Panel} axisPanel Panel on the axis where the dots of box
  5861. * items will be created
  5862. * @param {Object} [options] See GroupSet.setOptions for the available
  5863. * options.
  5864. * @constructor GroupSet
  5865. * @extends Panel
  5866. */
  5867. function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
  5868. this.id = util.randomUUID();
  5869. this.contentPanel = contentPanel;
  5870. this.labelPanel = labelPanel;
  5871. this.backgroundPanel = backgroundPanel;
  5872. this.axisPanel = axisPanel;
  5873. this.options = options || {};
  5874. this.range = null; // Range or Object {start: number, end: number}
  5875. this.itemsData = null; // DataSet with items
  5876. this.groupsData = null; // DataSet with groups
  5877. this.groups = {}; // map with groups
  5878. this.groupIds = []; // list with ordered group ids
  5879. this.dom = {};
  5880. this.props = {
  5881. labels: {
  5882. width: 0
  5883. }
  5884. };
  5885. // TODO: implement right orientation of the labels (left/right)
  5886. var me = this;
  5887. this.listeners = {
  5888. 'add': function (event, params) {
  5889. me._onAdd(params.items);
  5890. },
  5891. 'update': function (event, params) {
  5892. me._onUpdate(params.items);
  5893. },
  5894. 'remove': function (event, params) {
  5895. me._onRemove(params.items);
  5896. }
  5897. };
  5898. // create HTML DOM
  5899. this._create();
  5900. }
  5901. GroupSet.prototype = new Panel();
  5902. /**
  5903. * Create the HTML DOM elements for the GroupSet
  5904. * @private
  5905. */
  5906. GroupSet.prototype._create = function _create () {
  5907. // TODO: reimplement groupSet DOM elements
  5908. var frame = document.createElement('div');
  5909. frame.className = 'groupset';
  5910. frame['timeline-groupset'] = this;
  5911. this.frame = frame;
  5912. this.labelSet = new Panel({
  5913. className: 'labelset',
  5914. width: '100%',
  5915. height: '100%'
  5916. });
  5917. this.labelPanel.appendChild(this.labelSet);
  5918. };
  5919. /**
  5920. * Get the frame element of component
  5921. * @returns {null} Get frame is not supported by GroupSet
  5922. */
  5923. GroupSet.prototype.getFrame = function getFrame() {
  5924. return this.frame;
  5925. };
  5926. /**
  5927. * Set options for the GroupSet. Existing options will be extended/overwritten.
  5928. * @param {Object} [options] The following options are available:
  5929. * {String | function} groupsOrder
  5930. * TODO: describe options
  5931. */
  5932. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  5933. /**
  5934. * Set range (start and end).
  5935. * @param {Range | Object} range A Range or an object containing start and end.
  5936. */
  5937. GroupSet.prototype.setRange = function (range) {
  5938. this.range = range;
  5939. for (var id in this.groups) {
  5940. if (this.groups.hasOwnProperty(id)) {
  5941. this.groups[id].setRange(range);
  5942. }
  5943. }
  5944. };
  5945. /**
  5946. * Set items
  5947. * @param {vis.DataSet | null} items
  5948. */
  5949. GroupSet.prototype.setItems = function setItems(items) {
  5950. this.itemsData = items;
  5951. for (var id in this.groups) {
  5952. if (this.groups.hasOwnProperty(id)) {
  5953. var group = this.groups[id];
  5954. // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
  5955. group.setItems(items);
  5956. }
  5957. }
  5958. };
  5959. /**
  5960. * Get items
  5961. * @return {vis.DataSet | null} items
  5962. */
  5963. GroupSet.prototype.getItems = function getItems() {
  5964. return this.itemsData;
  5965. };
  5966. /**
  5967. * Set range (start and end).
  5968. * @param {Range | Object} range A Range or an object containing start and end.
  5969. */
  5970. GroupSet.prototype.setRange = function setRange(range) {
  5971. this.range = range;
  5972. };
  5973. /**
  5974. * Set groups
  5975. * @param {vis.DataSet} groups
  5976. */
  5977. GroupSet.prototype.setGroups = function setGroups(groups) {
  5978. var me = this,
  5979. ids;
  5980. // unsubscribe from current dataset
  5981. if (this.groupsData) {
  5982. util.forEach(this.listeners, function (callback, event) {
  5983. me.groupsData.unsubscribe(event, callback);
  5984. });
  5985. // remove all drawn groups
  5986. ids = this.groupsData.getIds();
  5987. this._onRemove(ids);
  5988. }
  5989. // replace the dataset
  5990. if (!groups) {
  5991. this.groupsData = null;
  5992. }
  5993. else if (groups instanceof DataSet) {
  5994. this.groupsData = groups;
  5995. }
  5996. else {
  5997. this.groupsData = new DataSet({
  5998. convert: {
  5999. start: 'Date',
  6000. end: 'Date'
  6001. }
  6002. });
  6003. this.groupsData.add(groups);
  6004. }
  6005. if (this.groupsData) {
  6006. // subscribe to new dataset
  6007. var id = this.id;
  6008. util.forEach(this.listeners, function (callback, event) {
  6009. me.groupsData.on(event, callback, id);
  6010. });
  6011. // draw all new groups
  6012. ids = this.groupsData.getIds();
  6013. this._onAdd(ids);
  6014. }
  6015. this.emit('change');
  6016. };
  6017. /**
  6018. * Get groups
  6019. * @return {vis.DataSet | null} groups
  6020. */
  6021. GroupSet.prototype.getGroups = function getGroups() {
  6022. return this.groupsData;
  6023. };
  6024. /**
  6025. * Set selected items by their id. Replaces the current selection.
  6026. * Unknown id's are silently ignored.
  6027. * @param {Array} [ids] An array with zero or more id's of the items to be
  6028. * selected. If ids is an empty array, all items will be
  6029. * unselected.
  6030. */
  6031. GroupSet.prototype.setSelection = function setSelection(ids) {
  6032. var selection = [],
  6033. groups = this.groups;
  6034. // iterate over each of the groups
  6035. for (var id in groups) {
  6036. if (groups.hasOwnProperty(id)) {
  6037. var group = groups[id];
  6038. group.setSelection(ids);
  6039. }
  6040. }
  6041. return selection;
  6042. };
  6043. /**
  6044. * Get the selected items by their id
  6045. * @return {Array} ids The ids of the selected items
  6046. */
  6047. GroupSet.prototype.getSelection = function getSelection() {
  6048. var selection = [],
  6049. groups = this.groups;
  6050. // iterate over each of the groups
  6051. for (var id in groups) {
  6052. if (groups.hasOwnProperty(id)) {
  6053. var group = groups[id];
  6054. selection = selection.concat(group.getSelection());
  6055. }
  6056. }
  6057. return selection;
  6058. };
  6059. /**
  6060. * Repaint the component
  6061. * @return {boolean} Returns true if the component was resized since previous repaint
  6062. */
  6063. GroupSet.prototype.repaint = function repaint() {
  6064. var i, id, group,
  6065. asSize = util.option.asSize,
  6066. asString = util.option.asString,
  6067. options = this.options,
  6068. orientation = this.getOption('orientation'),
  6069. frame = this.frame,
  6070. resized = false,
  6071. groups = this.groups;
  6072. // repaint all groups in order
  6073. this.groupIds.forEach(function (id) {
  6074. var groupResized = groups[id].repaint();
  6075. resized = resized || groupResized;
  6076. });
  6077. // reposition the labels and calculate the maximum label width
  6078. var maxWidth = 0;
  6079. for (id in groups) {
  6080. if (groups.hasOwnProperty(id)) {
  6081. group = groups[id];
  6082. maxWidth = Math.max(maxWidth, group.props.label.width);
  6083. }
  6084. }
  6085. resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
  6086. // recalculate the height of the groupset, and recalculate top positions of the groups
  6087. var fixedHeight = (asSize(options.height) != null);
  6088. var height;
  6089. if (!fixedHeight) {
  6090. // height is not specified, calculate the sum of the height of all groups
  6091. height = 0;
  6092. this.groupIds.forEach(function (id) {
  6093. var group = groups[id];
  6094. group.top = height;
  6095. if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
  6096. height += group.height;
  6097. });
  6098. }
  6099. // update classname
  6100. frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
  6101. // calculate actual size and position
  6102. this.top = frame.offsetTop;
  6103. this.left = frame.offsetLeft;
  6104. this.width = frame.offsetWidth;
  6105. this.height = height;
  6106. return resized;
  6107. };
  6108. /**
  6109. * Update the groupIds. Requires a repaint afterwards
  6110. * @private
  6111. */
  6112. GroupSet.prototype._updateGroupIds = function () {
  6113. // reorder the groups
  6114. this.groupIds = this.groupsData.getIds({
  6115. order: this.options.groupOrder
  6116. });
  6117. // hide the groups now, they will be shown again in the next repaint
  6118. // in correct order
  6119. var groups = this.groups;
  6120. this.groupIds.forEach(function (id) {
  6121. groups[id].hide();
  6122. });
  6123. };
  6124. /**
  6125. * Get the width of the group labels
  6126. * @return {Number} width
  6127. */
  6128. GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
  6129. return this.props.labels.width;
  6130. };
  6131. /**
  6132. * Hide the component from the DOM
  6133. */
  6134. GroupSet.prototype.hide = function hide() {
  6135. // hide labelset
  6136. this.labelPanel.removeChild(this.labelSet);
  6137. // hide each of the groups
  6138. for (var groupId in this.groups) {
  6139. if (this.groups.hasOwnProperty(groupId)) {
  6140. this.groups[groupId].hide();
  6141. }
  6142. }
  6143. };
  6144. /**
  6145. * Show the component in the DOM (when not already visible).
  6146. * @return {Boolean} changed
  6147. */
  6148. GroupSet.prototype.show = function show() {
  6149. // show label set
  6150. if (!this.labelPanel.hasChild(this.labelSet)) {
  6151. this.labelPanel.removeChild(this.labelSet);
  6152. }
  6153. // show each of the groups
  6154. for (var groupId in this.groups) {
  6155. if (this.groups.hasOwnProperty(groupId)) {
  6156. this.groups[groupId].show();
  6157. }
  6158. }
  6159. };
  6160. /**
  6161. * Handle updated groups
  6162. * @param {Number[]} ids
  6163. * @private
  6164. */
  6165. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6166. this._onAdd(ids);
  6167. };
  6168. /**
  6169. * Handle changed groups
  6170. * @param {Number[]} ids
  6171. * @private
  6172. */
  6173. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6174. var me = this;
  6175. ids.forEach(function (id) {
  6176. var group = me.groups[id];
  6177. if (!group) {
  6178. var groupOptions = Object.create(me.options);
  6179. util.extend(groupOptions, {
  6180. height: null
  6181. });
  6182. group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
  6183. group.on('change', me.emit.bind(me, 'change')); // propagate change event
  6184. group.setRange(me.range);
  6185. group.setItems(me.itemsData); // attach items data
  6186. me.groups[id] = group;
  6187. group.parent = me;
  6188. }
  6189. // update group data
  6190. group.setData(me.groupsData.get(id));
  6191. });
  6192. this._updateGroupIds();
  6193. this.emit('change');
  6194. };
  6195. /**
  6196. * Handle removed groups
  6197. * @param {Number[]} ids
  6198. * @private
  6199. */
  6200. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6201. var groups = this.groups;
  6202. ids.forEach(function (id) {
  6203. var group = groups[id];
  6204. if (group) {
  6205. group.setItems(); // detach items data
  6206. group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
  6207. delete groups[id];
  6208. }
  6209. });
  6210. this._updateGroupIds();
  6211. this.emit('change');
  6212. };
  6213. /**
  6214. * Find the GroupSet from an event target:
  6215. * searches for the attribute 'timeline-groupset' in the event target's element
  6216. * tree, then finds the right group in this groupset
  6217. * @param {Event} event
  6218. * @return {Group | null} group
  6219. */
  6220. GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
  6221. var target = event.target;
  6222. while (target) {
  6223. if (target.hasOwnProperty('timeline-groupset')) {
  6224. return target['timeline-groupset'];
  6225. }
  6226. target = target.parentNode;
  6227. }
  6228. return null;
  6229. };
  6230. /**
  6231. * Find the Group from an event target:
  6232. * searches for the two elements having attributes 'timeline-groupset' and
  6233. * 'timeline-itemset' in the event target's element, then finds the right group.
  6234. * @param {Event} event
  6235. * @return {Group | null} group
  6236. */
  6237. GroupSet.groupFromTarget = function groupFromTarget (event) {
  6238. // find the groupSet
  6239. var groupSet = GroupSet.groupSetFromTarget(event);
  6240. // find the ItemSet
  6241. var itemSet = ItemSet.itemSetFromTarget(event);
  6242. // find the right group
  6243. if (groupSet && itemSet) {
  6244. for (var groupId in groupSet.groups) {
  6245. if (groupSet.groups.hasOwnProperty(groupId)) {
  6246. var group = groupSet.groups[groupId];
  6247. if (group.itemSet == itemSet) {
  6248. return group;
  6249. }
  6250. }
  6251. }
  6252. }
  6253. return null;
  6254. };
  6255. /**
  6256. * Create a timeline visualization
  6257. * @param {HTMLElement} container
  6258. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  6259. * @param {Object} [options] See Timeline.setOptions for the available options.
  6260. * @constructor
  6261. */
  6262. function Timeline (container, items, options) {
  6263. // validate arguments
  6264. if (!container) throw new Error('No container element provided');
  6265. var me = this;
  6266. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6267. this.options = {
  6268. orientation: 'bottom',
  6269. direction: 'horizontal', // 'horizontal' or 'vertical'
  6270. autoResize: true,
  6271. editable: false,
  6272. selectable: true,
  6273. snap: null, // will be specified after timeaxis is created
  6274. min: null,
  6275. max: null,
  6276. zoomMin: 10, // milliseconds
  6277. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6278. // moveable: true, // TODO: option moveable
  6279. // zoomable: true, // TODO: option zoomable
  6280. showMinorLabels: true,
  6281. showMajorLabels: true,
  6282. showCurrentTime: false,
  6283. showCustomTime: false,
  6284. type: 'box',
  6285. align: 'center',
  6286. margin: {
  6287. axis: 20,
  6288. item: 10
  6289. },
  6290. padding: 5,
  6291. onAdd: function (item, callback) {
  6292. callback(item);
  6293. },
  6294. onUpdate: function (item, callback) {
  6295. callback(item);
  6296. },
  6297. onMove: function (item, callback) {
  6298. callback(item);
  6299. },
  6300. onRemove: function (item, callback) {
  6301. callback(item);
  6302. },
  6303. toScreen: me._toScreen.bind(me),
  6304. toTime: me._toTime.bind(me)
  6305. };
  6306. // root panel
  6307. var rootOptions = util.extend(Object.create(this.options), {
  6308. height: function () {
  6309. if (me.options.height) {
  6310. // fixed height
  6311. return me.options.height;
  6312. }
  6313. else {
  6314. // auto height
  6315. // TODO: implement a css based solution to automatically have the right hight
  6316. return (me.timeAxis.height + me.contentPanel.height) + 'px';
  6317. }
  6318. }
  6319. });
  6320. this.rootPanel = new RootPanel(container, rootOptions);
  6321. // single select (or unselect) when tapping an item
  6322. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  6323. // multi select when holding mouse/touch, or on ctrl+click
  6324. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  6325. // add item on doubletap
  6326. this.rootPanel.on('doubletap', this._onAddItem.bind(this));
  6327. // side panel
  6328. var sideOptions = util.extend(Object.create(this.options), {
  6329. top: function () {
  6330. return (sideOptions.orientation == 'top') ? '0' : '';
  6331. },
  6332. bottom: function () {
  6333. return (sideOptions.orientation == 'top') ? '' : '0';
  6334. },
  6335. left: '0',
  6336. right: null,
  6337. height: '100%',
  6338. width: function () {
  6339. if (me.groupSet) {
  6340. return me.groupSet.getLabelsWidth();
  6341. }
  6342. else {
  6343. return 0;
  6344. }
  6345. },
  6346. className: function () {
  6347. return 'side' + (me.groupsData ? '' : ' hidden');
  6348. }
  6349. });
  6350. this.sidePanel = new Panel(sideOptions);
  6351. this.rootPanel.appendChild(this.sidePanel);
  6352. // main panel (contains time axis and itemsets)
  6353. var mainOptions = util.extend(Object.create(this.options), {
  6354. left: function () {
  6355. // we align left to enable a smooth resizing of the window
  6356. return me.sidePanel.width;
  6357. },
  6358. right: null,
  6359. height: '100%',
  6360. width: function () {
  6361. return me.rootPanel.width - me.sidePanel.width;
  6362. },
  6363. className: 'main'
  6364. });
  6365. this.mainPanel = new Panel(mainOptions);
  6366. this.rootPanel.appendChild(this.mainPanel);
  6367. // range
  6368. // TODO: move range inside rootPanel?
  6369. var rangeOptions = Object.create(this.options);
  6370. this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
  6371. this.range.setRange(
  6372. now.clone().add('days', -3).valueOf(),
  6373. now.clone().add('days', 4).valueOf()
  6374. );
  6375. this.range.on('rangechange', function (properties) {
  6376. me.rootPanel.repaint();
  6377. me.emit('rangechange', properties);
  6378. });
  6379. this.range.on('rangechanged', function (properties) {
  6380. me.rootPanel.repaint();
  6381. me.emit('rangechanged', properties);
  6382. });
  6383. // panel with time axis
  6384. var timeAxisOptions = util.extend(Object.create(rootOptions), {
  6385. range: this.range,
  6386. left: null,
  6387. top: null,
  6388. width: null,
  6389. height: null
  6390. });
  6391. this.timeAxis = new TimeAxis(timeAxisOptions);
  6392. this.timeAxis.setRange(this.range);
  6393. this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
  6394. this.mainPanel.appendChild(this.timeAxis);
  6395. // content panel (contains itemset(s))
  6396. var contentOptions = util.extend(Object.create(this.options), {
  6397. top: function () {
  6398. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6399. },
  6400. bottom: function () {
  6401. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6402. },
  6403. left: null,
  6404. right: null,
  6405. height: null,
  6406. width: null,
  6407. className: 'content'
  6408. });
  6409. this.contentPanel = new Panel(contentOptions);
  6410. this.mainPanel.appendChild(this.contentPanel);
  6411. // content panel (contains the vertical lines of box items)
  6412. var backgroundOptions = util.extend(Object.create(this.options), {
  6413. top: function () {
  6414. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6415. },
  6416. bottom: function () {
  6417. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6418. },
  6419. left: null,
  6420. right: null,
  6421. height: function () {
  6422. return me.contentPanel.height;
  6423. },
  6424. width: null,
  6425. className: 'background'
  6426. });
  6427. this.backgroundPanel = new Panel(backgroundOptions);
  6428. this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
  6429. // panel with axis holding the dots of item boxes
  6430. var axisPanelOptions = util.extend(Object.create(rootOptions), {
  6431. left: 0,
  6432. top: function () {
  6433. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6434. },
  6435. bottom: function () {
  6436. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6437. },
  6438. width: '100%',
  6439. height: 0,
  6440. className: 'axis'
  6441. });
  6442. this.axisPanel = new Panel(axisPanelOptions);
  6443. this.mainPanel.appendChild(this.axisPanel);
  6444. // content panel (contains itemset(s))
  6445. var sideContentOptions = util.extend(Object.create(this.options), {
  6446. top: function () {
  6447. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6448. },
  6449. bottom: function () {
  6450. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6451. },
  6452. left: null,
  6453. right: null,
  6454. height: null,
  6455. width: null,
  6456. className: 'side-content'
  6457. });
  6458. this.sideContentPanel = new Panel(sideContentOptions);
  6459. this.sidePanel.appendChild(this.sideContentPanel);
  6460. // current time bar
  6461. // Note: time bar will be attached in this.setOptions when selected
  6462. this.currentTime = new CurrentTime(this.range, rootOptions);
  6463. // custom time bar
  6464. // Note: time bar will be attached in this.setOptions when selected
  6465. this.customTime = new CustomTime(rootOptions);
  6466. this.customTime.on('timechange', function (time) {
  6467. me.emit('timechange', time);
  6468. });
  6469. this.customTime.on('timechanged', function (time) {
  6470. me.emit('timechanged', time);
  6471. });
  6472. this.itemSet = null;
  6473. this.groupSet = null;
  6474. // create groupset
  6475. this.setGroups(null);
  6476. this.itemsData = null; // DataSet
  6477. this.groupsData = null; // DataSet
  6478. // apply options
  6479. if (options) {
  6480. this.setOptions(options);
  6481. }
  6482. // create itemset and groupset
  6483. if (items) {
  6484. this.setItems(items);
  6485. }
  6486. }
  6487. // turn Timeline into an event emitter
  6488. Emitter(Timeline.prototype);
  6489. /**
  6490. * Set options
  6491. * @param {Object} options TODO: describe the available options
  6492. */
  6493. Timeline.prototype.setOptions = function (options) {
  6494. util.extend(this.options, options);
  6495. // force update of range (apply new min/max etc.)
  6496. // both start and end are optional
  6497. this.range.setRange(options.start, options.end);
  6498. if ('editable' in options || 'selectable' in options) {
  6499. if (this.options.selectable) {
  6500. // force update of selection
  6501. this.setSelection(this.getSelection());
  6502. }
  6503. else {
  6504. // remove selection
  6505. this.setSelection([]);
  6506. }
  6507. }
  6508. // validate the callback functions
  6509. var validateCallback = (function (fn) {
  6510. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  6511. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  6512. }
  6513. }).bind(this);
  6514. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  6515. // add/remove the current time bar
  6516. if (this.options.showCurrentTime) {
  6517. if (!this.mainPanel.hasChild(this.currentTime)) {
  6518. this.mainPanel.appendChild(this.currentTime);
  6519. this.currentTime.start();
  6520. }
  6521. }
  6522. else {
  6523. if (this.mainPanel.hasChild(this.currentTime)) {
  6524. this.currentTime.stop();
  6525. this.mainPanel.removeChild(this.currentTime);
  6526. }
  6527. }
  6528. // add/remove the custom time bar
  6529. if (this.options.showCustomTime) {
  6530. if (!this.mainPanel.hasChild(this.customTime)) {
  6531. this.mainPanel.appendChild(this.customTime);
  6532. }
  6533. }
  6534. else {
  6535. if (this.mainPanel.hasChild(this.customTime)) {
  6536. this.mainPanel.removeChild(this.customTime);
  6537. }
  6538. }
  6539. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  6540. if (options && options.order) {
  6541. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  6542. }
  6543. // repaint everything
  6544. this.rootPanel.repaint();
  6545. };
  6546. /**
  6547. * Set a custom time bar
  6548. * @param {Date} time
  6549. */
  6550. Timeline.prototype.setCustomTime = function (time) {
  6551. if (!this.customTime) {
  6552. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6553. }
  6554. this.customTime.setCustomTime(time);
  6555. };
  6556. /**
  6557. * Retrieve the current custom time.
  6558. * @return {Date} customTime
  6559. */
  6560. Timeline.prototype.getCustomTime = function() {
  6561. if (!this.customTime) {
  6562. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6563. }
  6564. return this.customTime.getCustomTime();
  6565. };
  6566. /**
  6567. * Set items
  6568. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  6569. */
  6570. Timeline.prototype.setItems = function(items) {
  6571. var initialLoad = (this.itemsData == null);
  6572. // convert to type DataSet when needed
  6573. var newDataSet;
  6574. if (!items) {
  6575. newDataSet = null;
  6576. }
  6577. else if (items instanceof DataSet) {
  6578. newDataSet = items;
  6579. }
  6580. if (!(items instanceof DataSet)) {
  6581. newDataSet = new DataSet({
  6582. convert: {
  6583. start: 'Date',
  6584. end: 'Date'
  6585. }
  6586. });
  6587. newDataSet.add(items);
  6588. }
  6589. // set items
  6590. this.itemsData = newDataSet;
  6591. (this.itemSet || this.groupSet).setItems(newDataSet);
  6592. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6593. // apply the data range as range
  6594. var dataRange = this.getItemRange();
  6595. // add 5% space on both sides
  6596. var start = dataRange.min;
  6597. var end = dataRange.max;
  6598. if (start != null && end != null) {
  6599. var interval = (end.valueOf() - start.valueOf());
  6600. if (interval <= 0) {
  6601. // prevent an empty interval
  6602. interval = 24 * 60 * 60 * 1000; // 1 day
  6603. }
  6604. start = new Date(start.valueOf() - interval * 0.05);
  6605. end = new Date(end.valueOf() + interval * 0.05);
  6606. }
  6607. // override specified start and/or end date
  6608. if (this.options.start != undefined) {
  6609. start = util.convert(this.options.start, 'Date');
  6610. }
  6611. if (this.options.end != undefined) {
  6612. end = util.convert(this.options.end, 'Date');
  6613. }
  6614. // skip range set if there is no start and end date
  6615. if (start === null && end === null) {
  6616. return;
  6617. }
  6618. // if start and end dates are set but cannot be satisfyed due to zoom restrictions — correct end date
  6619. if (start != null && end != null) {
  6620. var diff = end.valueOf() - start.valueOf();
  6621. if (this.options.zoomMax != undefined && this.options.zoomMax < diff) {
  6622. end = new Date(start.valueOf() + this.options.zoomMax);
  6623. }
  6624. if (this.options.zoomMin != undefined && this.options.zoomMin > diff) {
  6625. end = new Date(start.valueOf() + this.options.zoomMin);
  6626. }
  6627. }
  6628. this.range.setRange(start, end);
  6629. }
  6630. };
  6631. /**
  6632. * Set groups
  6633. * @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
  6634. */
  6635. Timeline.prototype.setGroups = function(groupSet) {
  6636. var me = this;
  6637. this.groupsData = groupSet;
  6638. // create options for the itemset or groupset
  6639. var options = util.extend(Object.create(this.options), {
  6640. top: null,
  6641. bottom: null,
  6642. right: null,
  6643. left: null,
  6644. width: null,
  6645. height: null
  6646. });
  6647. if (this.groupsData) {
  6648. // Create a GroupSet
  6649. // remove itemset if existing
  6650. if (this.itemSet) {
  6651. this.itemSet.hide(); // TODO: not so nice having to hide here
  6652. this.contentPanel.removeChild(this.itemSet);
  6653. this.itemSet.setItems(); // disconnect from itemset
  6654. this.itemSet = null;
  6655. }
  6656. // create new GroupSet when needed
  6657. if (!this.groupSet) {
  6658. this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options);
  6659. this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel));
  6660. this.groupSet.setRange(this.range);
  6661. this.groupSet.setItems(this.itemsData);
  6662. this.groupSet.setGroups(this.groupsData);
  6663. this.contentPanel.appendChild(this.groupSet);
  6664. }
  6665. else {
  6666. this.groupSet.setGroups(this.groupsData);
  6667. }
  6668. }
  6669. else {
  6670. // ItemSet
  6671. if (this.groupSet) {
  6672. this.groupSet.hide(); // TODO: not so nice having to hide here
  6673. //this.groupSet.setGroups(); // disconnect from groupset
  6674. this.groupSet.setItems(); // disconnect from itemset
  6675. this.contentPanel.removeChild(this.groupSet);
  6676. this.groupSet = null;
  6677. }
  6678. // create new items
  6679. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options);
  6680. this.itemSet.setRange(this.range);
  6681. this.itemSet.setItems(this.itemsData);
  6682. this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  6683. this.contentPanel.appendChild(this.itemSet);
  6684. }
  6685. };
  6686. /**
  6687. * Get the data range of the item set.
  6688. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6689. * When no minimum is found, min==null
  6690. * When no maximum is found, max==null
  6691. */
  6692. Timeline.prototype.getItemRange = function getItemRange() {
  6693. // calculate min from start filed
  6694. var itemsData = this.itemsData,
  6695. min = null,
  6696. max = null;
  6697. if (itemsData) {
  6698. // calculate the minimum value of the field 'start'
  6699. var minItem = itemsData.min('start');
  6700. min = minItem ? minItem.start.valueOf() : null;
  6701. // calculate maximum value of fields 'start' and 'end'
  6702. var maxStartItem = itemsData.max('start');
  6703. if (maxStartItem) {
  6704. max = maxStartItem.start.valueOf();
  6705. }
  6706. var maxEndItem = itemsData.max('end');
  6707. if (maxEndItem) {
  6708. if (max == null) {
  6709. max = maxEndItem.end.valueOf();
  6710. }
  6711. else {
  6712. max = Math.max(max, maxEndItem.end.valueOf());
  6713. }
  6714. }
  6715. }
  6716. return {
  6717. min: (min != null) ? new Date(min) : null,
  6718. max: (max != null) ? new Date(max) : null
  6719. };
  6720. };
  6721. /**
  6722. * Set selected items by their id. Replaces the current selection
  6723. * Unknown id's are silently ignored.
  6724. * @param {Array} [ids] An array with zero or more id's of the items to be
  6725. * selected. If ids is an empty array, all items will be
  6726. * unselected.
  6727. */
  6728. Timeline.prototype.setSelection = function setSelection (ids) {
  6729. var itemOrGroupSet = (this.itemSet || this.groupSet);
  6730. if (itemOrGroupSet) itemOrGroupSet.setSelection(ids);
  6731. };
  6732. /**
  6733. * Get the selected items by their id
  6734. * @return {Array} ids The ids of the selected items
  6735. */
  6736. Timeline.prototype.getSelection = function getSelection() {
  6737. var itemOrGroupSet = (this.itemSet || this.groupSet);
  6738. return itemOrGroupSet ? itemOrGroupSet.getSelection() : [];
  6739. };
  6740. /**
  6741. * Set the visible window. Both parameters are optional, you can change only
  6742. * start or only end. Syntax:
  6743. *
  6744. * TimeLine.setWindow(start, end)
  6745. * TimeLine.setWindow(range)
  6746. *
  6747. * Where start and end can be a Date, number, or string, and range is an
  6748. * object with properties start and end.
  6749. *
  6750. * @param {Date | Number | String} [start] Start date of visible window
  6751. * @param {Date | Number | String} [end] End date of visible window
  6752. */
  6753. Timeline.prototype.setWindow = function setWindow(start, end) {
  6754. if (arguments.length == 1) {
  6755. var range = arguments[0];
  6756. this.range.setRange(range.start, range.end);
  6757. }
  6758. else {
  6759. this.range.setRange(start, end);
  6760. }
  6761. };
  6762. /**
  6763. * Get the visible window
  6764. * @return {{start: Date, end: Date}} Visible range
  6765. */
  6766. Timeline.prototype.getWindow = function setWindow() {
  6767. var range = this.range.getRange();
  6768. return {
  6769. start: new Date(range.start),
  6770. end: new Date(range.end)
  6771. };
  6772. };
  6773. /**
  6774. * Handle selecting/deselecting an item when tapping it
  6775. * @param {Event} event
  6776. * @private
  6777. */
  6778. // TODO: move this function to ItemSet
  6779. Timeline.prototype._onSelectItem = function (event) {
  6780. if (!this.options.selectable) return;
  6781. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  6782. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  6783. if (ctrlKey || shiftKey) {
  6784. this._onMultiSelectItem(event);
  6785. return;
  6786. }
  6787. var oldSelection = this.getSelection();
  6788. var item = ItemSet.itemFromTarget(event);
  6789. var selection = item ? [item.id] : [];
  6790. this.setSelection(selection);
  6791. var newSelection = this.getSelection();
  6792. // if selection is changed, emit a select event
  6793. if (!util.equalArray(oldSelection, newSelection)) {
  6794. this.emit('select', {
  6795. items: this.getSelection()
  6796. });
  6797. }
  6798. event.stopPropagation();
  6799. };
  6800. /**
  6801. * Handle creation and updates of an item on double tap
  6802. * @param event
  6803. * @private
  6804. */
  6805. Timeline.prototype._onAddItem = function (event) {
  6806. if (!this.options.selectable) return;
  6807. if (!this.options.editable) return;
  6808. var me = this,
  6809. item = ItemSet.itemFromTarget(event);
  6810. if (item) {
  6811. // update item
  6812. // execute async handler to update the item (or cancel it)
  6813. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  6814. this.options.onUpdate(itemData, function (itemData) {
  6815. if (itemData) {
  6816. me.itemsData.update(itemData);
  6817. }
  6818. });
  6819. }
  6820. else {
  6821. // add item
  6822. var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
  6823. var x = event.gesture.center.pageX - xAbs;
  6824. var newItem = {
  6825. start: this.timeAxis.snap(this._toTime(x)),
  6826. content: 'new item'
  6827. };
  6828. var id = util.randomUUID();
  6829. newItem[this.itemsData.fieldId] = id;
  6830. var group = GroupSet.groupFromTarget(event);
  6831. if (group) {
  6832. newItem.group = group.groupId;
  6833. }
  6834. // execute async handler to customize (or cancel) adding an item
  6835. this.options.onAdd(newItem, function (item) {
  6836. if (item) {
  6837. me.itemsData.add(newItem);
  6838. // TODO: need to trigger a repaint?
  6839. }
  6840. });
  6841. }
  6842. };
  6843. /**
  6844. * Handle selecting/deselecting multiple items when holding an item
  6845. * @param {Event} event
  6846. * @private
  6847. */
  6848. // TODO: move this function to ItemSet
  6849. Timeline.prototype._onMultiSelectItem = function (event) {
  6850. if (!this.options.selectable) return;
  6851. var selection,
  6852. item = ItemSet.itemFromTarget(event);
  6853. if (item) {
  6854. // multi select items
  6855. selection = this.getSelection(); // current selection
  6856. var index = selection.indexOf(item.id);
  6857. if (index == -1) {
  6858. // item is not yet selected -> select it
  6859. selection.push(item.id);
  6860. }
  6861. else {
  6862. // item is already selected -> deselect it
  6863. selection.splice(index, 1);
  6864. }
  6865. this.setSelection(selection);
  6866. this.emit('select', {
  6867. items: this.getSelection()
  6868. });
  6869. event.stopPropagation();
  6870. }
  6871. };
  6872. /**
  6873. * Convert a position on screen (pixels) to a datetime
  6874. * @param {int} x Position on the screen in pixels
  6875. * @return {Date} time The datetime the corresponds with given position x
  6876. * @private
  6877. */
  6878. Timeline.prototype._toTime = function _toTime(x) {
  6879. var conversion = this.range.conversion(this.mainPanel.width);
  6880. return new Date(x / conversion.scale + conversion.offset);
  6881. };
  6882. /**
  6883. * Convert a datetime (Date object) into a position on the screen
  6884. * @param {Date} time A date
  6885. * @return {int} x The position on the screen in pixels which corresponds
  6886. * with the given date.
  6887. * @private
  6888. */
  6889. Timeline.prototype._toScreen = function _toScreen(time) {
  6890. var conversion = this.range.conversion(this.mainPanel.width);
  6891. return (time.valueOf() - conversion.offset) * conversion.scale;
  6892. };
  6893. (function(exports) {
  6894. /**
  6895. * Parse a text source containing data in DOT language into a JSON object.
  6896. * The object contains two lists: one with nodes and one with edges.
  6897. *
  6898. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6899. *
  6900. * @param {String} data Text containing a graph in DOT-notation
  6901. * @return {Object} graph An object containing two parameters:
  6902. * {Object[]} nodes
  6903. * {Object[]} edges
  6904. */
  6905. function parseDOT (data) {
  6906. dot = data;
  6907. return parseGraph();
  6908. }
  6909. // token types enumeration
  6910. var TOKENTYPE = {
  6911. NULL : 0,
  6912. DELIMITER : 1,
  6913. IDENTIFIER: 2,
  6914. UNKNOWN : 3
  6915. };
  6916. // map with all delimiters
  6917. var DELIMITERS = {
  6918. '{': true,
  6919. '}': true,
  6920. '[': true,
  6921. ']': true,
  6922. ';': true,
  6923. '=': true,
  6924. ',': true,
  6925. '->': true,
  6926. '--': true
  6927. };
  6928. var dot = ''; // current dot file
  6929. var index = 0; // current index in dot file
  6930. var c = ''; // current token character in expr
  6931. var token = ''; // current token
  6932. var tokenType = TOKENTYPE.NULL; // type of the token
  6933. /**
  6934. * Get the first character from the dot file.
  6935. * The character is stored into the char c. If the end of the dot file is
  6936. * reached, the function puts an empty string in c.
  6937. */
  6938. function first() {
  6939. index = 0;
  6940. c = dot.charAt(0);
  6941. }
  6942. /**
  6943. * Get the next character from the dot file.
  6944. * The character is stored into the char c. If the end of the dot file is
  6945. * reached, the function puts an empty string in c.
  6946. */
  6947. function next() {
  6948. index++;
  6949. c = dot.charAt(index);
  6950. }
  6951. /**
  6952. * Preview the next character from the dot file.
  6953. * @return {String} cNext
  6954. */
  6955. function nextPreview() {
  6956. return dot.charAt(index + 1);
  6957. }
  6958. /**
  6959. * Test whether given character is alphabetic or numeric
  6960. * @param {String} c
  6961. * @return {Boolean} isAlphaNumeric
  6962. */
  6963. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6964. function isAlphaNumeric(c) {
  6965. return regexAlphaNumeric.test(c);
  6966. }
  6967. /**
  6968. * Merge all properties of object b into object b
  6969. * @param {Object} a
  6970. * @param {Object} b
  6971. * @return {Object} a
  6972. */
  6973. function merge (a, b) {
  6974. if (!a) {
  6975. a = {};
  6976. }
  6977. if (b) {
  6978. for (var name in b) {
  6979. if (b.hasOwnProperty(name)) {
  6980. a[name] = b[name];
  6981. }
  6982. }
  6983. }
  6984. return a;
  6985. }
  6986. /**
  6987. * Set a value in an object, where the provided parameter name can be a
  6988. * path with nested parameters. For example:
  6989. *
  6990. * var obj = {a: 2};
  6991. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6992. *
  6993. * @param {Object} obj
  6994. * @param {String} path A parameter name or dot-separated parameter path,
  6995. * like "color.highlight.border".
  6996. * @param {*} value
  6997. */
  6998. function setValue(obj, path, value) {
  6999. var keys = path.split('.');
  7000. var o = obj;
  7001. while (keys.length) {
  7002. var key = keys.shift();
  7003. if (keys.length) {
  7004. // this isn't the end point
  7005. if (!o[key]) {
  7006. o[key] = {};
  7007. }
  7008. o = o[key];
  7009. }
  7010. else {
  7011. // this is the end point
  7012. o[key] = value;
  7013. }
  7014. }
  7015. }
  7016. /**
  7017. * Add a node to a graph object. If there is already a node with
  7018. * the same id, their attributes will be merged.
  7019. * @param {Object} graph
  7020. * @param {Object} node
  7021. */
  7022. function addNode(graph, node) {
  7023. var i, len;
  7024. var current = null;
  7025. // find root graph (in case of subgraph)
  7026. var graphs = [graph]; // list with all graphs from current graph to root graph
  7027. var root = graph;
  7028. while (root.parent) {
  7029. graphs.push(root.parent);
  7030. root = root.parent;
  7031. }
  7032. // find existing node (at root level) by its id
  7033. if (root.nodes) {
  7034. for (i = 0, len = root.nodes.length; i < len; i++) {
  7035. if (node.id === root.nodes[i].id) {
  7036. current = root.nodes[i];
  7037. break;
  7038. }
  7039. }
  7040. }
  7041. if (!current) {
  7042. // this is a new node
  7043. current = {
  7044. id: node.id
  7045. };
  7046. if (graph.node) {
  7047. // clone default attributes
  7048. current.attr = merge(current.attr, graph.node);
  7049. }
  7050. }
  7051. // add node to this (sub)graph and all its parent graphs
  7052. for (i = graphs.length - 1; i >= 0; i--) {
  7053. var g = graphs[i];
  7054. if (!g.nodes) {
  7055. g.nodes = [];
  7056. }
  7057. if (g.nodes.indexOf(current) == -1) {
  7058. g.nodes.push(current);
  7059. }
  7060. }
  7061. // merge attributes
  7062. if (node.attr) {
  7063. current.attr = merge(current.attr, node.attr);
  7064. }
  7065. }
  7066. /**
  7067. * Add an edge to a graph object
  7068. * @param {Object} graph
  7069. * @param {Object} edge
  7070. */
  7071. function addEdge(graph, edge) {
  7072. if (!graph.edges) {
  7073. graph.edges = [];
  7074. }
  7075. graph.edges.push(edge);
  7076. if (graph.edge) {
  7077. var attr = merge({}, graph.edge); // clone default attributes
  7078. edge.attr = merge(attr, edge.attr); // merge attributes
  7079. }
  7080. }
  7081. /**
  7082. * Create an edge to a graph object
  7083. * @param {Object} graph
  7084. * @param {String | Number | Object} from
  7085. * @param {String | Number | Object} to
  7086. * @param {String} type
  7087. * @param {Object | null} attr
  7088. * @return {Object} edge
  7089. */
  7090. function createEdge(graph, from, to, type, attr) {
  7091. var edge = {
  7092. from: from,
  7093. to: to,
  7094. type: type
  7095. };
  7096. if (graph.edge) {
  7097. edge.attr = merge({}, graph.edge); // clone default attributes
  7098. }
  7099. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7100. return edge;
  7101. }
  7102. /**
  7103. * Get next token in the current dot file.
  7104. * The token and token type are available as token and tokenType
  7105. */
  7106. function getToken() {
  7107. tokenType = TOKENTYPE.NULL;
  7108. token = '';
  7109. // skip over whitespaces
  7110. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7111. next();
  7112. }
  7113. do {
  7114. var isComment = false;
  7115. // skip comment
  7116. if (c == '#') {
  7117. // find the previous non-space character
  7118. var i = index - 1;
  7119. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7120. i--;
  7121. }
  7122. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7123. // the # is at the start of a line, this is indeed a line comment
  7124. while (c != '' && c != '\n') {
  7125. next();
  7126. }
  7127. isComment = true;
  7128. }
  7129. }
  7130. if (c == '/' && nextPreview() == '/') {
  7131. // skip line comment
  7132. while (c != '' && c != '\n') {
  7133. next();
  7134. }
  7135. isComment = true;
  7136. }
  7137. if (c == '/' && nextPreview() == '*') {
  7138. // skip block comment
  7139. while (c != '') {
  7140. if (c == '*' && nextPreview() == '/') {
  7141. // end of block comment found. skip these last two characters
  7142. next();
  7143. next();
  7144. break;
  7145. }
  7146. else {
  7147. next();
  7148. }
  7149. }
  7150. isComment = true;
  7151. }
  7152. // skip over whitespaces
  7153. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7154. next();
  7155. }
  7156. }
  7157. while (isComment);
  7158. // check for end of dot file
  7159. if (c == '') {
  7160. // token is still empty
  7161. tokenType = TOKENTYPE.DELIMITER;
  7162. return;
  7163. }
  7164. // check for delimiters consisting of 2 characters
  7165. var c2 = c + nextPreview();
  7166. if (DELIMITERS[c2]) {
  7167. tokenType = TOKENTYPE.DELIMITER;
  7168. token = c2;
  7169. next();
  7170. next();
  7171. return;
  7172. }
  7173. // check for delimiters consisting of 1 character
  7174. if (DELIMITERS[c]) {
  7175. tokenType = TOKENTYPE.DELIMITER;
  7176. token = c;
  7177. next();
  7178. return;
  7179. }
  7180. // check for an identifier (number or string)
  7181. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7182. if (isAlphaNumeric(c) || c == '-') {
  7183. token += c;
  7184. next();
  7185. while (isAlphaNumeric(c)) {
  7186. token += c;
  7187. next();
  7188. }
  7189. if (token == 'false') {
  7190. token = false; // convert to boolean
  7191. }
  7192. else if (token == 'true') {
  7193. token = true; // convert to boolean
  7194. }
  7195. else if (!isNaN(Number(token))) {
  7196. token = Number(token); // convert to number
  7197. }
  7198. tokenType = TOKENTYPE.IDENTIFIER;
  7199. return;
  7200. }
  7201. // check for a string enclosed by double quotes
  7202. if (c == '"') {
  7203. next();
  7204. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7205. token += c;
  7206. if (c == '"') { // skip the escape character
  7207. next();
  7208. }
  7209. next();
  7210. }
  7211. if (c != '"') {
  7212. throw newSyntaxError('End of string " expected');
  7213. }
  7214. next();
  7215. tokenType = TOKENTYPE.IDENTIFIER;
  7216. return;
  7217. }
  7218. // something unknown is found, wrong characters, a syntax error
  7219. tokenType = TOKENTYPE.UNKNOWN;
  7220. while (c != '') {
  7221. token += c;
  7222. next();
  7223. }
  7224. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7225. }
  7226. /**
  7227. * Parse a graph.
  7228. * @returns {Object} graph
  7229. */
  7230. function parseGraph() {
  7231. var graph = {};
  7232. first();
  7233. getToken();
  7234. // optional strict keyword
  7235. if (token == 'strict') {
  7236. graph.strict = true;
  7237. getToken();
  7238. }
  7239. // graph or digraph keyword
  7240. if (token == 'graph' || token == 'digraph') {
  7241. graph.type = token;
  7242. getToken();
  7243. }
  7244. // optional graph id
  7245. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7246. graph.id = token;
  7247. getToken();
  7248. }
  7249. // open angle bracket
  7250. if (token != '{') {
  7251. throw newSyntaxError('Angle bracket { expected');
  7252. }
  7253. getToken();
  7254. // statements
  7255. parseStatements(graph);
  7256. // close angle bracket
  7257. if (token != '}') {
  7258. throw newSyntaxError('Angle bracket } expected');
  7259. }
  7260. getToken();
  7261. // end of file
  7262. if (token !== '') {
  7263. throw newSyntaxError('End of file expected');
  7264. }
  7265. getToken();
  7266. // remove temporary default properties
  7267. delete graph.node;
  7268. delete graph.edge;
  7269. delete graph.graph;
  7270. return graph;
  7271. }
  7272. /**
  7273. * Parse a list with statements.
  7274. * @param {Object} graph
  7275. */
  7276. function parseStatements (graph) {
  7277. while (token !== '' && token != '}') {
  7278. parseStatement(graph);
  7279. if (token == ';') {
  7280. getToken();
  7281. }
  7282. }
  7283. }
  7284. /**
  7285. * Parse a single statement. Can be a an attribute statement, node
  7286. * statement, a series of node statements and edge statements, or a
  7287. * parameter.
  7288. * @param {Object} graph
  7289. */
  7290. function parseStatement(graph) {
  7291. // parse subgraph
  7292. var subgraph = parseSubgraph(graph);
  7293. if (subgraph) {
  7294. // edge statements
  7295. parseEdge(graph, subgraph);
  7296. return;
  7297. }
  7298. // parse an attribute statement
  7299. var attr = parseAttributeStatement(graph);
  7300. if (attr) {
  7301. return;
  7302. }
  7303. // parse node
  7304. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7305. throw newSyntaxError('Identifier expected');
  7306. }
  7307. var id = token; // id can be a string or a number
  7308. getToken();
  7309. if (token == '=') {
  7310. // id statement
  7311. getToken();
  7312. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7313. throw newSyntaxError('Identifier expected');
  7314. }
  7315. graph[id] = token;
  7316. getToken();
  7317. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7318. }
  7319. else {
  7320. parseNodeStatement(graph, id);
  7321. }
  7322. }
  7323. /**
  7324. * Parse a subgraph
  7325. * @param {Object} graph parent graph object
  7326. * @return {Object | null} subgraph
  7327. */
  7328. function parseSubgraph (graph) {
  7329. var subgraph = null;
  7330. // optional subgraph keyword
  7331. if (token == 'subgraph') {
  7332. subgraph = {};
  7333. subgraph.type = 'subgraph';
  7334. getToken();
  7335. // optional graph id
  7336. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7337. subgraph.id = token;
  7338. getToken();
  7339. }
  7340. }
  7341. // open angle bracket
  7342. if (token == '{') {
  7343. getToken();
  7344. if (!subgraph) {
  7345. subgraph = {};
  7346. }
  7347. subgraph.parent = graph;
  7348. subgraph.node = graph.node;
  7349. subgraph.edge = graph.edge;
  7350. subgraph.graph = graph.graph;
  7351. // statements
  7352. parseStatements(subgraph);
  7353. // close angle bracket
  7354. if (token != '}') {
  7355. throw newSyntaxError('Angle bracket } expected');
  7356. }
  7357. getToken();
  7358. // remove temporary default properties
  7359. delete subgraph.node;
  7360. delete subgraph.edge;
  7361. delete subgraph.graph;
  7362. delete subgraph.parent;
  7363. // register at the parent graph
  7364. if (!graph.subgraphs) {
  7365. graph.subgraphs = [];
  7366. }
  7367. graph.subgraphs.push(subgraph);
  7368. }
  7369. return subgraph;
  7370. }
  7371. /**
  7372. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7373. * Available keywords are 'node', 'edge', 'graph'.
  7374. * The previous list with default attributes will be replaced
  7375. * @param {Object} graph
  7376. * @returns {String | null} keyword Returns the name of the parsed attribute
  7377. * (node, edge, graph), or null if nothing
  7378. * is parsed.
  7379. */
  7380. function parseAttributeStatement (graph) {
  7381. // attribute statements
  7382. if (token == 'node') {
  7383. getToken();
  7384. // node attributes
  7385. graph.node = parseAttributeList();
  7386. return 'node';
  7387. }
  7388. else if (token == 'edge') {
  7389. getToken();
  7390. // edge attributes
  7391. graph.edge = parseAttributeList();
  7392. return 'edge';
  7393. }
  7394. else if (token == 'graph') {
  7395. getToken();
  7396. // graph attributes
  7397. graph.graph = parseAttributeList();
  7398. return 'graph';
  7399. }
  7400. return null;
  7401. }
  7402. /**
  7403. * parse a node statement
  7404. * @param {Object} graph
  7405. * @param {String | Number} id
  7406. */
  7407. function parseNodeStatement(graph, id) {
  7408. // node statement
  7409. var node = {
  7410. id: id
  7411. };
  7412. var attr = parseAttributeList();
  7413. if (attr) {
  7414. node.attr = attr;
  7415. }
  7416. addNode(graph, node);
  7417. // edge statements
  7418. parseEdge(graph, id);
  7419. }
  7420. /**
  7421. * Parse an edge or a series of edges
  7422. * @param {Object} graph
  7423. * @param {String | Number} from Id of the from node
  7424. */
  7425. function parseEdge(graph, from) {
  7426. while (token == '->' || token == '--') {
  7427. var to;
  7428. var type = token;
  7429. getToken();
  7430. var subgraph = parseSubgraph(graph);
  7431. if (subgraph) {
  7432. to = subgraph;
  7433. }
  7434. else {
  7435. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7436. throw newSyntaxError('Identifier or subgraph expected');
  7437. }
  7438. to = token;
  7439. addNode(graph, {
  7440. id: to
  7441. });
  7442. getToken();
  7443. }
  7444. // parse edge attributes
  7445. var attr = parseAttributeList();
  7446. // create edge
  7447. var edge = createEdge(graph, from, to, type, attr);
  7448. addEdge(graph, edge);
  7449. from = to;
  7450. }
  7451. }
  7452. /**
  7453. * Parse a set with attributes,
  7454. * for example [label="1.000", shape=solid]
  7455. * @return {Object | null} attr
  7456. */
  7457. function parseAttributeList() {
  7458. var attr = null;
  7459. while (token == '[') {
  7460. getToken();
  7461. attr = {};
  7462. while (token !== '' && token != ']') {
  7463. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7464. throw newSyntaxError('Attribute name expected');
  7465. }
  7466. var name = token;
  7467. getToken();
  7468. if (token != '=') {
  7469. throw newSyntaxError('Equal sign = expected');
  7470. }
  7471. getToken();
  7472. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7473. throw newSyntaxError('Attribute value expected');
  7474. }
  7475. var value = token;
  7476. setValue(attr, name, value); // name can be a path
  7477. getToken();
  7478. if (token ==',') {
  7479. getToken();
  7480. }
  7481. }
  7482. if (token != ']') {
  7483. throw newSyntaxError('Bracket ] expected');
  7484. }
  7485. getToken();
  7486. }
  7487. return attr;
  7488. }
  7489. /**
  7490. * Create a syntax error with extra information on current token and index.
  7491. * @param {String} message
  7492. * @returns {SyntaxError} err
  7493. */
  7494. function newSyntaxError(message) {
  7495. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7496. }
  7497. /**
  7498. * Chop off text after a maximum length
  7499. * @param {String} text
  7500. * @param {Number} maxLength
  7501. * @returns {String}
  7502. */
  7503. function chop (text, maxLength) {
  7504. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7505. }
  7506. /**
  7507. * Execute a function fn for each pair of elements in two arrays
  7508. * @param {Array | *} array1
  7509. * @param {Array | *} array2
  7510. * @param {function} fn
  7511. */
  7512. function forEach2(array1, array2, fn) {
  7513. if (array1 instanceof Array) {
  7514. array1.forEach(function (elem1) {
  7515. if (array2 instanceof Array) {
  7516. array2.forEach(function (elem2) {
  7517. fn(elem1, elem2);
  7518. });
  7519. }
  7520. else {
  7521. fn(elem1, array2);
  7522. }
  7523. });
  7524. }
  7525. else {
  7526. if (array2 instanceof Array) {
  7527. array2.forEach(function (elem2) {
  7528. fn(array1, elem2);
  7529. });
  7530. }
  7531. else {
  7532. fn(array1, array2);
  7533. }
  7534. }
  7535. }
  7536. /**
  7537. * Convert a string containing a graph in DOT language into a map containing
  7538. * with nodes and edges in the format of graph.
  7539. * @param {String} data Text containing a graph in DOT-notation
  7540. * @return {Object} graphData
  7541. */
  7542. function DOTToGraph (data) {
  7543. // parse the DOT file
  7544. var dotData = parseDOT(data);
  7545. var graphData = {
  7546. nodes: [],
  7547. edges: [],
  7548. options: {}
  7549. };
  7550. // copy the nodes
  7551. if (dotData.nodes) {
  7552. dotData.nodes.forEach(function (dotNode) {
  7553. var graphNode = {
  7554. id: dotNode.id,
  7555. label: String(dotNode.label || dotNode.id)
  7556. };
  7557. merge(graphNode, dotNode.attr);
  7558. if (graphNode.image) {
  7559. graphNode.shape = 'image';
  7560. }
  7561. graphData.nodes.push(graphNode);
  7562. });
  7563. }
  7564. // copy the edges
  7565. if (dotData.edges) {
  7566. /**
  7567. * Convert an edge in DOT format to an edge with VisGraph format
  7568. * @param {Object} dotEdge
  7569. * @returns {Object} graphEdge
  7570. */
  7571. function convertEdge(dotEdge) {
  7572. var graphEdge = {
  7573. from: dotEdge.from,
  7574. to: dotEdge.to
  7575. };
  7576. merge(graphEdge, dotEdge.attr);
  7577. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7578. return graphEdge;
  7579. }
  7580. dotData.edges.forEach(function (dotEdge) {
  7581. var from, to;
  7582. if (dotEdge.from instanceof Object) {
  7583. from = dotEdge.from.nodes;
  7584. }
  7585. else {
  7586. from = {
  7587. id: dotEdge.from
  7588. }
  7589. }
  7590. if (dotEdge.to instanceof Object) {
  7591. to = dotEdge.to.nodes;
  7592. }
  7593. else {
  7594. to = {
  7595. id: dotEdge.to
  7596. }
  7597. }
  7598. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7599. dotEdge.from.edges.forEach(function (subEdge) {
  7600. var graphEdge = convertEdge(subEdge);
  7601. graphData.edges.push(graphEdge);
  7602. });
  7603. }
  7604. forEach2(from, to, function (from, to) {
  7605. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7606. var graphEdge = convertEdge(subEdge);
  7607. graphData.edges.push(graphEdge);
  7608. });
  7609. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7610. dotEdge.to.edges.forEach(function (subEdge) {
  7611. var graphEdge = convertEdge(subEdge);
  7612. graphData.edges.push(graphEdge);
  7613. });
  7614. }
  7615. });
  7616. }
  7617. // copy the options
  7618. if (dotData.attr) {
  7619. graphData.options = dotData.attr;
  7620. }
  7621. return graphData;
  7622. }
  7623. // exports
  7624. exports.parseDOT = parseDOT;
  7625. exports.DOTToGraph = DOTToGraph;
  7626. })(typeof util !== 'undefined' ? util : exports);
  7627. /**
  7628. * Canvas shapes used by the Graph
  7629. */
  7630. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7631. /**
  7632. * Draw a circle shape
  7633. */
  7634. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7635. this.beginPath();
  7636. this.arc(x, y, r, 0, 2*Math.PI, false);
  7637. };
  7638. /**
  7639. * Draw a square shape
  7640. * @param {Number} x horizontal center
  7641. * @param {Number} y vertical center
  7642. * @param {Number} r size, width and height of the square
  7643. */
  7644. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7645. this.beginPath();
  7646. this.rect(x - r, y - r, r * 2, r * 2);
  7647. };
  7648. /**
  7649. * Draw a triangle shape
  7650. * @param {Number} x horizontal center
  7651. * @param {Number} y vertical center
  7652. * @param {Number} r radius, half the length of the sides of the triangle
  7653. */
  7654. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7655. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7656. this.beginPath();
  7657. var s = r * 2;
  7658. var s2 = s / 2;
  7659. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7660. var h = Math.sqrt(s * s - s2 * s2); // height
  7661. this.moveTo(x, y - (h - ir));
  7662. this.lineTo(x + s2, y + ir);
  7663. this.lineTo(x - s2, y + ir);
  7664. this.lineTo(x, y - (h - ir));
  7665. this.closePath();
  7666. };
  7667. /**
  7668. * Draw a triangle shape in downward orientation
  7669. * @param {Number} x horizontal center
  7670. * @param {Number} y vertical center
  7671. * @param {Number} r radius
  7672. */
  7673. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7674. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7675. this.beginPath();
  7676. var s = r * 2;
  7677. var s2 = s / 2;
  7678. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7679. var h = Math.sqrt(s * s - s2 * s2); // height
  7680. this.moveTo(x, y + (h - ir));
  7681. this.lineTo(x + s2, y - ir);
  7682. this.lineTo(x - s2, y - ir);
  7683. this.lineTo(x, y + (h - ir));
  7684. this.closePath();
  7685. };
  7686. /**
  7687. * Draw a star shape, a star with 5 points
  7688. * @param {Number} x horizontal center
  7689. * @param {Number} y vertical center
  7690. * @param {Number} r radius, half the length of the sides of the triangle
  7691. */
  7692. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7693. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7694. this.beginPath();
  7695. for (var n = 0; n < 10; n++) {
  7696. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7697. this.lineTo(
  7698. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7699. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7700. );
  7701. }
  7702. this.closePath();
  7703. };
  7704. /**
  7705. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7706. */
  7707. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7708. var r2d = Math.PI/180;
  7709. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7710. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7711. this.beginPath();
  7712. this.moveTo(x+r,y);
  7713. this.lineTo(x+w-r,y);
  7714. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7715. this.lineTo(x+w,y+h-r);
  7716. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7717. this.lineTo(x+r,y+h);
  7718. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7719. this.lineTo(x,y+r);
  7720. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7721. };
  7722. /**
  7723. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7724. */
  7725. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7726. var kappa = .5522848,
  7727. ox = (w / 2) * kappa, // control point offset horizontal
  7728. oy = (h / 2) * kappa, // control point offset vertical
  7729. xe = x + w, // x-end
  7730. ye = y + h, // y-end
  7731. xm = x + w / 2, // x-middle
  7732. ym = y + h / 2; // y-middle
  7733. this.beginPath();
  7734. this.moveTo(x, ym);
  7735. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7736. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7737. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7738. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7739. };
  7740. /**
  7741. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7742. */
  7743. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7744. var f = 1/3;
  7745. var wEllipse = w;
  7746. var hEllipse = h * f;
  7747. var kappa = .5522848,
  7748. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7749. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7750. xe = x + wEllipse, // x-end
  7751. ye = y + hEllipse, // y-end
  7752. xm = x + wEllipse / 2, // x-middle
  7753. ym = y + hEllipse / 2, // y-middle
  7754. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7755. yeb = y + h; // y-end, bottom ellipse
  7756. this.beginPath();
  7757. this.moveTo(xe, ym);
  7758. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7759. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7760. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7761. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7762. this.lineTo(xe, ymb);
  7763. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7764. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7765. this.lineTo(x, ym);
  7766. };
  7767. /**
  7768. * Draw an arrow point (no line)
  7769. */
  7770. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7771. // tail
  7772. var xt = x - length * Math.cos(angle);
  7773. var yt = y - length * Math.sin(angle);
  7774. // inner tail
  7775. // TODO: allow to customize different shapes
  7776. var xi = x - length * 0.9 * Math.cos(angle);
  7777. var yi = y - length * 0.9 * Math.sin(angle);
  7778. // left
  7779. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7780. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7781. // right
  7782. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7783. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7784. this.beginPath();
  7785. this.moveTo(x, y);
  7786. this.lineTo(xl, yl);
  7787. this.lineTo(xi, yi);
  7788. this.lineTo(xr, yr);
  7789. this.closePath();
  7790. };
  7791. /**
  7792. * Sets up the dashedLine functionality for drawing
  7793. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7794. * @author David Jordan
  7795. * @date 2012-08-08
  7796. */
  7797. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7798. if (!dashArray) dashArray=[10,5];
  7799. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7800. var dashCount = dashArray.length;
  7801. this.moveTo(x, y);
  7802. var dx = (x2-x), dy = (y2-y);
  7803. var slope = dy/dx;
  7804. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7805. var dashIndex=0, draw=true;
  7806. while (distRemaining>=0.1){
  7807. var dashLength = dashArray[dashIndex++%dashCount];
  7808. if (dashLength > distRemaining) dashLength = distRemaining;
  7809. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7810. if (dx<0) xStep = -xStep;
  7811. x += xStep;
  7812. y += slope*xStep;
  7813. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7814. distRemaining -= dashLength;
  7815. draw = !draw;
  7816. }
  7817. };
  7818. // TODO: add diamond shape
  7819. }
  7820. /**
  7821. * @class Node
  7822. * A node. A node can be connected to other nodes via one or multiple edges.
  7823. * @param {object} properties An object containing properties for the node. All
  7824. * properties are optional, except for the id.
  7825. * {number} id Id of the node. Required
  7826. * {string} label Text label for the node
  7827. * {number} x Horizontal position of the node
  7828. * {number} y Vertical position of the node
  7829. * {string} shape Node shape, available:
  7830. * "database", "circle", "ellipse",
  7831. * "box", "image", "text", "dot",
  7832. * "star", "triangle", "triangleDown",
  7833. * "square"
  7834. * {string} image An image url
  7835. * {string} title An title text, can be HTML
  7836. * {anytype} group A group name or number
  7837. * @param {Graph.Images} imagelist A list with images. Only needed
  7838. * when the node has an image
  7839. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7840. * retrieving group properties
  7841. * @param {Object} constants An object with default values for
  7842. * example for the color
  7843. *
  7844. */
  7845. function Node(properties, imagelist, grouplist, constants) {
  7846. this.selected = false;
  7847. this.edges = []; // all edges connected to this node
  7848. this.dynamicEdges = [];
  7849. this.reroutedEdges = {};
  7850. this.group = constants.nodes.group;
  7851. this.fontSize = constants.nodes.fontSize;
  7852. this.fontFace = constants.nodes.fontFace;
  7853. this.fontColor = constants.nodes.fontColor;
  7854. this.fontDrawThreshold = 3;
  7855. this.color = constants.nodes.color;
  7856. // set defaults for the properties
  7857. this.id = undefined;
  7858. this.shape = constants.nodes.shape;
  7859. this.image = constants.nodes.image;
  7860. this.x = null;
  7861. this.y = null;
  7862. this.xFixed = false;
  7863. this.yFixed = false;
  7864. this.horizontalAlignLeft = true; // these are for the navigation controls
  7865. this.verticalAlignTop = true; // these are for the navigation controls
  7866. this.radius = constants.nodes.radius;
  7867. this.baseRadiusValue = constants.nodes.radius;
  7868. this.radiusFixed = false;
  7869. this.radiusMin = constants.nodes.radiusMin;
  7870. this.radiusMax = constants.nodes.radiusMax;
  7871. this.level = -1;
  7872. this.preassignedLevel = false;
  7873. this.imagelist = imagelist;
  7874. this.grouplist = grouplist;
  7875. // physics properties
  7876. this.fx = 0.0; // external force x
  7877. this.fy = 0.0; // external force y
  7878. this.vx = 0.0; // velocity x
  7879. this.vy = 0.0; // velocity y
  7880. this.minForce = constants.minForce;
  7881. this.damping = constants.physics.damping;
  7882. this.mass = 1; // kg
  7883. this.fixedData = {x:null,y:null};
  7884. this.setProperties(properties, constants);
  7885. // creating the variables for clustering
  7886. this.resetCluster();
  7887. this.dynamicEdgesLength = 0;
  7888. this.clusterSession = 0;
  7889. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  7890. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  7891. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  7892. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  7893. this.growthIndicator = 0;
  7894. // variables to tell the node about the graph.
  7895. this.graphScaleInv = 1;
  7896. this.graphScale = 1;
  7897. this.canvasTopLeft = {"x": -300, "y": -300};
  7898. this.canvasBottomRight = {"x": 300, "y": 300};
  7899. this.parentEdgeId = null;
  7900. }
  7901. /**
  7902. * (re)setting the clustering variables and objects
  7903. */
  7904. Node.prototype.resetCluster = function() {
  7905. // clustering variables
  7906. this.formationScale = undefined; // this is used to determine when to open the cluster
  7907. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  7908. this.containedNodes = {};
  7909. this.containedEdges = {};
  7910. this.clusterSessions = [];
  7911. };
  7912. /**
  7913. * Attach a edge to the node
  7914. * @param {Edge} edge
  7915. */
  7916. Node.prototype.attachEdge = function(edge) {
  7917. if (this.edges.indexOf(edge) == -1) {
  7918. this.edges.push(edge);
  7919. }
  7920. if (this.dynamicEdges.indexOf(edge) == -1) {
  7921. this.dynamicEdges.push(edge);
  7922. }
  7923. this.dynamicEdgesLength = this.dynamicEdges.length;
  7924. };
  7925. /**
  7926. * Detach a edge from the node
  7927. * @param {Edge} edge
  7928. */
  7929. Node.prototype.detachEdge = function(edge) {
  7930. var index = this.edges.indexOf(edge);
  7931. if (index != -1) {
  7932. this.edges.splice(index, 1);
  7933. this.dynamicEdges.splice(index, 1);
  7934. }
  7935. this.dynamicEdgesLength = this.dynamicEdges.length;
  7936. };
  7937. /**
  7938. * Set or overwrite properties for the node
  7939. * @param {Object} properties an object with properties
  7940. * @param {Object} constants and object with default, global properties
  7941. */
  7942. Node.prototype.setProperties = function(properties, constants) {
  7943. if (!properties) {
  7944. return;
  7945. }
  7946. this.originalLabel = undefined;
  7947. // basic properties
  7948. if (properties.id !== undefined) {this.id = properties.id;}
  7949. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  7950. if (properties.title !== undefined) {this.title = properties.title;}
  7951. if (properties.group !== undefined) {this.group = properties.group;}
  7952. if (properties.x !== undefined) {this.x = properties.x;}
  7953. if (properties.y !== undefined) {this.y = properties.y;}
  7954. if (properties.value !== undefined) {this.value = properties.value;}
  7955. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  7956. // physics
  7957. if (properties.mass !== undefined) {this.mass = properties.mass;}
  7958. // navigation controls properties
  7959. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  7960. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  7961. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  7962. if (this.id === undefined) {
  7963. throw "Node must have an id";
  7964. }
  7965. // copy group properties
  7966. if (this.group) {
  7967. var groupObj = this.grouplist.get(this.group);
  7968. for (var prop in groupObj) {
  7969. if (groupObj.hasOwnProperty(prop)) {
  7970. this[prop] = groupObj[prop];
  7971. }
  7972. }
  7973. }
  7974. // individual shape properties
  7975. if (properties.shape !== undefined) {this.shape = properties.shape;}
  7976. if (properties.image !== undefined) {this.image = properties.image;}
  7977. if (properties.radius !== undefined) {this.radius = properties.radius;}
  7978. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  7979. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  7980. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  7981. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  7982. if (this.image !== undefined && this.image != "") {
  7983. if (this.imagelist) {
  7984. this.imageObj = this.imagelist.load(this.image);
  7985. }
  7986. else {
  7987. throw "No imagelist provided";
  7988. }
  7989. }
  7990. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  7991. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  7992. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  7993. if (this.shape == 'image') {
  7994. this.radiusMin = constants.nodes.widthMin;
  7995. this.radiusMax = constants.nodes.widthMax;
  7996. }
  7997. // choose draw method depending on the shape
  7998. switch (this.shape) {
  7999. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8000. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8001. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8002. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8003. // TODO: add diamond shape
  8004. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8005. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8006. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8007. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8008. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8009. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8010. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8011. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8012. }
  8013. // reset the size of the node, this can be changed
  8014. this._reset();
  8015. };
  8016. /**
  8017. * select this node
  8018. */
  8019. Node.prototype.select = function() {
  8020. this.selected = true;
  8021. this._reset();
  8022. };
  8023. /**
  8024. * unselect this node
  8025. */
  8026. Node.prototype.unselect = function() {
  8027. this.selected = false;
  8028. this._reset();
  8029. };
  8030. /**
  8031. * Reset the calculated size of the node, forces it to recalculate its size
  8032. */
  8033. Node.prototype.clearSizeCache = function() {
  8034. this._reset();
  8035. };
  8036. /**
  8037. * Reset the calculated size of the node, forces it to recalculate its size
  8038. * @private
  8039. */
  8040. Node.prototype._reset = function() {
  8041. this.width = undefined;
  8042. this.height = undefined;
  8043. };
  8044. /**
  8045. * get the title of this node.
  8046. * @return {string} title The title of the node, or undefined when no title
  8047. * has been set.
  8048. */
  8049. Node.prototype.getTitle = function() {
  8050. return typeof this.title === "function" ? this.title() : this.title;
  8051. };
  8052. /**
  8053. * Calculate the distance to the border of the Node
  8054. * @param {CanvasRenderingContext2D} ctx
  8055. * @param {Number} angle Angle in radians
  8056. * @returns {number} distance Distance to the border in pixels
  8057. */
  8058. Node.prototype.distanceToBorder = function (ctx, angle) {
  8059. var borderWidth = 1;
  8060. if (!this.width) {
  8061. this.resize(ctx);
  8062. }
  8063. switch (this.shape) {
  8064. case 'circle':
  8065. case 'dot':
  8066. return this.radius + borderWidth;
  8067. case 'ellipse':
  8068. var a = this.width / 2;
  8069. var b = this.height / 2;
  8070. var w = (Math.sin(angle) * a);
  8071. var h = (Math.cos(angle) * b);
  8072. return a * b / Math.sqrt(w * w + h * h);
  8073. // TODO: implement distanceToBorder for database
  8074. // TODO: implement distanceToBorder for triangle
  8075. // TODO: implement distanceToBorder for triangleDown
  8076. case 'box':
  8077. case 'image':
  8078. case 'text':
  8079. default:
  8080. if (this.width) {
  8081. return Math.min(
  8082. Math.abs(this.width / 2 / Math.cos(angle)),
  8083. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8084. // TODO: reckon with border radius too in case of box
  8085. }
  8086. else {
  8087. return 0;
  8088. }
  8089. }
  8090. // TODO: implement calculation of distance to border for all shapes
  8091. };
  8092. /**
  8093. * Set forces acting on the node
  8094. * @param {number} fx Force in horizontal direction
  8095. * @param {number} fy Force in vertical direction
  8096. */
  8097. Node.prototype._setForce = function(fx, fy) {
  8098. this.fx = fx;
  8099. this.fy = fy;
  8100. };
  8101. /**
  8102. * Add forces acting on the node
  8103. * @param {number} fx Force in horizontal direction
  8104. * @param {number} fy Force in vertical direction
  8105. * @private
  8106. */
  8107. Node.prototype._addForce = function(fx, fy) {
  8108. this.fx += fx;
  8109. this.fy += fy;
  8110. };
  8111. /**
  8112. * Perform one discrete step for the node
  8113. * @param {number} interval Time interval in seconds
  8114. */
  8115. Node.prototype.discreteStep = function(interval) {
  8116. if (!this.xFixed) {
  8117. var dx = this.damping * this.vx; // damping force
  8118. var ax = (this.fx - dx) / this.mass; // acceleration
  8119. this.vx += ax * interval; // velocity
  8120. this.x += this.vx * interval; // position
  8121. }
  8122. if (!this.yFixed) {
  8123. var dy = this.damping * this.vy; // damping force
  8124. var ay = (this.fy - dy) / this.mass; // acceleration
  8125. this.vy += ay * interval; // velocity
  8126. this.y += this.vy * interval; // position
  8127. }
  8128. };
  8129. /**
  8130. * Perform one discrete step for the node
  8131. * @param {number} interval Time interval in seconds
  8132. * @param {number} maxVelocity The speed limit imposed on the velocity
  8133. */
  8134. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8135. if (!this.xFixed) {
  8136. var dx = this.damping * this.vx; // damping force
  8137. var ax = (this.fx - dx) / this.mass; // acceleration
  8138. this.vx += ax * interval; // velocity
  8139. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8140. this.x += this.vx * interval; // position
  8141. }
  8142. else {
  8143. this.fx = 0;
  8144. }
  8145. if (!this.yFixed) {
  8146. var dy = this.damping * this.vy; // damping force
  8147. var ay = (this.fy - dy) / this.mass; // acceleration
  8148. this.vy += ay * interval; // velocity
  8149. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8150. this.y += this.vy * interval; // position
  8151. }
  8152. else {
  8153. this.fy = 0;
  8154. }
  8155. };
  8156. /**
  8157. * Check if this node has a fixed x and y position
  8158. * @return {boolean} true if fixed, false if not
  8159. */
  8160. Node.prototype.isFixed = function() {
  8161. return (this.xFixed && this.yFixed);
  8162. };
  8163. /**
  8164. * Check if this node is moving
  8165. * @param {number} vmin the minimum velocity considered as "moving"
  8166. * @return {boolean} true if moving, false if it has no velocity
  8167. */
  8168. // TODO: replace this method with calculating the kinetic energy
  8169. Node.prototype.isMoving = function(vmin) {
  8170. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8171. };
  8172. /**
  8173. * check if this node is selecte
  8174. * @return {boolean} selected True if node is selected, else false
  8175. */
  8176. Node.prototype.isSelected = function() {
  8177. return this.selected;
  8178. };
  8179. /**
  8180. * Retrieve the value of the node. Can be undefined
  8181. * @return {Number} value
  8182. */
  8183. Node.prototype.getValue = function() {
  8184. return this.value;
  8185. };
  8186. /**
  8187. * Calculate the distance from the nodes location to the given location (x,y)
  8188. * @param {Number} x
  8189. * @param {Number} y
  8190. * @return {Number} value
  8191. */
  8192. Node.prototype.getDistance = function(x, y) {
  8193. var dx = this.x - x,
  8194. dy = this.y - y;
  8195. return Math.sqrt(dx * dx + dy * dy);
  8196. };
  8197. /**
  8198. * Adjust the value range of the node. The node will adjust it's radius
  8199. * based on its value.
  8200. * @param {Number} min
  8201. * @param {Number} max
  8202. */
  8203. Node.prototype.setValueRange = function(min, max) {
  8204. if (!this.radiusFixed && this.value !== undefined) {
  8205. if (max == min) {
  8206. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8207. }
  8208. else {
  8209. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8210. this.radius = (this.value - min) * scale + this.radiusMin;
  8211. }
  8212. }
  8213. this.baseRadiusValue = this.radius;
  8214. };
  8215. /**
  8216. * Draw this node in the given canvas
  8217. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8218. * @param {CanvasRenderingContext2D} ctx
  8219. */
  8220. Node.prototype.draw = function(ctx) {
  8221. throw "Draw method not initialized for node";
  8222. };
  8223. /**
  8224. * Recalculate the size of this node in the given canvas
  8225. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8226. * @param {CanvasRenderingContext2D} ctx
  8227. */
  8228. Node.prototype.resize = function(ctx) {
  8229. throw "Resize method not initialized for node";
  8230. };
  8231. /**
  8232. * Check if this object is overlapping with the provided object
  8233. * @param {Object} obj an object with parameters left, top, right, bottom
  8234. * @return {boolean} True if location is located on node
  8235. */
  8236. Node.prototype.isOverlappingWith = function(obj) {
  8237. return (this.left < obj.right &&
  8238. this.left + this.width > obj.left &&
  8239. this.top < obj.bottom &&
  8240. this.top + this.height > obj.top);
  8241. };
  8242. Node.prototype._resizeImage = function (ctx) {
  8243. // TODO: pre calculate the image size
  8244. if (!this.width || !this.height) { // undefined or 0
  8245. var width, height;
  8246. if (this.value) {
  8247. this.radius = this.baseRadiusValue;
  8248. var scale = this.imageObj.height / this.imageObj.width;
  8249. if (scale !== undefined) {
  8250. width = this.radius || this.imageObj.width;
  8251. height = this.radius * scale || this.imageObj.height;
  8252. }
  8253. else {
  8254. width = 0;
  8255. height = 0;
  8256. }
  8257. }
  8258. else {
  8259. width = this.imageObj.width;
  8260. height = this.imageObj.height;
  8261. }
  8262. this.width = width;
  8263. this.height = height;
  8264. this.growthIndicator = 0;
  8265. if (this.width > 0 && this.height > 0) {
  8266. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8267. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8268. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8269. this.growthIndicator = this.width - width;
  8270. }
  8271. }
  8272. };
  8273. Node.prototype._drawImage = function (ctx) {
  8274. this._resizeImage(ctx);
  8275. this.left = this.x - this.width / 2;
  8276. this.top = this.y - this.height / 2;
  8277. var yLabel;
  8278. if (this.imageObj.width != 0 ) {
  8279. // draw the shade
  8280. if (this.clusterSize > 1) {
  8281. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8282. lineWidth *= this.graphScaleInv;
  8283. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8284. ctx.globalAlpha = 0.5;
  8285. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8286. }
  8287. // draw the image
  8288. ctx.globalAlpha = 1.0;
  8289. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8290. yLabel = this.y + this.height / 2;
  8291. }
  8292. else {
  8293. // image still loading... just draw the label for now
  8294. yLabel = this.y;
  8295. }
  8296. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8297. };
  8298. Node.prototype._resizeBox = function (ctx) {
  8299. if (!this.width) {
  8300. var margin = 5;
  8301. var textSize = this.getTextSize(ctx);
  8302. this.width = textSize.width + 2 * margin;
  8303. this.height = textSize.height + 2 * margin;
  8304. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8305. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8306. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8307. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8308. }
  8309. };
  8310. Node.prototype._drawBox = function (ctx) {
  8311. this._resizeBox(ctx);
  8312. this.left = this.x - this.width / 2;
  8313. this.top = this.y - this.height / 2;
  8314. var clusterLineWidth = 2.5;
  8315. var selectionLineWidth = 2;
  8316. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8317. // draw the outer border
  8318. if (this.clusterSize > 1) {
  8319. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8320. ctx.lineWidth *= this.graphScaleInv;
  8321. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8322. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8323. ctx.stroke();
  8324. }
  8325. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8326. ctx.lineWidth *= this.graphScaleInv;
  8327. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8328. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8329. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8330. ctx.fill();
  8331. ctx.stroke();
  8332. this._label(ctx, this.label, this.x, this.y);
  8333. };
  8334. Node.prototype._resizeDatabase = function (ctx) {
  8335. if (!this.width) {
  8336. var margin = 5;
  8337. var textSize = this.getTextSize(ctx);
  8338. var size = textSize.width + 2 * margin;
  8339. this.width = size;
  8340. this.height = size;
  8341. // scaling used for clustering
  8342. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8343. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8344. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8345. this.growthIndicator = this.width - size;
  8346. }
  8347. };
  8348. Node.prototype._drawDatabase = function (ctx) {
  8349. this._resizeDatabase(ctx);
  8350. this.left = this.x - this.width / 2;
  8351. this.top = this.y - this.height / 2;
  8352. var clusterLineWidth = 2.5;
  8353. var selectionLineWidth = 2;
  8354. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8355. // draw the outer border
  8356. if (this.clusterSize > 1) {
  8357. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8358. ctx.lineWidth *= this.graphScaleInv;
  8359. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8360. 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);
  8361. ctx.stroke();
  8362. }
  8363. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8364. ctx.lineWidth *= this.graphScaleInv;
  8365. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8366. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8367. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8368. ctx.fill();
  8369. ctx.stroke();
  8370. this._label(ctx, this.label, this.x, this.y);
  8371. };
  8372. Node.prototype._resizeCircle = function (ctx) {
  8373. if (!this.width) {
  8374. var margin = 5;
  8375. var textSize = this.getTextSize(ctx);
  8376. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8377. this.radius = diameter / 2;
  8378. this.width = diameter;
  8379. this.height = diameter;
  8380. // scaling used for clustering
  8381. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8382. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8383. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8384. this.growthIndicator = this.radius - 0.5*diameter;
  8385. }
  8386. };
  8387. Node.prototype._drawCircle = function (ctx) {
  8388. this._resizeCircle(ctx);
  8389. this.left = this.x - this.width / 2;
  8390. this.top = this.y - this.height / 2;
  8391. var clusterLineWidth = 2.5;
  8392. var selectionLineWidth = 2;
  8393. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8394. // draw the outer border
  8395. if (this.clusterSize > 1) {
  8396. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8397. ctx.lineWidth *= this.graphScaleInv;
  8398. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8399. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8400. ctx.stroke();
  8401. }
  8402. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8403. ctx.lineWidth *= this.graphScaleInv;
  8404. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8405. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8406. ctx.circle(this.x, this.y, this.radius);
  8407. ctx.fill();
  8408. ctx.stroke();
  8409. this._label(ctx, this.label, this.x, this.y);
  8410. };
  8411. Node.prototype._resizeEllipse = function (ctx) {
  8412. if (!this.width) {
  8413. var textSize = this.getTextSize(ctx);
  8414. this.width = textSize.width * 1.5;
  8415. this.height = textSize.height * 2;
  8416. if (this.width < this.height) {
  8417. this.width = this.height;
  8418. }
  8419. var defaultSize = this.width;
  8420. // scaling used for clustering
  8421. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8422. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8423. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8424. this.growthIndicator = this.width - defaultSize;
  8425. }
  8426. };
  8427. Node.prototype._drawEllipse = function (ctx) {
  8428. this._resizeEllipse(ctx);
  8429. this.left = this.x - this.width / 2;
  8430. this.top = this.y - this.height / 2;
  8431. var clusterLineWidth = 2.5;
  8432. var selectionLineWidth = 2;
  8433. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8434. // draw the outer border
  8435. if (this.clusterSize > 1) {
  8436. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8437. ctx.lineWidth *= this.graphScaleInv;
  8438. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8439. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8440. ctx.stroke();
  8441. }
  8442. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8443. ctx.lineWidth *= this.graphScaleInv;
  8444. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8445. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8446. ctx.ellipse(this.left, this.top, this.width, this.height);
  8447. ctx.fill();
  8448. ctx.stroke();
  8449. this._label(ctx, this.label, this.x, this.y);
  8450. };
  8451. Node.prototype._drawDot = function (ctx) {
  8452. this._drawShape(ctx, 'circle');
  8453. };
  8454. Node.prototype._drawTriangle = function (ctx) {
  8455. this._drawShape(ctx, 'triangle');
  8456. };
  8457. Node.prototype._drawTriangleDown = function (ctx) {
  8458. this._drawShape(ctx, 'triangleDown');
  8459. };
  8460. Node.prototype._drawSquare = function (ctx) {
  8461. this._drawShape(ctx, 'square');
  8462. };
  8463. Node.prototype._drawStar = function (ctx) {
  8464. this._drawShape(ctx, 'star');
  8465. };
  8466. Node.prototype._resizeShape = function (ctx) {
  8467. if (!this.width) {
  8468. this.radius = this.baseRadiusValue;
  8469. var size = 2 * this.radius;
  8470. this.width = size;
  8471. this.height = size;
  8472. // scaling used for clustering
  8473. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8474. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8475. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8476. this.growthIndicator = this.width - size;
  8477. }
  8478. };
  8479. Node.prototype._drawShape = function (ctx, shape) {
  8480. this._resizeShape(ctx);
  8481. this.left = this.x - this.width / 2;
  8482. this.top = this.y - this.height / 2;
  8483. var clusterLineWidth = 2.5;
  8484. var selectionLineWidth = 2;
  8485. var radiusMultiplier = 2;
  8486. // choose draw method depending on the shape
  8487. switch (shape) {
  8488. case 'dot': radiusMultiplier = 2; break;
  8489. case 'square': radiusMultiplier = 2; break;
  8490. case 'triangle': radiusMultiplier = 3; break;
  8491. case 'triangleDown': radiusMultiplier = 3; break;
  8492. case 'star': radiusMultiplier = 4; break;
  8493. }
  8494. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8495. // draw the outer border
  8496. if (this.clusterSize > 1) {
  8497. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8498. ctx.lineWidth *= this.graphScaleInv;
  8499. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8500. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8501. ctx.stroke();
  8502. }
  8503. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8504. ctx.lineWidth *= this.graphScaleInv;
  8505. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8506. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8507. ctx[shape](this.x, this.y, this.radius);
  8508. ctx.fill();
  8509. ctx.stroke();
  8510. if (this.label) {
  8511. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8512. }
  8513. };
  8514. Node.prototype._resizeText = function (ctx) {
  8515. if (!this.width) {
  8516. var margin = 5;
  8517. var textSize = this.getTextSize(ctx);
  8518. this.width = textSize.width + 2 * margin;
  8519. this.height = textSize.height + 2 * margin;
  8520. // scaling used for clustering
  8521. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8522. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8523. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8524. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8525. }
  8526. };
  8527. Node.prototype._drawText = function (ctx) {
  8528. this._resizeText(ctx);
  8529. this.left = this.x - this.width / 2;
  8530. this.top = this.y - this.height / 2;
  8531. this._label(ctx, this.label, this.x, this.y);
  8532. };
  8533. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8534. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  8535. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8536. ctx.fillStyle = this.fontColor || "black";
  8537. ctx.textAlign = align || "center";
  8538. ctx.textBaseline = baseline || "middle";
  8539. var lines = text.split('\n'),
  8540. lineCount = lines.length,
  8541. fontSize = (this.fontSize + 4),
  8542. yLine = y + (1 - lineCount) / 2 * fontSize;
  8543. for (var i = 0; i < lineCount; i++) {
  8544. ctx.fillText(lines[i], x, yLine);
  8545. yLine += fontSize;
  8546. }
  8547. }
  8548. };
  8549. Node.prototype.getTextSize = function(ctx) {
  8550. if (this.label !== undefined) {
  8551. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8552. var lines = this.label.split('\n'),
  8553. height = (this.fontSize + 4) * lines.length,
  8554. width = 0;
  8555. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8556. width = Math.max(width, ctx.measureText(lines[i]).width);
  8557. }
  8558. return {"width": width, "height": height};
  8559. }
  8560. else {
  8561. return {"width": 0, "height": 0};
  8562. }
  8563. };
  8564. /**
  8565. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8566. * there is a safety margin of 0.3 * width;
  8567. *
  8568. * @returns {boolean}
  8569. */
  8570. Node.prototype.inArea = function() {
  8571. if (this.width !== undefined) {
  8572. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  8573. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  8574. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  8575. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  8576. }
  8577. else {
  8578. return true;
  8579. }
  8580. };
  8581. /**
  8582. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8583. * @returns {boolean}
  8584. */
  8585. Node.prototype.inView = function() {
  8586. return (this.x >= this.canvasTopLeft.x &&
  8587. this.x < this.canvasBottomRight.x &&
  8588. this.y >= this.canvasTopLeft.y &&
  8589. this.y < this.canvasBottomRight.y);
  8590. };
  8591. /**
  8592. * This allows the zoom level of the graph to influence the rendering
  8593. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8594. *
  8595. * @param scale
  8596. * @param canvasTopLeft
  8597. * @param canvasBottomRight
  8598. */
  8599. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8600. this.graphScaleInv = 1.0/scale;
  8601. this.graphScale = scale;
  8602. this.canvasTopLeft = canvasTopLeft;
  8603. this.canvasBottomRight = canvasBottomRight;
  8604. };
  8605. /**
  8606. * This allows the zoom level of the graph to influence the rendering
  8607. *
  8608. * @param scale
  8609. */
  8610. Node.prototype.setScale = function(scale) {
  8611. this.graphScaleInv = 1.0/scale;
  8612. this.graphScale = scale;
  8613. };
  8614. /**
  8615. * set the velocity at 0. Is called when this node is contained in another during clustering
  8616. */
  8617. Node.prototype.clearVelocity = function() {
  8618. this.vx = 0;
  8619. this.vy = 0;
  8620. };
  8621. /**
  8622. * Basic preservation of (kinectic) energy
  8623. *
  8624. * @param massBeforeClustering
  8625. */
  8626. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8627. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8628. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8629. this.vx = Math.sqrt(energyBefore/this.mass);
  8630. energyBefore = this.vy * this.vy * massBeforeClustering;
  8631. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8632. this.vy = Math.sqrt(energyBefore/this.mass);
  8633. };
  8634. /**
  8635. * @class Edge
  8636. *
  8637. * A edge connects two nodes
  8638. * @param {Object} properties Object with properties. Must contain
  8639. * At least properties from and to.
  8640. * Available properties: from (number),
  8641. * to (number), label (string, color (string),
  8642. * width (number), style (string),
  8643. * length (number), title (string)
  8644. * @param {Graph} graph A graph object, used to find and edge to
  8645. * nodes.
  8646. * @param {Object} constants An object with default values for
  8647. * example for the color
  8648. */
  8649. function Edge (properties, graph, constants) {
  8650. if (!graph) {
  8651. throw "No graph provided";
  8652. }
  8653. this.graph = graph;
  8654. // initialize constants
  8655. this.widthMin = constants.edges.widthMin;
  8656. this.widthMax = constants.edges.widthMax;
  8657. // initialize variables
  8658. this.id = undefined;
  8659. this.fromId = undefined;
  8660. this.toId = undefined;
  8661. this.style = constants.edges.style;
  8662. this.title = undefined;
  8663. this.width = constants.edges.width;
  8664. this.value = undefined;
  8665. this.length = constants.physics.springLength;
  8666. this.customLength = false;
  8667. this.selected = false;
  8668. this.smooth = constants.smoothCurves;
  8669. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  8670. this.from = null; // a node
  8671. this.to = null; // a node
  8672. this.via = null; // a temp node
  8673. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8674. // by storing the original information we can revert to the original connection when the cluser is opened.
  8675. this.originalFromId = [];
  8676. this.originalToId = [];
  8677. this.connected = false;
  8678. // Added to support dashed lines
  8679. // David Jordan
  8680. // 2012-08-08
  8681. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8682. this.color = {color:constants.edges.color.color,
  8683. highlight:constants.edges.color.highlight};
  8684. this.widthFixed = false;
  8685. this.lengthFixed = false;
  8686. this.setProperties(properties, constants);
  8687. }
  8688. /**
  8689. * Set or overwrite properties for the edge
  8690. * @param {Object} properties an object with properties
  8691. * @param {Object} constants and object with default, global properties
  8692. */
  8693. Edge.prototype.setProperties = function(properties, constants) {
  8694. if (!properties) {
  8695. return;
  8696. }
  8697. if (properties.from !== undefined) {this.fromId = properties.from;}
  8698. if (properties.to !== undefined) {this.toId = properties.to;}
  8699. if (properties.id !== undefined) {this.id = properties.id;}
  8700. if (properties.style !== undefined) {this.style = properties.style;}
  8701. if (properties.label !== undefined) {this.label = properties.label;}
  8702. if (this.label) {
  8703. this.fontSize = constants.edges.fontSize;
  8704. this.fontFace = constants.edges.fontFace;
  8705. this.fontColor = constants.edges.fontColor;
  8706. this.fontFill = constants.edges.fontFill;
  8707. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8708. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8709. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8710. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  8711. }
  8712. if (properties.title !== undefined) {this.title = properties.title;}
  8713. if (properties.width !== undefined) {this.width = properties.width;}
  8714. if (properties.value !== undefined) {this.value = properties.value;}
  8715. if (properties.length !== undefined) {this.length = properties.length;
  8716. this.customLength = true;}
  8717. // scale the arrow
  8718. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  8719. // Added to support dashed lines
  8720. // David Jordan
  8721. // 2012-08-08
  8722. if (properties.dash) {
  8723. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  8724. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  8725. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  8726. }
  8727. if (properties.color !== undefined) {
  8728. if (util.isString(properties.color)) {
  8729. this.color.color = properties.color;
  8730. this.color.highlight = properties.color;
  8731. }
  8732. else {
  8733. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  8734. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  8735. }
  8736. }
  8737. // A node is connected when it has a from and to node.
  8738. this.connect();
  8739. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  8740. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  8741. // set draw method based on style
  8742. switch (this.style) {
  8743. case 'line': this.draw = this._drawLine; break;
  8744. case 'arrow': this.draw = this._drawArrow; break;
  8745. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8746. case 'dash-line': this.draw = this._drawDashLine; break;
  8747. default: this.draw = this._drawLine; break;
  8748. }
  8749. };
  8750. /**
  8751. * Connect an edge to its nodes
  8752. */
  8753. Edge.prototype.connect = function () {
  8754. this.disconnect();
  8755. this.from = this.graph.nodes[this.fromId] || null;
  8756. this.to = this.graph.nodes[this.toId] || null;
  8757. this.connected = (this.from && this.to);
  8758. if (this.connected) {
  8759. this.from.attachEdge(this);
  8760. this.to.attachEdge(this);
  8761. }
  8762. else {
  8763. if (this.from) {
  8764. this.from.detachEdge(this);
  8765. }
  8766. if (this.to) {
  8767. this.to.detachEdge(this);
  8768. }
  8769. }
  8770. };
  8771. /**
  8772. * Disconnect an edge from its nodes
  8773. */
  8774. Edge.prototype.disconnect = function () {
  8775. if (this.from) {
  8776. this.from.detachEdge(this);
  8777. this.from = null;
  8778. }
  8779. if (this.to) {
  8780. this.to.detachEdge(this);
  8781. this.to = null;
  8782. }
  8783. this.connected = false;
  8784. };
  8785. /**
  8786. * get the title of this edge.
  8787. * @return {string} title The title of the edge, or undefined when no title
  8788. * has been set.
  8789. */
  8790. Edge.prototype.getTitle = function() {
  8791. return typeof this.title === "function" ? this.title() : this.title;
  8792. };
  8793. /**
  8794. * Retrieve the value of the edge. Can be undefined
  8795. * @return {Number} value
  8796. */
  8797. Edge.prototype.getValue = function() {
  8798. return this.value;
  8799. };
  8800. /**
  8801. * Adjust the value range of the edge. The edge will adjust it's width
  8802. * based on its value.
  8803. * @param {Number} min
  8804. * @param {Number} max
  8805. */
  8806. Edge.prototype.setValueRange = function(min, max) {
  8807. if (!this.widthFixed && this.value !== undefined) {
  8808. var scale = (this.widthMax - this.widthMin) / (max - min);
  8809. this.width = (this.value - min) * scale + this.widthMin;
  8810. }
  8811. };
  8812. /**
  8813. * Redraw a edge
  8814. * Draw this edge in the given canvas
  8815. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8816. * @param {CanvasRenderingContext2D} ctx
  8817. */
  8818. Edge.prototype.draw = function(ctx) {
  8819. throw "Method draw not initialized in edge";
  8820. };
  8821. /**
  8822. * Check if this object is overlapping with the provided object
  8823. * @param {Object} obj an object with parameters left, top
  8824. * @return {boolean} True if location is located on the edge
  8825. */
  8826. Edge.prototype.isOverlappingWith = function(obj) {
  8827. if (this.connected) {
  8828. var distMax = 10;
  8829. var xFrom = this.from.x;
  8830. var yFrom = this.from.y;
  8831. var xTo = this.to.x;
  8832. var yTo = this.to.y;
  8833. var xObj = obj.left;
  8834. var yObj = obj.top;
  8835. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8836. return (dist < distMax);
  8837. }
  8838. else {
  8839. return false
  8840. }
  8841. };
  8842. /**
  8843. * Redraw a edge as a line
  8844. * Draw this edge in the given canvas
  8845. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8846. * @param {CanvasRenderingContext2D} ctx
  8847. * @private
  8848. */
  8849. Edge.prototype._drawLine = function(ctx) {
  8850. // set style
  8851. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8852. else {ctx.strokeStyle = this.color.color;}
  8853. ctx.lineWidth = this._getLineWidth();
  8854. if (this.from != this.to) {
  8855. // draw line
  8856. this._line(ctx);
  8857. // draw label
  8858. var point;
  8859. if (this.label) {
  8860. if (this.smooth == true) {
  8861. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8862. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8863. point = {x:midpointX, y:midpointY};
  8864. }
  8865. else {
  8866. point = this._pointOnLine(0.5);
  8867. }
  8868. this._label(ctx, this.label, point.x, point.y);
  8869. }
  8870. }
  8871. else {
  8872. var x, y;
  8873. var radius = this.length / 4;
  8874. var node = this.from;
  8875. if (!node.width) {
  8876. node.resize(ctx);
  8877. }
  8878. if (node.width > node.height) {
  8879. x = node.x + node.width / 2;
  8880. y = node.y - radius;
  8881. }
  8882. else {
  8883. x = node.x + radius;
  8884. y = node.y - node.height / 2;
  8885. }
  8886. this._circle(ctx, x, y, radius);
  8887. point = this._pointOnCircle(x, y, radius, 0.5);
  8888. this._label(ctx, this.label, point.x, point.y);
  8889. }
  8890. };
  8891. /**
  8892. * Get the line width of the edge. Depends on width and whether one of the
  8893. * connected nodes is selected.
  8894. * @return {Number} width
  8895. * @private
  8896. */
  8897. Edge.prototype._getLineWidth = function() {
  8898. if (this.selected == true) {
  8899. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  8900. }
  8901. else {
  8902. return this.width*this.graphScaleInv;
  8903. }
  8904. };
  8905. /**
  8906. * Draw a line between two nodes
  8907. * @param {CanvasRenderingContext2D} ctx
  8908. * @private
  8909. */
  8910. Edge.prototype._line = function (ctx) {
  8911. // draw a straight line
  8912. ctx.beginPath();
  8913. ctx.moveTo(this.from.x, this.from.y);
  8914. if (this.smooth == true) {
  8915. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8916. }
  8917. else {
  8918. ctx.lineTo(this.to.x, this.to.y);
  8919. }
  8920. ctx.stroke();
  8921. };
  8922. /**
  8923. * Draw a line from a node to itself, a circle
  8924. * @param {CanvasRenderingContext2D} ctx
  8925. * @param {Number} x
  8926. * @param {Number} y
  8927. * @param {Number} radius
  8928. * @private
  8929. */
  8930. Edge.prototype._circle = function (ctx, x, y, radius) {
  8931. // draw a circle
  8932. ctx.beginPath();
  8933. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8934. ctx.stroke();
  8935. };
  8936. /**
  8937. * Draw label with white background and with the middle at (x, y)
  8938. * @param {CanvasRenderingContext2D} ctx
  8939. * @param {String} text
  8940. * @param {Number} x
  8941. * @param {Number} y
  8942. * @private
  8943. */
  8944. Edge.prototype._label = function (ctx, text, x, y) {
  8945. if (text) {
  8946. // TODO: cache the calculated size
  8947. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8948. this.fontSize + "px " + this.fontFace;
  8949. ctx.fillStyle = this.fontFill;
  8950. var width = ctx.measureText(text).width;
  8951. var height = this.fontSize;
  8952. var left = x - width / 2;
  8953. var top = y - height / 2;
  8954. ctx.fillRect(left, top, width, height);
  8955. // draw text
  8956. ctx.fillStyle = this.fontColor || "black";
  8957. ctx.textAlign = "left";
  8958. ctx.textBaseline = "top";
  8959. ctx.fillText(text, left, top);
  8960. }
  8961. };
  8962. /**
  8963. * Redraw a edge as a dashed line
  8964. * Draw this edge in the given canvas
  8965. * @author David Jordan
  8966. * @date 2012-08-08
  8967. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8968. * @param {CanvasRenderingContext2D} ctx
  8969. * @private
  8970. */
  8971. Edge.prototype._drawDashLine = function(ctx) {
  8972. // set style
  8973. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8974. else {ctx.strokeStyle = this.color.color;}
  8975. ctx.lineWidth = this._getLineWidth();
  8976. // only firefox and chrome support this method, else we use the legacy one.
  8977. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  8978. ctx.beginPath();
  8979. ctx.moveTo(this.from.x, this.from.y);
  8980. // configure the dash pattern
  8981. var pattern = [0];
  8982. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  8983. pattern = [this.dash.length,this.dash.gap];
  8984. }
  8985. else {
  8986. pattern = [5,5];
  8987. }
  8988. // set dash settings for chrome or firefox
  8989. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  8990. ctx.setLineDash(pattern);
  8991. ctx.lineDashOffset = 0;
  8992. } else { //Firefox
  8993. ctx.mozDash = pattern;
  8994. ctx.mozDashOffset = 0;
  8995. }
  8996. // draw the line
  8997. if (this.smooth == true) {
  8998. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8999. }
  9000. else {
  9001. ctx.lineTo(this.to.x, this.to.y);
  9002. }
  9003. ctx.stroke();
  9004. // restore the dash settings.
  9005. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9006. ctx.setLineDash([0]);
  9007. ctx.lineDashOffset = 0;
  9008. } else { //Firefox
  9009. ctx.mozDash = [0];
  9010. ctx.mozDashOffset = 0;
  9011. }
  9012. }
  9013. else { // unsupporting smooth lines
  9014. // draw dashed line
  9015. ctx.beginPath();
  9016. ctx.lineCap = 'round';
  9017. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9018. {
  9019. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9020. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  9021. }
  9022. 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
  9023. {
  9024. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9025. [this.dash.length,this.dash.gap]);
  9026. }
  9027. else //If all else fails draw a line
  9028. {
  9029. ctx.moveTo(this.from.x, this.from.y);
  9030. ctx.lineTo(this.to.x, this.to.y);
  9031. }
  9032. ctx.stroke();
  9033. }
  9034. // draw label
  9035. if (this.label) {
  9036. var point;
  9037. if (this.smooth == true) {
  9038. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9039. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9040. point = {x:midpointX, y:midpointY};
  9041. }
  9042. else {
  9043. point = this._pointOnLine(0.5);
  9044. }
  9045. this._label(ctx, this.label, point.x, point.y);
  9046. }
  9047. };
  9048. /**
  9049. * Get a point on a line
  9050. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9051. * @return {Object} point
  9052. * @private
  9053. */
  9054. Edge.prototype._pointOnLine = function (percentage) {
  9055. return {
  9056. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9057. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9058. }
  9059. };
  9060. /**
  9061. * Get a point on a circle
  9062. * @param {Number} x
  9063. * @param {Number} y
  9064. * @param {Number} radius
  9065. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9066. * @return {Object} point
  9067. * @private
  9068. */
  9069. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9070. var angle = (percentage - 3/8) * 2 * Math.PI;
  9071. return {
  9072. x: x + radius * Math.cos(angle),
  9073. y: y - radius * Math.sin(angle)
  9074. }
  9075. };
  9076. /**
  9077. * Redraw a edge as a line with an arrow halfway the line
  9078. * Draw this edge in the given canvas
  9079. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9080. * @param {CanvasRenderingContext2D} ctx
  9081. * @private
  9082. */
  9083. Edge.prototype._drawArrowCenter = function(ctx) {
  9084. var point;
  9085. // set style
  9086. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9087. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9088. ctx.lineWidth = this._getLineWidth();
  9089. if (this.from != this.to) {
  9090. // draw line
  9091. this._line(ctx);
  9092. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9093. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9094. // draw an arrow halfway the line
  9095. if (this.smooth == true) {
  9096. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9097. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9098. point = {x:midpointX, y:midpointY};
  9099. }
  9100. else {
  9101. point = this._pointOnLine(0.5);
  9102. }
  9103. ctx.arrow(point.x, point.y, angle, length);
  9104. ctx.fill();
  9105. ctx.stroke();
  9106. // draw label
  9107. if (this.label) {
  9108. this._label(ctx, this.label, point.x, point.y);
  9109. }
  9110. }
  9111. else {
  9112. // draw circle
  9113. var x, y;
  9114. var radius = 0.25 * Math.max(100,this.length);
  9115. var node = this.from;
  9116. if (!node.width) {
  9117. node.resize(ctx);
  9118. }
  9119. if (node.width > node.height) {
  9120. x = node.x + node.width * 0.5;
  9121. y = node.y - radius;
  9122. }
  9123. else {
  9124. x = node.x + radius;
  9125. y = node.y - node.height * 0.5;
  9126. }
  9127. this._circle(ctx, x, y, radius);
  9128. // draw all arrows
  9129. var angle = 0.2 * Math.PI;
  9130. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9131. point = this._pointOnCircle(x, y, radius, 0.5);
  9132. ctx.arrow(point.x, point.y, angle, length);
  9133. ctx.fill();
  9134. ctx.stroke();
  9135. // draw label
  9136. if (this.label) {
  9137. point = this._pointOnCircle(x, y, radius, 0.5);
  9138. this._label(ctx, this.label, point.x, point.y);
  9139. }
  9140. }
  9141. };
  9142. /**
  9143. * Redraw a edge as a line with an arrow
  9144. * Draw this edge in the given canvas
  9145. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9146. * @param {CanvasRenderingContext2D} ctx
  9147. * @private
  9148. */
  9149. Edge.prototype._drawArrow = function(ctx) {
  9150. // set style
  9151. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9152. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9153. ctx.lineWidth = this._getLineWidth();
  9154. var angle, length;
  9155. //draw a line
  9156. if (this.from != this.to) {
  9157. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9158. var dx = (this.to.x - this.from.x);
  9159. var dy = (this.to.y - this.from.y);
  9160. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9161. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9162. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9163. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9164. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9165. if (this.smooth == true) {
  9166. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9167. dx = (this.to.x - this.via.x);
  9168. dy = (this.to.y - this.via.y);
  9169. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9170. }
  9171. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9172. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9173. var xTo,yTo;
  9174. if (this.smooth == true) {
  9175. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9176. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9177. }
  9178. else {
  9179. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9180. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9181. }
  9182. ctx.beginPath();
  9183. ctx.moveTo(xFrom,yFrom);
  9184. if (this.smooth == true) {
  9185. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9186. }
  9187. else {
  9188. ctx.lineTo(xTo, yTo);
  9189. }
  9190. ctx.stroke();
  9191. // draw arrow at the end of the line
  9192. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9193. ctx.arrow(xTo, yTo, angle, length);
  9194. ctx.fill();
  9195. ctx.stroke();
  9196. // draw label
  9197. if (this.label) {
  9198. var point;
  9199. if (this.smooth == true) {
  9200. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9201. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9202. point = {x:midpointX, y:midpointY};
  9203. }
  9204. else {
  9205. point = this._pointOnLine(0.5);
  9206. }
  9207. this._label(ctx, this.label, point.x, point.y);
  9208. }
  9209. }
  9210. else {
  9211. // draw circle
  9212. var node = this.from;
  9213. var x, y, arrow;
  9214. var radius = 0.25 * Math.max(100,this.length);
  9215. if (!node.width) {
  9216. node.resize(ctx);
  9217. }
  9218. if (node.width > node.height) {
  9219. x = node.x + node.width * 0.5;
  9220. y = node.y - radius;
  9221. arrow = {
  9222. x: x,
  9223. y: node.y,
  9224. angle: 0.9 * Math.PI
  9225. };
  9226. }
  9227. else {
  9228. x = node.x + radius;
  9229. y = node.y - node.height * 0.5;
  9230. arrow = {
  9231. x: node.x,
  9232. y: y,
  9233. angle: 0.6 * Math.PI
  9234. };
  9235. }
  9236. ctx.beginPath();
  9237. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9238. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9239. ctx.stroke();
  9240. // draw all arrows
  9241. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9242. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9243. ctx.fill();
  9244. ctx.stroke();
  9245. // draw label
  9246. if (this.label) {
  9247. point = this._pointOnCircle(x, y, radius, 0.5);
  9248. this._label(ctx, this.label, point.x, point.y);
  9249. }
  9250. }
  9251. };
  9252. /**
  9253. * Calculate the distance between a point (x3,y3) and a line segment from
  9254. * (x1,y1) to (x2,y2).
  9255. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9256. * @param {number} x1
  9257. * @param {number} y1
  9258. * @param {number} x2
  9259. * @param {number} y2
  9260. * @param {number} x3
  9261. * @param {number} y3
  9262. * @private
  9263. */
  9264. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9265. if (this.smooth == true) {
  9266. var minDistance = 1e9;
  9267. var i,t,x,y,dx,dy;
  9268. for (i = 0; i < 10; i++) {
  9269. t = 0.1*i;
  9270. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  9271. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  9272. dx = Math.abs(x3-x);
  9273. dy = Math.abs(y3-y);
  9274. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  9275. }
  9276. return minDistance
  9277. }
  9278. else {
  9279. var px = x2-x1,
  9280. py = y2-y1,
  9281. something = px*px + py*py,
  9282. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9283. if (u > 1) {
  9284. u = 1;
  9285. }
  9286. else if (u < 0) {
  9287. u = 0;
  9288. }
  9289. var x = x1 + u * px,
  9290. y = y1 + u * py,
  9291. dx = x - x3,
  9292. dy = y - y3;
  9293. //# Note: If the actual distance does not matter,
  9294. //# if you only want to compare what this function
  9295. //# returns to other results of this function, you
  9296. //# can just return the squared distance instead
  9297. //# (i.e. remove the sqrt) to gain a little performance
  9298. return Math.sqrt(dx*dx + dy*dy);
  9299. }
  9300. };
  9301. /**
  9302. * This allows the zoom level of the graph to influence the rendering
  9303. *
  9304. * @param scale
  9305. */
  9306. Edge.prototype.setScale = function(scale) {
  9307. this.graphScaleInv = 1.0/scale;
  9308. };
  9309. Edge.prototype.select = function() {
  9310. this.selected = true;
  9311. };
  9312. Edge.prototype.unselect = function() {
  9313. this.selected = false;
  9314. };
  9315. Edge.prototype.positionBezierNode = function() {
  9316. if (this.via !== null) {
  9317. this.via.x = 0.5 * (this.from.x + this.to.x);
  9318. this.via.y = 0.5 * (this.from.y + this.to.y);
  9319. }
  9320. };
  9321. /**
  9322. * Popup is a class to create a popup window with some text
  9323. * @param {Element} container The container object.
  9324. * @param {Number} [x]
  9325. * @param {Number} [y]
  9326. * @param {String} [text]
  9327. * @param {Object} [style] An object containing borderColor,
  9328. * backgroundColor, etc.
  9329. */
  9330. function Popup(container, x, y, text, style) {
  9331. if (container) {
  9332. this.container = container;
  9333. }
  9334. else {
  9335. this.container = document.body;
  9336. }
  9337. // x, y and text are optional, see if a style object was passed in their place
  9338. if (style === undefined) {
  9339. if (typeof x === "object") {
  9340. style = x;
  9341. x = undefined;
  9342. } else if (typeof text === "object") {
  9343. style = text;
  9344. text = undefined;
  9345. } else {
  9346. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  9347. style = {
  9348. fontColor: 'black',
  9349. fontSize: 14, // px
  9350. fontFace: 'verdana',
  9351. color: {
  9352. border: '#666',
  9353. background: '#FFFFC6'
  9354. }
  9355. }
  9356. }
  9357. }
  9358. this.x = 0;
  9359. this.y = 0;
  9360. this.padding = 5;
  9361. if (x !== undefined && y !== undefined ) {
  9362. this.setPosition(x, y);
  9363. }
  9364. if (text !== undefined) {
  9365. this.setText(text);
  9366. }
  9367. // create the frame
  9368. this.frame = document.createElement("div");
  9369. var styleAttr = this.frame.style;
  9370. styleAttr.position = "absolute";
  9371. styleAttr.visibility = "hidden";
  9372. styleAttr.border = "1px solid " + style.color.border;
  9373. styleAttr.color = style.fontColor;
  9374. styleAttr.fontSize = style.fontSize + "px";
  9375. styleAttr.fontFamily = style.fontFace;
  9376. styleAttr.padding = this.padding + "px";
  9377. styleAttr.backgroundColor = style.color.background;
  9378. styleAttr.borderRadius = "3px";
  9379. styleAttr.MozBorderRadius = "3px";
  9380. styleAttr.WebkitBorderRadius = "3px";
  9381. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9382. styleAttr.whiteSpace = "nowrap";
  9383. this.container.appendChild(this.frame);
  9384. }
  9385. /**
  9386. * @param {number} x Horizontal position of the popup window
  9387. * @param {number} y Vertical position of the popup window
  9388. */
  9389. Popup.prototype.setPosition = function(x, y) {
  9390. this.x = parseInt(x);
  9391. this.y = parseInt(y);
  9392. };
  9393. /**
  9394. * Set the text for the popup window. This can be HTML code
  9395. * @param {string} text
  9396. */
  9397. Popup.prototype.setText = function(text) {
  9398. this.frame.innerHTML = text;
  9399. };
  9400. /**
  9401. * Show the popup window
  9402. * @param {boolean} show Optional. Show or hide the window
  9403. */
  9404. Popup.prototype.show = function (show) {
  9405. if (show === undefined) {
  9406. show = true;
  9407. }
  9408. if (show) {
  9409. var height = this.frame.clientHeight;
  9410. var width = this.frame.clientWidth;
  9411. var maxHeight = this.frame.parentNode.clientHeight;
  9412. var maxWidth = this.frame.parentNode.clientWidth;
  9413. var top = (this.y - height);
  9414. if (top + height + this.padding > maxHeight) {
  9415. top = maxHeight - height - this.padding;
  9416. }
  9417. if (top < this.padding) {
  9418. top = this.padding;
  9419. }
  9420. var left = this.x;
  9421. if (left + width + this.padding > maxWidth) {
  9422. left = maxWidth - width - this.padding;
  9423. }
  9424. if (left < this.padding) {
  9425. left = this.padding;
  9426. }
  9427. this.frame.style.left = left + "px";
  9428. this.frame.style.top = top + "px";
  9429. this.frame.style.visibility = "visible";
  9430. }
  9431. else {
  9432. this.hide();
  9433. }
  9434. };
  9435. /**
  9436. * Hide the popup window
  9437. */
  9438. Popup.prototype.hide = function () {
  9439. this.frame.style.visibility = "hidden";
  9440. };
  9441. /**
  9442. * @class Groups
  9443. * This class can store groups and properties specific for groups.
  9444. */
  9445. Groups = function () {
  9446. this.clear();
  9447. this.defaultIndex = 0;
  9448. };
  9449. /**
  9450. * default constants for group colors
  9451. */
  9452. Groups.DEFAULT = [
  9453. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9454. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9455. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9456. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9457. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9458. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9459. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9460. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9461. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9462. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9463. ];
  9464. /**
  9465. * Clear all groups
  9466. */
  9467. Groups.prototype.clear = function () {
  9468. this.groups = {};
  9469. this.groups.length = function()
  9470. {
  9471. var i = 0;
  9472. for ( var p in this ) {
  9473. if (this.hasOwnProperty(p)) {
  9474. i++;
  9475. }
  9476. }
  9477. return i;
  9478. }
  9479. };
  9480. /**
  9481. * get group properties of a groupname. If groupname is not found, a new group
  9482. * is added.
  9483. * @param {*} groupname Can be a number, string, Date, etc.
  9484. * @return {Object} group The created group, containing all group properties
  9485. */
  9486. Groups.prototype.get = function (groupname) {
  9487. var group = this.groups[groupname];
  9488. if (group == undefined) {
  9489. // create new group
  9490. var index = this.defaultIndex % Groups.DEFAULT.length;
  9491. this.defaultIndex++;
  9492. group = {};
  9493. group.color = Groups.DEFAULT[index];
  9494. this.groups[groupname] = group;
  9495. }
  9496. return group;
  9497. };
  9498. /**
  9499. * Add a custom group style
  9500. * @param {String} groupname
  9501. * @param {Object} style An object containing borderColor,
  9502. * backgroundColor, etc.
  9503. * @return {Object} group The created group object
  9504. */
  9505. Groups.prototype.add = function (groupname, style) {
  9506. this.groups[groupname] = style;
  9507. if (style.color) {
  9508. style.color = util.parseColor(style.color);
  9509. }
  9510. return style;
  9511. };
  9512. /**
  9513. * @class Images
  9514. * This class loads images and keeps them stored.
  9515. */
  9516. Images = function () {
  9517. this.images = {};
  9518. this.callback = undefined;
  9519. };
  9520. /**
  9521. * Set an onload callback function. This will be called each time an image
  9522. * is loaded
  9523. * @param {function} callback
  9524. */
  9525. Images.prototype.setOnloadCallback = function(callback) {
  9526. this.callback = callback;
  9527. };
  9528. /**
  9529. *
  9530. * @param {string} url Url of the image
  9531. * @return {Image} img The image object
  9532. */
  9533. Images.prototype.load = function(url) {
  9534. var img = this.images[url];
  9535. if (img == undefined) {
  9536. // create the image
  9537. var images = this;
  9538. img = new Image();
  9539. this.images[url] = img;
  9540. img.onload = function() {
  9541. if (images.callback) {
  9542. images.callback(this);
  9543. }
  9544. };
  9545. img.src = url;
  9546. }
  9547. return img;
  9548. };
  9549. /**
  9550. * Created by Alex on 2/6/14.
  9551. */
  9552. var physicsMixin = {
  9553. /**
  9554. * Toggling barnes Hut calculation on and off.
  9555. *
  9556. * @private
  9557. */
  9558. _toggleBarnesHut: function () {
  9559. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  9560. this._loadSelectedForceSolver();
  9561. this.moving = true;
  9562. this.start();
  9563. },
  9564. /**
  9565. * This loads the node force solver based on the barnes hut or repulsion algorithm
  9566. *
  9567. * @private
  9568. */
  9569. _loadSelectedForceSolver: function () {
  9570. // this overloads the this._calculateNodeForces
  9571. if (this.constants.physics.barnesHut.enabled == true) {
  9572. this._clearMixin(repulsionMixin);
  9573. this._clearMixin(hierarchalRepulsionMixin);
  9574. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  9575. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  9576. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  9577. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  9578. this._loadMixin(barnesHutMixin);
  9579. }
  9580. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  9581. this._clearMixin(barnesHutMixin);
  9582. this._clearMixin(repulsionMixin);
  9583. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  9584. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  9585. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  9586. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  9587. this._loadMixin(hierarchalRepulsionMixin);
  9588. }
  9589. else {
  9590. this._clearMixin(barnesHutMixin);
  9591. this._clearMixin(hierarchalRepulsionMixin);
  9592. this.barnesHutTree = undefined;
  9593. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  9594. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  9595. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  9596. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  9597. this._loadMixin(repulsionMixin);
  9598. }
  9599. },
  9600. /**
  9601. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  9602. * if there is more than one node. If it is just one node, we dont calculate anything.
  9603. *
  9604. * @private
  9605. */
  9606. _initializeForceCalculation: function () {
  9607. // stop calculation if there is only one node
  9608. if (this.nodeIndices.length == 1) {
  9609. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  9610. }
  9611. else {
  9612. // if there are too many nodes on screen, we cluster without repositioning
  9613. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  9614. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  9615. }
  9616. // we now start the force calculation
  9617. this._calculateForces();
  9618. }
  9619. },
  9620. /**
  9621. * Calculate the external forces acting on the nodes
  9622. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9623. * @private
  9624. */
  9625. _calculateForces: function () {
  9626. // Gravity is required to keep separated groups from floating off
  9627. // the forces are reset to zero in this loop by using _setForce instead
  9628. // of _addForce
  9629. this._calculateGravitationalForces();
  9630. this._calculateNodeForces();
  9631. if (this.constants.smoothCurves == true) {
  9632. this._calculateSpringForcesWithSupport();
  9633. }
  9634. else {
  9635. this._calculateSpringForces();
  9636. }
  9637. },
  9638. /**
  9639. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  9640. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  9641. * This function joins the datanodes and invisible (called support) nodes into one object.
  9642. * We do this so we do not contaminate this.nodes with the support nodes.
  9643. *
  9644. * @private
  9645. */
  9646. _updateCalculationNodes: function () {
  9647. if (this.constants.smoothCurves == true) {
  9648. this.calculationNodes = {};
  9649. this.calculationNodeIndices = [];
  9650. for (var nodeId in this.nodes) {
  9651. if (this.nodes.hasOwnProperty(nodeId)) {
  9652. this.calculationNodes[nodeId] = this.nodes[nodeId];
  9653. }
  9654. }
  9655. var supportNodes = this.sectors['support']['nodes'];
  9656. for (var supportNodeId in supportNodes) {
  9657. if (supportNodes.hasOwnProperty(supportNodeId)) {
  9658. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  9659. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  9660. }
  9661. else {
  9662. supportNodes[supportNodeId]._setForce(0, 0);
  9663. }
  9664. }
  9665. }
  9666. for (var idx in this.calculationNodes) {
  9667. if (this.calculationNodes.hasOwnProperty(idx)) {
  9668. this.calculationNodeIndices.push(idx);
  9669. }
  9670. }
  9671. }
  9672. else {
  9673. this.calculationNodes = this.nodes;
  9674. this.calculationNodeIndices = this.nodeIndices;
  9675. }
  9676. },
  9677. /**
  9678. * this function applies the central gravity effect to keep groups from floating off
  9679. *
  9680. * @private
  9681. */
  9682. _calculateGravitationalForces: function () {
  9683. var dx, dy, distance, node, i;
  9684. var nodes = this.calculationNodes;
  9685. var gravity = this.constants.physics.centralGravity;
  9686. var gravityForce = 0;
  9687. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  9688. node = nodes[this.calculationNodeIndices[i]];
  9689. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  9690. // gravity does not apply when we are in a pocket sector
  9691. if (this._sector() == "default" && gravity != 0) {
  9692. dx = -node.x;
  9693. dy = -node.y;
  9694. distance = Math.sqrt(dx * dx + dy * dy);
  9695. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  9696. node.fx = dx * gravityForce;
  9697. node.fy = dy * gravityForce;
  9698. }
  9699. else {
  9700. node.fx = 0;
  9701. node.fy = 0;
  9702. }
  9703. }
  9704. },
  9705. /**
  9706. * this function calculates the effects of the springs in the case of unsmooth curves.
  9707. *
  9708. * @private
  9709. */
  9710. _calculateSpringForces: function () {
  9711. var edgeLength, edge, edgeId;
  9712. var dx, dy, fx, fy, springForce, length;
  9713. var edges = this.edges;
  9714. // forces caused by the edges, modelled as springs
  9715. for (edgeId in edges) {
  9716. if (edges.hasOwnProperty(edgeId)) {
  9717. edge = edges[edgeId];
  9718. if (edge.connected) {
  9719. // only calculate forces if nodes are in the same sector
  9720. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9721. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9722. // this implies that the edges between big clusters are longer
  9723. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  9724. dx = (edge.from.x - edge.to.x);
  9725. dy = (edge.from.y - edge.to.y);
  9726. length = Math.sqrt(dx * dx + dy * dy);
  9727. if (length == 0) {
  9728. length = 0.01;
  9729. }
  9730. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9731. fx = dx * springForce;
  9732. fy = dy * springForce;
  9733. edge.from.fx += fx;
  9734. edge.from.fy += fy;
  9735. edge.to.fx -= fx;
  9736. edge.to.fy -= fy;
  9737. }
  9738. }
  9739. }
  9740. }
  9741. },
  9742. /**
  9743. * This function calculates the springforces on the nodes, accounting for the support nodes.
  9744. *
  9745. * @private
  9746. */
  9747. _calculateSpringForcesWithSupport: function () {
  9748. var edgeLength, edge, edgeId, combinedClusterSize;
  9749. var edges = this.edges;
  9750. // forces caused by the edges, modelled as springs
  9751. for (edgeId in edges) {
  9752. if (edges.hasOwnProperty(edgeId)) {
  9753. edge = edges[edgeId];
  9754. if (edge.connected) {
  9755. // only calculate forces if nodes are in the same sector
  9756. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9757. if (edge.via != null) {
  9758. var node1 = edge.to;
  9759. var node2 = edge.via;
  9760. var node3 = edge.from;
  9761. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9762. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  9763. // this implies that the edges between big clusters are longer
  9764. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  9765. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  9766. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  9767. }
  9768. }
  9769. }
  9770. }
  9771. }
  9772. },
  9773. /**
  9774. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  9775. *
  9776. * @param node1
  9777. * @param node2
  9778. * @param edgeLength
  9779. * @private
  9780. */
  9781. _calculateSpringForce: function (node1, node2, edgeLength) {
  9782. var dx, dy, fx, fy, springForce, length;
  9783. dx = (node1.x - node2.x);
  9784. dy = (node1.y - node2.y);
  9785. length = Math.sqrt(dx * dx + dy * dy);
  9786. if (length == 0) {
  9787. length = 0.01;
  9788. }
  9789. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9790. fx = dx * springForce;
  9791. fy = dy * springForce;
  9792. node1.fx += fx;
  9793. node1.fy += fy;
  9794. node2.fx -= fx;
  9795. node2.fy -= fy;
  9796. },
  9797. /**
  9798. * Load the HTML for the physics config and bind it
  9799. * @private
  9800. */
  9801. _loadPhysicsConfiguration: function () {
  9802. if (this.physicsConfiguration === undefined) {
  9803. this.backupConstants = {};
  9804. util.copyObject(this.constants, this.backupConstants);
  9805. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  9806. this.physicsConfiguration = document.createElement('div');
  9807. this.physicsConfiguration.className = "PhysicsConfiguration";
  9808. this.physicsConfiguration.innerHTML = '' +
  9809. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  9810. '<tr>' +
  9811. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  9812. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  9813. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  9814. '</tr>' +
  9815. '</table>' +
  9816. '<table id="graph_BH_table" style="display:none">' +
  9817. '<tr><td><b>Barnes Hut</b></td></tr>' +
  9818. '<tr>' +
  9819. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" 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>' +
  9820. '</tr>' +
  9821. '<tr>' +
  9822. '<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>' +
  9823. '</tr>' +
  9824. '<tr>' +
  9825. '<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>' +
  9826. '</tr>' +
  9827. '<tr>' +
  9828. '<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>' +
  9829. '</tr>' +
  9830. '<tr>' +
  9831. '<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>' +
  9832. '</tr>' +
  9833. '</table>' +
  9834. '<table id="graph_R_table" style="display:none">' +
  9835. '<tr><td><b>Repulsion</b></td></tr>' +
  9836. '<tr>' +
  9837. '<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>' +
  9838. '</tr>' +
  9839. '<tr>' +
  9840. '<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>' +
  9841. '</tr>' +
  9842. '<tr>' +
  9843. '<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>' +
  9844. '</tr>' +
  9845. '<tr>' +
  9846. '<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>' +
  9847. '</tr>' +
  9848. '<tr>' +
  9849. '<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>' +
  9850. '</tr>' +
  9851. '</table>' +
  9852. '<table id="graph_H_table" style="display:none">' +
  9853. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  9854. '<tr>' +
  9855. '<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>' +
  9856. '</tr>' +
  9857. '<tr>' +
  9858. '<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>' +
  9859. '</tr>' +
  9860. '<tr>' +
  9861. '<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>' +
  9862. '</tr>' +
  9863. '<tr>' +
  9864. '<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>' +
  9865. '</tr>' +
  9866. '<tr>' +
  9867. '<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>' +
  9868. '</tr>' +
  9869. '<tr>' +
  9870. '<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>' +
  9871. '</tr>' +
  9872. '<tr>' +
  9873. '<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>' +
  9874. '</tr>' +
  9875. '<tr>' +
  9876. '<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>' +
  9877. '</tr>' +
  9878. '</table>' +
  9879. '<table><tr><td><b>Options:</b></td></tr>' +
  9880. '<tr>' +
  9881. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  9882. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  9883. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  9884. '</tr>' +
  9885. '</table>'
  9886. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  9887. this.optionsDiv = document.createElement("div");
  9888. this.optionsDiv.style.fontSize = "14px";
  9889. this.optionsDiv.style.fontFamily = "verdana";
  9890. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  9891. var rangeElement;
  9892. rangeElement = document.getElementById('graph_BH_gc');
  9893. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  9894. rangeElement = document.getElementById('graph_BH_cg');
  9895. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  9896. rangeElement = document.getElementById('graph_BH_sc');
  9897. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  9898. rangeElement = document.getElementById('graph_BH_sl');
  9899. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  9900. rangeElement = document.getElementById('graph_BH_damp');
  9901. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  9902. rangeElement = document.getElementById('graph_R_nd');
  9903. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  9904. rangeElement = document.getElementById('graph_R_cg');
  9905. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  9906. rangeElement = document.getElementById('graph_R_sc');
  9907. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  9908. rangeElement = document.getElementById('graph_R_sl');
  9909. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  9910. rangeElement = document.getElementById('graph_R_damp');
  9911. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  9912. rangeElement = document.getElementById('graph_H_nd');
  9913. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  9914. rangeElement = document.getElementById('graph_H_cg');
  9915. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  9916. rangeElement = document.getElementById('graph_H_sc');
  9917. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  9918. rangeElement = document.getElementById('graph_H_sl');
  9919. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  9920. rangeElement = document.getElementById('graph_H_damp');
  9921. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  9922. rangeElement = document.getElementById('graph_H_direction');
  9923. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  9924. rangeElement = document.getElementById('graph_H_levsep');
  9925. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  9926. rangeElement = document.getElementById('graph_H_nspac');
  9927. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  9928. var radioButton1 = document.getElementById("graph_physicsMethod1");
  9929. var radioButton2 = document.getElementById("graph_physicsMethod2");
  9930. var radioButton3 = document.getElementById("graph_physicsMethod3");
  9931. radioButton2.checked = true;
  9932. if (this.constants.physics.barnesHut.enabled) {
  9933. radioButton1.checked = true;
  9934. }
  9935. if (this.constants.hierarchicalLayout.enabled) {
  9936. radioButton3.checked = true;
  9937. }
  9938. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9939. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  9940. var graph_generateOptions = document.getElementById("graph_generateOptions");
  9941. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  9942. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  9943. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  9944. if (this.constants.smoothCurves == true) {
  9945. graph_toggleSmooth.style.background = "#A4FF56";
  9946. }
  9947. else {
  9948. graph_toggleSmooth.style.background = "#FF8532";
  9949. }
  9950. switchConfigurations.apply(this);
  9951. radioButton1.onchange = switchConfigurations.bind(this);
  9952. radioButton2.onchange = switchConfigurations.bind(this);
  9953. radioButton3.onchange = switchConfigurations.bind(this);
  9954. }
  9955. },
  9956. /**
  9957. * This overwrites the this.constants.
  9958. *
  9959. * @param constantsVariableName
  9960. * @param value
  9961. * @private
  9962. */
  9963. _overWriteGraphConstants: function (constantsVariableName, value) {
  9964. var nameArray = constantsVariableName.split("_");
  9965. if (nameArray.length == 1) {
  9966. this.constants[nameArray[0]] = value;
  9967. }
  9968. else if (nameArray.length == 2) {
  9969. this.constants[nameArray[0]][nameArray[1]] = value;
  9970. }
  9971. else if (nameArray.length == 3) {
  9972. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  9973. }
  9974. }
  9975. };
  9976. /**
  9977. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  9978. */
  9979. function graphToggleSmoothCurves () {
  9980. this.constants.smoothCurves = !this.constants.smoothCurves;
  9981. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9982. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  9983. else {graph_toggleSmooth.style.background = "#FF8532";}
  9984. this._configureSmoothCurves(false);
  9985. };
  9986. /**
  9987. * this function is used to scramble the nodes
  9988. *
  9989. */
  9990. function graphRepositionNodes () {
  9991. for (var nodeId in this.calculationNodes) {
  9992. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  9993. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  9994. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  9995. }
  9996. }
  9997. if (this.constants.hierarchicalLayout.enabled == true) {
  9998. this._setupHierarchicalLayout();
  9999. }
  10000. else {
  10001. this.repositionNodes();
  10002. }
  10003. this.moving = true;
  10004. this.start();
  10005. };
  10006. /**
  10007. * this is used to generate an options file from the playing with physics system.
  10008. */
  10009. function graphGenerateOptions () {
  10010. var options = "No options are required, default values used.";
  10011. var optionsSpecific = [];
  10012. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10013. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10014. if (radioButton1.checked == true) {
  10015. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  10016. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10017. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10018. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10019. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10020. if (optionsSpecific.length != 0) {
  10021. options = "var options = {";
  10022. options += "physics: {barnesHut: {";
  10023. for (var i = 0; i < optionsSpecific.length; i++) {
  10024. options += optionsSpecific[i];
  10025. if (i < optionsSpecific.length - 1) {
  10026. options += ", "
  10027. }
  10028. }
  10029. options += '}}'
  10030. }
  10031. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10032. if (optionsSpecific.length == 0) {options = "var options = {";}
  10033. else {options += ", "}
  10034. options += "smoothCurves: " + this.constants.smoothCurves;
  10035. }
  10036. if (options != "No options are required, default values used.") {
  10037. options += '};'
  10038. }
  10039. }
  10040. else if (radioButton2.checked == true) {
  10041. options = "var options = {";
  10042. options += "physics: {barnesHut: {enabled: false}";
  10043. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  10044. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10045. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10046. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10047. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10048. if (optionsSpecific.length != 0) {
  10049. options += ", repulsion: {";
  10050. for (var i = 0; i < optionsSpecific.length; i++) {
  10051. options += optionsSpecific[i];
  10052. if (i < optionsSpecific.length - 1) {
  10053. options += ", "
  10054. }
  10055. }
  10056. options += '}}'
  10057. }
  10058. if (optionsSpecific.length == 0) {options += "}"}
  10059. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10060. options += ", smoothCurves: " + this.constants.smoothCurves;
  10061. }
  10062. options += '};'
  10063. }
  10064. else {
  10065. options = "var options = {";
  10066. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  10067. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10068. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10069. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10070. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10071. if (optionsSpecific.length != 0) {
  10072. options += "physics: {hierarchicalRepulsion: {";
  10073. for (var i = 0; i < optionsSpecific.length; i++) {
  10074. options += optionsSpecific[i];
  10075. if (i < optionsSpecific.length - 1) {
  10076. options += ", ";
  10077. }
  10078. }
  10079. options += '}},';
  10080. }
  10081. options += 'hierarchicalLayout: {';
  10082. optionsSpecific = [];
  10083. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  10084. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  10085. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  10086. if (optionsSpecific.length != 0) {
  10087. for (var i = 0; i < optionsSpecific.length; i++) {
  10088. options += optionsSpecific[i];
  10089. if (i < optionsSpecific.length - 1) {
  10090. options += ", "
  10091. }
  10092. }
  10093. options += '}'
  10094. }
  10095. else {
  10096. options += "enabled:true}";
  10097. }
  10098. options += '};'
  10099. }
  10100. this.optionsDiv.innerHTML = options;
  10101. };
  10102. /**
  10103. * this is used to switch between barnesHut, repulsion and hierarchical.
  10104. *
  10105. */
  10106. function switchConfigurations () {
  10107. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  10108. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  10109. var tableId = "graph_" + radioButton + "_table";
  10110. var table = document.getElementById(tableId);
  10111. table.style.display = "block";
  10112. for (var i = 0; i < ids.length; i++) {
  10113. if (ids[i] != tableId) {
  10114. table = document.getElementById(ids[i]);
  10115. table.style.display = "none";
  10116. }
  10117. }
  10118. this._restoreNodes();
  10119. if (radioButton == "R") {
  10120. this.constants.hierarchicalLayout.enabled = false;
  10121. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10122. this.constants.physics.barnesHut.enabled = false;
  10123. }
  10124. else if (radioButton == "H") {
  10125. this.constants.hierarchicalLayout.enabled = true;
  10126. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10127. this.constants.physics.barnesHut.enabled = false;
  10128. this._setupHierarchicalLayout();
  10129. }
  10130. else {
  10131. this.constants.hierarchicalLayout.enabled = false;
  10132. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10133. this.constants.physics.barnesHut.enabled = true;
  10134. }
  10135. this._loadSelectedForceSolver();
  10136. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10137. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10138. else {graph_toggleSmooth.style.background = "#FF8532";}
  10139. this.moving = true;
  10140. this.start();
  10141. }
  10142. /**
  10143. * this generates the ranges depending on the iniital values.
  10144. *
  10145. * @param id
  10146. * @param map
  10147. * @param constantsVariableName
  10148. */
  10149. function showValueOfRange (id,map,constantsVariableName) {
  10150. var valueId = id + "_value";
  10151. var rangeValue = document.getElementById(id).value;
  10152. if (map instanceof Array) {
  10153. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10154. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10155. }
  10156. else {
  10157. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  10158. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  10159. }
  10160. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10161. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10162. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10163. this._setupHierarchicalLayout();
  10164. }
  10165. this.moving = true;
  10166. this.start();
  10167. };
  10168. /**
  10169. * Created by Alex on 2/10/14.
  10170. */
  10171. var hierarchalRepulsionMixin = {
  10172. /**
  10173. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10174. * This field is linearly approximated.
  10175. *
  10176. * @private
  10177. */
  10178. _calculateNodeForces: function () {
  10179. var dx, dy, distance, fx, fy, combinedClusterSize,
  10180. repulsingForce, node1, node2, i, j;
  10181. var nodes = this.calculationNodes;
  10182. var nodeIndices = this.calculationNodeIndices;
  10183. // approximation constants
  10184. var b = 5;
  10185. var a_base = 0.5 * -b;
  10186. // repulsing forces between nodes
  10187. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10188. var minimumDistance = nodeDistance;
  10189. // we loop from i over all but the last entree in the array
  10190. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10191. for (i = 0; i < nodeIndices.length - 1; i++) {
  10192. node1 = nodes[nodeIndices[i]];
  10193. for (j = i + 1; j < nodeIndices.length; j++) {
  10194. node2 = nodes[nodeIndices[j]];
  10195. dx = node2.x - node1.x;
  10196. dy = node2.y - node1.y;
  10197. distance = Math.sqrt(dx * dx + dy * dy);
  10198. var a = a_base / minimumDistance;
  10199. if (distance < 2 * minimumDistance) {
  10200. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10201. // normalize force with
  10202. if (distance == 0) {
  10203. distance = 0.01;
  10204. }
  10205. else {
  10206. repulsingForce = repulsingForce / distance;
  10207. }
  10208. fx = dx * repulsingForce;
  10209. fy = dy * repulsingForce;
  10210. node1.fx -= fx;
  10211. node1.fy -= fy;
  10212. node2.fx += fx;
  10213. node2.fy += fy;
  10214. }
  10215. }
  10216. }
  10217. }
  10218. };
  10219. /**
  10220. * Created by Alex on 2/10/14.
  10221. */
  10222. var barnesHutMixin = {
  10223. /**
  10224. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10225. * The Barnes Hut method is used to speed up this N-body simulation.
  10226. *
  10227. * @private
  10228. */
  10229. _calculateNodeForces : function() {
  10230. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  10231. var node;
  10232. var nodes = this.calculationNodes;
  10233. var nodeIndices = this.calculationNodeIndices;
  10234. var nodeCount = nodeIndices.length;
  10235. this._formBarnesHutTree(nodes,nodeIndices);
  10236. var barnesHutTree = this.barnesHutTree;
  10237. // place the nodes one by one recursively
  10238. for (var i = 0; i < nodeCount; i++) {
  10239. node = nodes[nodeIndices[i]];
  10240. // starting with root is irrelevant, it never passes the BarnesHut condition
  10241. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10242. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10243. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10244. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10245. }
  10246. }
  10247. },
  10248. /**
  10249. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10250. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10251. *
  10252. * @param parentBranch
  10253. * @param node
  10254. * @private
  10255. */
  10256. _getForceContribution : function(parentBranch,node) {
  10257. // we get no force contribution from an empty region
  10258. if (parentBranch.childrenCount > 0) {
  10259. var dx,dy,distance;
  10260. // get the distance from the center of mass to the node.
  10261. dx = parentBranch.centerOfMass.x - node.x;
  10262. dy = parentBranch.centerOfMass.y - node.y;
  10263. distance = Math.sqrt(dx * dx + dy * dy);
  10264. // BarnesHut condition
  10265. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10266. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10267. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10268. // duplicate code to reduce function calls to speed up program
  10269. if (distance == 0) {
  10270. distance = 0.1*Math.random();
  10271. dx = distance;
  10272. }
  10273. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10274. var fx = dx * gravityForce;
  10275. var fy = dy * gravityForce;
  10276. node.fx += fx;
  10277. node.fy += fy;
  10278. }
  10279. else {
  10280. // Did not pass the condition, go into children if available
  10281. if (parentBranch.childrenCount == 4) {
  10282. this._getForceContribution(parentBranch.children.NW,node);
  10283. this._getForceContribution(parentBranch.children.NE,node);
  10284. this._getForceContribution(parentBranch.children.SW,node);
  10285. this._getForceContribution(parentBranch.children.SE,node);
  10286. }
  10287. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10288. if (parentBranch.children.data.id != node.id) { // if it is not self
  10289. // duplicate code to reduce function calls to speed up program
  10290. if (distance == 0) {
  10291. distance = 0.5*Math.random();
  10292. dx = distance;
  10293. }
  10294. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10295. var fx = dx * gravityForce;
  10296. var fy = dy * gravityForce;
  10297. node.fx += fx;
  10298. node.fy += fy;
  10299. }
  10300. }
  10301. }
  10302. }
  10303. },
  10304. /**
  10305. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10306. *
  10307. * @param nodes
  10308. * @param nodeIndices
  10309. * @private
  10310. */
  10311. _formBarnesHutTree : function(nodes,nodeIndices) {
  10312. var node;
  10313. var nodeCount = nodeIndices.length;
  10314. var minX = Number.MAX_VALUE,
  10315. minY = Number.MAX_VALUE,
  10316. maxX =-Number.MAX_VALUE,
  10317. maxY =-Number.MAX_VALUE;
  10318. // get the range of the nodes
  10319. for (var i = 0; i < nodeCount; i++) {
  10320. var x = nodes[nodeIndices[i]].x;
  10321. var y = nodes[nodeIndices[i]].y;
  10322. if (x < minX) { minX = x; }
  10323. if (x > maxX) { maxX = x; }
  10324. if (y < minY) { minY = y; }
  10325. if (y > maxY) { maxY = y; }
  10326. }
  10327. // make the range a square
  10328. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10329. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10330. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10331. var minimumTreeSize = 1e-5;
  10332. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10333. var halfRootSize = 0.5 * rootSize;
  10334. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10335. // construct the barnesHutTree
  10336. var barnesHutTree = {root:{
  10337. centerOfMass:{x:0,y:0}, // Center of Mass
  10338. mass:0,
  10339. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10340. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10341. size: rootSize,
  10342. calcSize: 1 / rootSize,
  10343. children: {data:null},
  10344. maxWidth: 0,
  10345. level: 0,
  10346. childrenCount: 4
  10347. }};
  10348. this._splitBranch(barnesHutTree.root);
  10349. // place the nodes one by one recursively
  10350. for (i = 0; i < nodeCount; i++) {
  10351. node = nodes[nodeIndices[i]];
  10352. this._placeInTree(barnesHutTree.root,node);
  10353. }
  10354. // make global
  10355. this.barnesHutTree = barnesHutTree
  10356. },
  10357. /**
  10358. * this updates the mass of a branch. this is increased by adding a node.
  10359. *
  10360. * @param parentBranch
  10361. * @param node
  10362. * @private
  10363. */
  10364. _updateBranchMass : function(parentBranch, node) {
  10365. var totalMass = parentBranch.mass + node.mass;
  10366. var totalMassInv = 1/totalMass;
  10367. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10368. parentBranch.centerOfMass.x *= totalMassInv;
  10369. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10370. parentBranch.centerOfMass.y *= totalMassInv;
  10371. parentBranch.mass = totalMass;
  10372. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10373. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10374. },
  10375. /**
  10376. * determine in which branch the node will be placed.
  10377. *
  10378. * @param parentBranch
  10379. * @param node
  10380. * @param skipMassUpdate
  10381. * @private
  10382. */
  10383. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10384. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10385. // update the mass of the branch.
  10386. this._updateBranchMass(parentBranch,node);
  10387. }
  10388. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10389. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10390. this._placeInRegion(parentBranch,node,"NW");
  10391. }
  10392. else { // in SW
  10393. this._placeInRegion(parentBranch,node,"SW");
  10394. }
  10395. }
  10396. else { // in NE or SE
  10397. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10398. this._placeInRegion(parentBranch,node,"NE");
  10399. }
  10400. else { // in SE
  10401. this._placeInRegion(parentBranch,node,"SE");
  10402. }
  10403. }
  10404. },
  10405. /**
  10406. * actually place the node in a region (or branch)
  10407. *
  10408. * @param parentBranch
  10409. * @param node
  10410. * @param region
  10411. * @private
  10412. */
  10413. _placeInRegion : function(parentBranch,node,region) {
  10414. switch (parentBranch.children[region].childrenCount) {
  10415. case 0: // place node here
  10416. parentBranch.children[region].children.data = node;
  10417. parentBranch.children[region].childrenCount = 1;
  10418. this._updateBranchMass(parentBranch.children[region],node);
  10419. break;
  10420. case 1: // convert into children
  10421. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10422. // we move one node a pixel and we do not put it in the tree.
  10423. if (parentBranch.children[region].children.data.x == node.x &&
  10424. parentBranch.children[region].children.data.y == node.y) {
  10425. node.x += Math.random();
  10426. node.y += Math.random();
  10427. }
  10428. else {
  10429. this._splitBranch(parentBranch.children[region]);
  10430. this._placeInTree(parentBranch.children[region],node);
  10431. }
  10432. break;
  10433. case 4: // place in branch
  10434. this._placeInTree(parentBranch.children[region],node);
  10435. break;
  10436. }
  10437. },
  10438. /**
  10439. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10440. * after the split is complete.
  10441. *
  10442. * @param parentBranch
  10443. * @private
  10444. */
  10445. _splitBranch : function(parentBranch) {
  10446. // if the branch is filled with a node, replace the node in the new subset.
  10447. var containedNode = null;
  10448. if (parentBranch.childrenCount == 1) {
  10449. containedNode = parentBranch.children.data;
  10450. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  10451. }
  10452. parentBranch.childrenCount = 4;
  10453. parentBranch.children.data = null;
  10454. this._insertRegion(parentBranch,"NW");
  10455. this._insertRegion(parentBranch,"NE");
  10456. this._insertRegion(parentBranch,"SW");
  10457. this._insertRegion(parentBranch,"SE");
  10458. if (containedNode != null) {
  10459. this._placeInTree(parentBranch,containedNode);
  10460. }
  10461. },
  10462. /**
  10463. * This function subdivides the region into four new segments.
  10464. * Specifically, this inserts a single new segment.
  10465. * It fills the children section of the parentBranch
  10466. *
  10467. * @param parentBranch
  10468. * @param region
  10469. * @param parentRange
  10470. * @private
  10471. */
  10472. _insertRegion : function(parentBranch, region) {
  10473. var minX,maxX,minY,maxY;
  10474. var childSize = 0.5 * parentBranch.size;
  10475. switch (region) {
  10476. case "NW":
  10477. minX = parentBranch.range.minX;
  10478. maxX = parentBranch.range.minX + childSize;
  10479. minY = parentBranch.range.minY;
  10480. maxY = parentBranch.range.minY + childSize;
  10481. break;
  10482. case "NE":
  10483. minX = parentBranch.range.minX + childSize;
  10484. maxX = parentBranch.range.maxX;
  10485. minY = parentBranch.range.minY;
  10486. maxY = parentBranch.range.minY + childSize;
  10487. break;
  10488. case "SW":
  10489. minX = parentBranch.range.minX;
  10490. maxX = parentBranch.range.minX + childSize;
  10491. minY = parentBranch.range.minY + childSize;
  10492. maxY = parentBranch.range.maxY;
  10493. break;
  10494. case "SE":
  10495. minX = parentBranch.range.minX + childSize;
  10496. maxX = parentBranch.range.maxX;
  10497. minY = parentBranch.range.minY + childSize;
  10498. maxY = parentBranch.range.maxY;
  10499. break;
  10500. }
  10501. parentBranch.children[region] = {
  10502. centerOfMass:{x:0,y:0},
  10503. mass:0,
  10504. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  10505. size: 0.5 * parentBranch.size,
  10506. calcSize: 2 * parentBranch.calcSize,
  10507. children: {data:null},
  10508. maxWidth: 0,
  10509. level: parentBranch.level+1,
  10510. childrenCount: 0
  10511. };
  10512. },
  10513. /**
  10514. * This function is for debugging purposed, it draws the tree.
  10515. *
  10516. * @param ctx
  10517. * @param color
  10518. * @private
  10519. */
  10520. _drawTree : function(ctx,color) {
  10521. if (this.barnesHutTree !== undefined) {
  10522. ctx.lineWidth = 1;
  10523. this._drawBranch(this.barnesHutTree.root,ctx,color);
  10524. }
  10525. },
  10526. /**
  10527. * This function is for debugging purposes. It draws the branches recursively.
  10528. *
  10529. * @param branch
  10530. * @param ctx
  10531. * @param color
  10532. * @private
  10533. */
  10534. _drawBranch : function(branch,ctx,color) {
  10535. if (color === undefined) {
  10536. color = "#FF0000";
  10537. }
  10538. if (branch.childrenCount == 4) {
  10539. this._drawBranch(branch.children.NW,ctx);
  10540. this._drawBranch(branch.children.NE,ctx);
  10541. this._drawBranch(branch.children.SE,ctx);
  10542. this._drawBranch(branch.children.SW,ctx);
  10543. }
  10544. ctx.strokeStyle = color;
  10545. ctx.beginPath();
  10546. ctx.moveTo(branch.range.minX,branch.range.minY);
  10547. ctx.lineTo(branch.range.maxX,branch.range.minY);
  10548. ctx.stroke();
  10549. ctx.beginPath();
  10550. ctx.moveTo(branch.range.maxX,branch.range.minY);
  10551. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  10552. ctx.stroke();
  10553. ctx.beginPath();
  10554. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  10555. ctx.lineTo(branch.range.minX,branch.range.maxY);
  10556. ctx.stroke();
  10557. ctx.beginPath();
  10558. ctx.moveTo(branch.range.minX,branch.range.maxY);
  10559. ctx.lineTo(branch.range.minX,branch.range.minY);
  10560. ctx.stroke();
  10561. /*
  10562. if (branch.mass > 0) {
  10563. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  10564. ctx.stroke();
  10565. }
  10566. */
  10567. }
  10568. };
  10569. /**
  10570. * Created by Alex on 2/10/14.
  10571. */
  10572. var repulsionMixin = {
  10573. /**
  10574. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10575. * This field is linearly approximated.
  10576. *
  10577. * @private
  10578. */
  10579. _calculateNodeForces: function () {
  10580. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  10581. repulsingForce, node1, node2, i, j;
  10582. var nodes = this.calculationNodes;
  10583. var nodeIndices = this.calculationNodeIndices;
  10584. // approximation constants
  10585. var a_base = -2 / 3;
  10586. var b = 4 / 3;
  10587. // repulsing forces between nodes
  10588. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  10589. var minimumDistance = nodeDistance;
  10590. // we loop from i over all but the last entree in the array
  10591. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10592. for (i = 0; i < nodeIndices.length - 1; i++) {
  10593. node1 = nodes[nodeIndices[i]];
  10594. for (j = i + 1; j < nodeIndices.length; j++) {
  10595. node2 = nodes[nodeIndices[j]];
  10596. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  10597. dx = node2.x - node1.x;
  10598. dy = node2.y - node1.y;
  10599. distance = Math.sqrt(dx * dx + dy * dy);
  10600. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  10601. var a = a_base / minimumDistance;
  10602. if (distance < 2 * minimumDistance) {
  10603. if (distance < 0.5 * minimumDistance) {
  10604. repulsingForce = 1.0;
  10605. }
  10606. else {
  10607. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10608. }
  10609. // amplify the repulsion for clusters.
  10610. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  10611. repulsingForce = repulsingForce / distance;
  10612. fx = dx * repulsingForce;
  10613. fy = dy * repulsingForce;
  10614. node1.fx -= fx;
  10615. node1.fy -= fy;
  10616. node2.fx += fx;
  10617. node2.fy += fy;
  10618. }
  10619. }
  10620. }
  10621. }
  10622. };
  10623. var HierarchicalLayoutMixin = {
  10624. _resetLevels : function() {
  10625. for (var nodeId in this.nodes) {
  10626. if (this.nodes.hasOwnProperty(nodeId)) {
  10627. var node = this.nodes[nodeId];
  10628. if (node.preassignedLevel == false) {
  10629. node.level = -1;
  10630. }
  10631. }
  10632. }
  10633. },
  10634. /**
  10635. * This is the main function to layout the nodes in a hierarchical way.
  10636. * It checks if the node details are supplied correctly
  10637. *
  10638. * @private
  10639. */
  10640. _setupHierarchicalLayout : function() {
  10641. if (this.constants.hierarchicalLayout.enabled == true) {
  10642. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  10643. this.constants.hierarchicalLayout.levelSeparation *= -1;
  10644. }
  10645. else {
  10646. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  10647. }
  10648. // get the size of the largest hubs and check if the user has defined a level for a node.
  10649. var hubsize = 0;
  10650. var node, nodeId;
  10651. var definedLevel = false;
  10652. var undefinedLevel = false;
  10653. for (nodeId in this.nodes) {
  10654. if (this.nodes.hasOwnProperty(nodeId)) {
  10655. node = this.nodes[nodeId];
  10656. if (node.level != -1) {
  10657. definedLevel = true;
  10658. }
  10659. else {
  10660. undefinedLevel = true;
  10661. }
  10662. if (hubsize < node.edges.length) {
  10663. hubsize = node.edges.length;
  10664. }
  10665. }
  10666. }
  10667. // if the user defined some levels but not all, alert and run without hierarchical layout
  10668. if (undefinedLevel == true && definedLevel == true) {
  10669. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  10670. this.zoomExtent(true,this.constants.clustering.enabled);
  10671. if (!this.constants.clustering.enabled) {
  10672. this.start();
  10673. }
  10674. }
  10675. else {
  10676. // setup the system to use hierarchical method.
  10677. this._changeConstants();
  10678. // define levels if undefined by the users. Based on hubsize
  10679. if (undefinedLevel == true) {
  10680. this._determineLevels(hubsize);
  10681. }
  10682. // check the distribution of the nodes per level.
  10683. var distribution = this._getDistribution();
  10684. // place the nodes on the canvas. This also stablilizes the system.
  10685. this._placeNodesByHierarchy(distribution);
  10686. // start the simulation.
  10687. this.start();
  10688. }
  10689. }
  10690. },
  10691. /**
  10692. * This function places the nodes on the canvas based on the hierarchial distribution.
  10693. *
  10694. * @param {Object} distribution | obtained by the function this._getDistribution()
  10695. * @private
  10696. */
  10697. _placeNodesByHierarchy : function(distribution) {
  10698. var nodeId, node;
  10699. // start placing all the level 0 nodes first. Then recursively position their branches.
  10700. for (nodeId in distribution[0].nodes) {
  10701. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  10702. node = distribution[0].nodes[nodeId];
  10703. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10704. if (node.xFixed) {
  10705. node.x = distribution[0].minPos;
  10706. node.xFixed = false;
  10707. distribution[0].minPos += distribution[0].nodeSpacing;
  10708. }
  10709. }
  10710. else {
  10711. if (node.yFixed) {
  10712. node.y = distribution[0].minPos;
  10713. node.yFixed = false;
  10714. distribution[0].minPos += distribution[0].nodeSpacing;
  10715. }
  10716. }
  10717. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  10718. }
  10719. }
  10720. // stabilize the system after positioning. This function calls zoomExtent.
  10721. this._stabilize();
  10722. },
  10723. /**
  10724. * This function get the distribution of levels based on hubsize
  10725. *
  10726. * @returns {Object}
  10727. * @private
  10728. */
  10729. _getDistribution : function() {
  10730. var distribution = {};
  10731. var nodeId, node, level;
  10732. // 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.
  10733. // the fix of X is removed after the x value has been set.
  10734. for (nodeId in this.nodes) {
  10735. if (this.nodes.hasOwnProperty(nodeId)) {
  10736. node = this.nodes[nodeId];
  10737. node.xFixed = true;
  10738. node.yFixed = true;
  10739. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10740. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10741. }
  10742. else {
  10743. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10744. }
  10745. if (!distribution.hasOwnProperty(node.level)) {
  10746. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  10747. }
  10748. distribution[node.level].amount += 1;
  10749. distribution[node.level].nodes[node.id] = node;
  10750. }
  10751. }
  10752. // determine the largest amount of nodes of all levels
  10753. var maxCount = 0;
  10754. for (level in distribution) {
  10755. if (distribution.hasOwnProperty(level)) {
  10756. if (maxCount < distribution[level].amount) {
  10757. maxCount = distribution[level].amount;
  10758. }
  10759. }
  10760. }
  10761. // set the initial position and spacing of each nodes accordingly
  10762. for (level in distribution) {
  10763. if (distribution.hasOwnProperty(level)) {
  10764. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  10765. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  10766. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  10767. }
  10768. }
  10769. return distribution;
  10770. },
  10771. /**
  10772. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  10773. *
  10774. * @param hubsize
  10775. * @private
  10776. */
  10777. _determineLevels : function(hubsize) {
  10778. var nodeId, node;
  10779. // determine hubs
  10780. for (nodeId in this.nodes) {
  10781. if (this.nodes.hasOwnProperty(nodeId)) {
  10782. node = this.nodes[nodeId];
  10783. if (node.edges.length == hubsize) {
  10784. node.level = 0;
  10785. }
  10786. }
  10787. }
  10788. // branch from hubs
  10789. for (nodeId in this.nodes) {
  10790. if (this.nodes.hasOwnProperty(nodeId)) {
  10791. node = this.nodes[nodeId];
  10792. if (node.level == 0) {
  10793. this._setLevel(1,node.edges,node.id);
  10794. }
  10795. }
  10796. }
  10797. },
  10798. /**
  10799. * Since hierarchical layout does not support:
  10800. * - smooth curves (based on the physics),
  10801. * - clustering (based on dynamic node counts)
  10802. *
  10803. * We disable both features so there will be no problems.
  10804. *
  10805. * @private
  10806. */
  10807. _changeConstants : function() {
  10808. this.constants.clustering.enabled = false;
  10809. this.constants.physics.barnesHut.enabled = false;
  10810. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10811. this._loadSelectedForceSolver();
  10812. this.constants.smoothCurves = false;
  10813. this._configureSmoothCurves();
  10814. },
  10815. /**
  10816. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  10817. * on a X position that ensures there will be no overlap.
  10818. *
  10819. * @param edges
  10820. * @param parentId
  10821. * @param distribution
  10822. * @param parentLevel
  10823. * @private
  10824. */
  10825. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  10826. for (var i = 0; i < edges.length; i++) {
  10827. var childNode = null;
  10828. if (edges[i].toId == parentId) {
  10829. childNode = edges[i].from;
  10830. }
  10831. else {
  10832. childNode = edges[i].to;
  10833. }
  10834. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  10835. var nodeMoved = false;
  10836. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10837. if (childNode.xFixed && childNode.level > parentLevel) {
  10838. childNode.xFixed = false;
  10839. childNode.x = distribution[childNode.level].minPos;
  10840. nodeMoved = true;
  10841. }
  10842. }
  10843. else {
  10844. if (childNode.yFixed && childNode.level > parentLevel) {
  10845. childNode.yFixed = false;
  10846. childNode.y = distribution[childNode.level].minPos;
  10847. nodeMoved = true;
  10848. }
  10849. }
  10850. if (nodeMoved == true) {
  10851. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  10852. if (childNode.edges.length > 1) {
  10853. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  10854. }
  10855. }
  10856. }
  10857. },
  10858. /**
  10859. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  10860. *
  10861. * @param level
  10862. * @param edges
  10863. * @param parentId
  10864. * @private
  10865. */
  10866. _setLevel : function(level, edges, parentId) {
  10867. for (var i = 0; i < edges.length; i++) {
  10868. var childNode = null;
  10869. if (edges[i].toId == parentId) {
  10870. childNode = edges[i].from;
  10871. }
  10872. else {
  10873. childNode = edges[i].to;
  10874. }
  10875. if (childNode.level == -1 || childNode.level > level) {
  10876. childNode.level = level;
  10877. if (edges.length > 1) {
  10878. this._setLevel(level+1, childNode.edges, childNode.id);
  10879. }
  10880. }
  10881. }
  10882. },
  10883. /**
  10884. * Unfix nodes
  10885. *
  10886. * @private
  10887. */
  10888. _restoreNodes : function() {
  10889. for (nodeId in this.nodes) {
  10890. if (this.nodes.hasOwnProperty(nodeId)) {
  10891. this.nodes[nodeId].xFixed = false;
  10892. this.nodes[nodeId].yFixed = false;
  10893. }
  10894. }
  10895. }
  10896. };
  10897. /**
  10898. * Created by Alex on 2/4/14.
  10899. */
  10900. var manipulationMixin = {
  10901. /**
  10902. * clears the toolbar div element of children
  10903. *
  10904. * @private
  10905. */
  10906. _clearManipulatorBar : function() {
  10907. while (this.manipulationDiv.hasChildNodes()) {
  10908. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10909. }
  10910. },
  10911. /**
  10912. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  10913. * these functions to their original functionality, we saved them in this.cachedFunctions.
  10914. * This function restores these functions to their original function.
  10915. *
  10916. * @private
  10917. */
  10918. _restoreOverloadedFunctions : function() {
  10919. for (var functionName in this.cachedFunctions) {
  10920. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  10921. this[functionName] = this.cachedFunctions[functionName];
  10922. }
  10923. }
  10924. },
  10925. /**
  10926. * Enable or disable edit-mode.
  10927. *
  10928. * @private
  10929. */
  10930. _toggleEditMode : function() {
  10931. this.editMode = !this.editMode;
  10932. var toolbar = document.getElementById("graph-manipulationDiv");
  10933. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  10934. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  10935. if (this.editMode == true) {
  10936. toolbar.style.display="block";
  10937. closeDiv.style.display="block";
  10938. editModeDiv.style.display="none";
  10939. closeDiv.onclick = this._toggleEditMode.bind(this);
  10940. }
  10941. else {
  10942. toolbar.style.display="none";
  10943. closeDiv.style.display="none";
  10944. editModeDiv.style.display="block";
  10945. closeDiv.onclick = null;
  10946. }
  10947. this._createManipulatorBar()
  10948. },
  10949. /**
  10950. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  10951. *
  10952. * @private
  10953. */
  10954. _createManipulatorBar : function() {
  10955. // remove bound functions
  10956. if (this.boundFunction) {
  10957. this.off('select', this.boundFunction);
  10958. }
  10959. // restore overloaded functions
  10960. this._restoreOverloadedFunctions();
  10961. // resume calculation
  10962. this.freezeSimulation = false;
  10963. // reset global variables
  10964. this.blockConnectingEdgeSelection = false;
  10965. this.forceAppendSelection = false;
  10966. if (this.editMode == true) {
  10967. while (this.manipulationDiv.hasChildNodes()) {
  10968. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10969. }
  10970. // add the icons to the manipulator div
  10971. this.manipulationDiv.innerHTML = "" +
  10972. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  10973. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  10974. "<div class='graph-seperatorLine'></div>" +
  10975. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  10976. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  10977. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10978. this.manipulationDiv.innerHTML += "" +
  10979. "<div class='graph-seperatorLine'></div>" +
  10980. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  10981. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  10982. }
  10983. if (this._selectionIsEmpty() == false) {
  10984. this.manipulationDiv.innerHTML += "" +
  10985. "<div class='graph-seperatorLine'></div>" +
  10986. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  10987. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  10988. }
  10989. // bind the icons
  10990. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  10991. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  10992. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  10993. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  10994. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10995. var editButton = document.getElementById("graph-manipulate-editNode");
  10996. editButton.onclick = this._editNode.bind(this);
  10997. }
  10998. if (this._selectionIsEmpty() == false) {
  10999. var deleteButton = document.getElementById("graph-manipulate-delete");
  11000. deleteButton.onclick = this._deleteSelected.bind(this);
  11001. }
  11002. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11003. closeDiv.onclick = this._toggleEditMode.bind(this);
  11004. this.boundFunction = this._createManipulatorBar.bind(this);
  11005. this.on('select', this.boundFunction);
  11006. }
  11007. else {
  11008. this.editModeDiv.innerHTML = "" +
  11009. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  11010. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  11011. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  11012. editModeButton.onclick = this._toggleEditMode.bind(this);
  11013. }
  11014. },
  11015. /**
  11016. * Create the toolbar for adding Nodes
  11017. *
  11018. * @private
  11019. */
  11020. _createAddNodeToolbar : function() {
  11021. // clear the toolbar
  11022. this._clearManipulatorBar();
  11023. if (this.boundFunction) {
  11024. this.off('select', this.boundFunction);
  11025. }
  11026. // create the toolbar contents
  11027. this.manipulationDiv.innerHTML = "" +
  11028. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11029. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11030. "<div class='graph-seperatorLine'></div>" +
  11031. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11032. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  11033. // bind the icon
  11034. var backButton = document.getElementById("graph-manipulate-back");
  11035. backButton.onclick = this._createManipulatorBar.bind(this);
  11036. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11037. this.boundFunction = this._addNode.bind(this);
  11038. this.on('select', this.boundFunction);
  11039. },
  11040. /**
  11041. * create the toolbar to connect nodes
  11042. *
  11043. * @private
  11044. */
  11045. _createAddEdgeToolbar : function() {
  11046. // clear the toolbar
  11047. this._clearManipulatorBar();
  11048. this._unselectAll(true);
  11049. this.freezeSimulation = true;
  11050. if (this.boundFunction) {
  11051. this.off('select', this.boundFunction);
  11052. }
  11053. this._unselectAll();
  11054. this.forceAppendSelection = false;
  11055. this.blockConnectingEdgeSelection = true;
  11056. this.manipulationDiv.innerHTML = "" +
  11057. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11058. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11059. "<div class='graph-seperatorLine'></div>" +
  11060. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11061. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  11062. // bind the icon
  11063. var backButton = document.getElementById("graph-manipulate-back");
  11064. backButton.onclick = this._createManipulatorBar.bind(this);
  11065. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11066. this.boundFunction = this._handleConnect.bind(this);
  11067. this.on('select', this.boundFunction);
  11068. // temporarily overload functions
  11069. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  11070. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  11071. this._handleTouch = this._handleConnect;
  11072. this._handleOnRelease = this._finishConnect;
  11073. // redraw to show the unselect
  11074. this._redraw();
  11075. },
  11076. /**
  11077. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  11078. * to walk the user through the process.
  11079. *
  11080. * @private
  11081. */
  11082. _handleConnect : function(pointer) {
  11083. if (this._getSelectedNodeCount() == 0) {
  11084. var node = this._getNodeAt(pointer);
  11085. if (node != null) {
  11086. if (node.clusterSize > 1) {
  11087. alert("Cannot create edges to a cluster.")
  11088. }
  11089. else {
  11090. this._selectObject(node,false);
  11091. // create a node the temporary line can look at
  11092. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  11093. this.sectors['support']['nodes']['targetNode'].x = node.x;
  11094. this.sectors['support']['nodes']['targetNode'].y = node.y;
  11095. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  11096. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  11097. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  11098. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  11099. // create a temporary edge
  11100. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  11101. this.edges['connectionEdge'].from = node;
  11102. this.edges['connectionEdge'].connected = true;
  11103. this.edges['connectionEdge'].smooth = true;
  11104. this.edges['connectionEdge'].selected = true;
  11105. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  11106. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  11107. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  11108. this._handleOnDrag = function(event) {
  11109. var pointer = this._getPointer(event.gesture.center);
  11110. this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
  11111. this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
  11112. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
  11113. this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
  11114. };
  11115. this.moving = true;
  11116. this.start();
  11117. }
  11118. }
  11119. }
  11120. },
  11121. _finishConnect : function(pointer) {
  11122. if (this._getSelectedNodeCount() == 1) {
  11123. // restore the drag function
  11124. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  11125. delete this.cachedFunctions["_handleOnDrag"];
  11126. // remember the edge id
  11127. var connectFromId = this.edges['connectionEdge'].fromId;
  11128. // remove the temporary nodes and edge
  11129. delete this.edges['connectionEdge'];
  11130. delete this.sectors['support']['nodes']['targetNode'];
  11131. delete this.sectors['support']['nodes']['targetViaNode'];
  11132. var node = this._getNodeAt(pointer);
  11133. if (node != null) {
  11134. if (node.clusterSize > 1) {
  11135. alert("Cannot create edges to a cluster.")
  11136. }
  11137. else {
  11138. this._createEdge(connectFromId,node.id);
  11139. this._createManipulatorBar();
  11140. }
  11141. }
  11142. this._unselectAll();
  11143. }
  11144. },
  11145. /**
  11146. * Adds a node on the specified location
  11147. */
  11148. _addNode : function() {
  11149. if (this._selectionIsEmpty() && this.editMode == true) {
  11150. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  11151. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  11152. if (this.triggerFunctions.add) {
  11153. if (this.triggerFunctions.add.length == 2) {
  11154. var me = this;
  11155. this.triggerFunctions.add(defaultData, function(finalizedData) {
  11156. me.nodesData.add(finalizedData);
  11157. me._createManipulatorBar();
  11158. me.moving = true;
  11159. me.start();
  11160. });
  11161. }
  11162. else {
  11163. alert(this.constants.labels['addError']);
  11164. this._createManipulatorBar();
  11165. this.moving = true;
  11166. this.start();
  11167. }
  11168. }
  11169. else {
  11170. this.nodesData.add(defaultData);
  11171. this._createManipulatorBar();
  11172. this.moving = true;
  11173. this.start();
  11174. }
  11175. }
  11176. },
  11177. /**
  11178. * connect two nodes with a new edge.
  11179. *
  11180. * @private
  11181. */
  11182. _createEdge : function(sourceNodeId,targetNodeId) {
  11183. if (this.editMode == true) {
  11184. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11185. if (this.triggerFunctions.connect) {
  11186. if (this.triggerFunctions.connect.length == 2) {
  11187. var me = this;
  11188. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11189. me.edgesData.add(finalizedData);
  11190. me.moving = true;
  11191. me.start();
  11192. });
  11193. }
  11194. else {
  11195. alert(this.constants.labels["linkError"]);
  11196. this.moving = true;
  11197. this.start();
  11198. }
  11199. }
  11200. else {
  11201. this.edgesData.add(defaultData);
  11202. this.moving = true;
  11203. this.start();
  11204. }
  11205. }
  11206. },
  11207. /**
  11208. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11209. *
  11210. * @private
  11211. */
  11212. _editNode : function() {
  11213. if (this.triggerFunctions.edit && this.editMode == true) {
  11214. var node = this._getSelectedNode();
  11215. var data = {id:node.id,
  11216. label: node.label,
  11217. group: node.group,
  11218. shape: node.shape,
  11219. color: {
  11220. background:node.color.background,
  11221. border:node.color.border,
  11222. highlight: {
  11223. background:node.color.highlight.background,
  11224. border:node.color.highlight.border
  11225. }
  11226. }};
  11227. if (this.triggerFunctions.edit.length == 2) {
  11228. var me = this;
  11229. this.triggerFunctions.edit(data, function (finalizedData) {
  11230. me.nodesData.update(finalizedData);
  11231. me._createManipulatorBar();
  11232. me.moving = true;
  11233. me.start();
  11234. });
  11235. }
  11236. else {
  11237. alert(this.constants.labels["editError"]);
  11238. }
  11239. }
  11240. else {
  11241. alert(this.constants.labels["editBoundError"]);
  11242. }
  11243. },
  11244. /**
  11245. * delete everything in the selection
  11246. *
  11247. * @private
  11248. */
  11249. _deleteSelected : function() {
  11250. if (!this._selectionIsEmpty() && this.editMode == true) {
  11251. if (!this._clusterInSelection()) {
  11252. var selectedNodes = this.getSelectedNodes();
  11253. var selectedEdges = this.getSelectedEdges();
  11254. if (this.triggerFunctions.del) {
  11255. var me = this;
  11256. var data = {nodes: selectedNodes, edges: selectedEdges};
  11257. if (this.triggerFunctions.del.length = 2) {
  11258. this.triggerFunctions.del(data, function (finalizedData) {
  11259. me.edgesData.remove(finalizedData.edges);
  11260. me.nodesData.remove(finalizedData.nodes);
  11261. me._unselectAll();
  11262. me.moving = true;
  11263. me.start();
  11264. });
  11265. }
  11266. else {
  11267. alert(this.constants.labels["deleteError"])
  11268. }
  11269. }
  11270. else {
  11271. this.edgesData.remove(selectedEdges);
  11272. this.nodesData.remove(selectedNodes);
  11273. this._unselectAll();
  11274. this.moving = true;
  11275. this.start();
  11276. }
  11277. }
  11278. else {
  11279. alert(this.constants.labels["deleteClusterError"]);
  11280. }
  11281. }
  11282. }
  11283. };
  11284. /**
  11285. * Creation of the SectorMixin var.
  11286. *
  11287. * This contains all the functions the Graph object can use to employ the sector system.
  11288. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11289. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11290. *
  11291. * Alex de Mulder
  11292. * 21-01-2013
  11293. */
  11294. var SectorMixin = {
  11295. /**
  11296. * This function is only called by the setData function of the Graph object.
  11297. * This loads the global references into the active sector. This initializes the sector.
  11298. *
  11299. * @private
  11300. */
  11301. _putDataInSector : function() {
  11302. this.sectors["active"][this._sector()].nodes = this.nodes;
  11303. this.sectors["active"][this._sector()].edges = this.edges;
  11304. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11305. },
  11306. /**
  11307. * /**
  11308. * This function sets the global references to nodes, edges and nodeIndices back to
  11309. * those of the supplied (active) sector. If a type is defined, do the specific type
  11310. *
  11311. * @param {String} sectorId
  11312. * @param {String} [sectorType] | "active" or "frozen"
  11313. * @private
  11314. */
  11315. _switchToSector : function(sectorId, sectorType) {
  11316. if (sectorType === undefined || sectorType == "active") {
  11317. this._switchToActiveSector(sectorId);
  11318. }
  11319. else {
  11320. this._switchToFrozenSector(sectorId);
  11321. }
  11322. },
  11323. /**
  11324. * This function sets the global references to nodes, edges and nodeIndices back to
  11325. * those of the supplied active sector.
  11326. *
  11327. * @param sectorId
  11328. * @private
  11329. */
  11330. _switchToActiveSector : function(sectorId) {
  11331. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11332. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11333. this.edges = this.sectors["active"][sectorId]["edges"];
  11334. },
  11335. /**
  11336. * This function sets the global references to nodes, edges and nodeIndices back to
  11337. * those of the supplied active sector.
  11338. *
  11339. * @param sectorId
  11340. * @private
  11341. */
  11342. _switchToSupportSector : function() {
  11343. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11344. this.nodes = this.sectors["support"]["nodes"];
  11345. this.edges = this.sectors["support"]["edges"];
  11346. },
  11347. /**
  11348. * This function sets the global references to nodes, edges and nodeIndices back to
  11349. * those of the supplied frozen sector.
  11350. *
  11351. * @param sectorId
  11352. * @private
  11353. */
  11354. _switchToFrozenSector : function(sectorId) {
  11355. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11356. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11357. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11358. },
  11359. /**
  11360. * This function sets the global references to nodes, edges and nodeIndices back to
  11361. * those of the currently active sector.
  11362. *
  11363. * @private
  11364. */
  11365. _loadLatestSector : function() {
  11366. this._switchToSector(this._sector());
  11367. },
  11368. /**
  11369. * This function returns the currently active sector Id
  11370. *
  11371. * @returns {String}
  11372. * @private
  11373. */
  11374. _sector : function() {
  11375. return this.activeSector[this.activeSector.length-1];
  11376. },
  11377. /**
  11378. * This function returns the previously active sector Id
  11379. *
  11380. * @returns {String}
  11381. * @private
  11382. */
  11383. _previousSector : function() {
  11384. if (this.activeSector.length > 1) {
  11385. return this.activeSector[this.activeSector.length-2];
  11386. }
  11387. else {
  11388. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11389. }
  11390. },
  11391. /**
  11392. * We add the active sector at the end of the this.activeSector array
  11393. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11394. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11395. *
  11396. * @param newId
  11397. * @private
  11398. */
  11399. _setActiveSector : function(newId) {
  11400. this.activeSector.push(newId);
  11401. },
  11402. /**
  11403. * We remove the currently active sector id from the active sector stack. This happens when
  11404. * we reactivate the previously active sector
  11405. *
  11406. * @private
  11407. */
  11408. _forgetLastSector : function() {
  11409. this.activeSector.pop();
  11410. },
  11411. /**
  11412. * This function creates a new active sector with the supplied newId. This newId
  11413. * is the expanding node id.
  11414. *
  11415. * @param {String} newId | Id of the new active sector
  11416. * @private
  11417. */
  11418. _createNewSector : function(newId) {
  11419. // create the new sector
  11420. this.sectors["active"][newId] = {"nodes":{},
  11421. "edges":{},
  11422. "nodeIndices":[],
  11423. "formationScale": this.scale,
  11424. "drawingNode": undefined};
  11425. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11426. this.sectors["active"][newId]['drawingNode'] = new Node(
  11427. {id:newId,
  11428. color: {
  11429. background: "#eaefef",
  11430. border: "495c5e"
  11431. }
  11432. },{},{},this.constants);
  11433. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11434. },
  11435. /**
  11436. * This function removes the currently active sector. This is called when we create a new
  11437. * active sector.
  11438. *
  11439. * @param {String} sectorId | Id of the active sector that will be removed
  11440. * @private
  11441. */
  11442. _deleteActiveSector : function(sectorId) {
  11443. delete this.sectors["active"][sectorId];
  11444. },
  11445. /**
  11446. * This function removes the currently active sector. This is called when we reactivate
  11447. * the previously active sector.
  11448. *
  11449. * @param {String} sectorId | Id of the active sector that will be removed
  11450. * @private
  11451. */
  11452. _deleteFrozenSector : function(sectorId) {
  11453. delete this.sectors["frozen"][sectorId];
  11454. },
  11455. /**
  11456. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11457. * We copy the references, then delete the active entree.
  11458. *
  11459. * @param sectorId
  11460. * @private
  11461. */
  11462. _freezeSector : function(sectorId) {
  11463. // we move the set references from the active to the frozen stack.
  11464. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  11465. // we have moved the sector data into the frozen set, we now remove it from the active set
  11466. this._deleteActiveSector(sectorId);
  11467. },
  11468. /**
  11469. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  11470. * object to the "active" object.
  11471. *
  11472. * @param sectorId
  11473. * @private
  11474. */
  11475. _activateSector : function(sectorId) {
  11476. // we move the set references from the frozen to the active stack.
  11477. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  11478. // we have moved the sector data into the active set, we now remove it from the frozen stack
  11479. this._deleteFrozenSector(sectorId);
  11480. },
  11481. /**
  11482. * This function merges the data from the currently active sector with a frozen sector. This is used
  11483. * in the process of reverting back to the previously active sector.
  11484. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  11485. * upon the creation of a new active sector.
  11486. *
  11487. * @param sectorId
  11488. * @private
  11489. */
  11490. _mergeThisWithFrozen : function(sectorId) {
  11491. // copy all nodes
  11492. for (var nodeId in this.nodes) {
  11493. if (this.nodes.hasOwnProperty(nodeId)) {
  11494. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  11495. }
  11496. }
  11497. // copy all edges (if not fully clustered, else there are no edges)
  11498. for (var edgeId in this.edges) {
  11499. if (this.edges.hasOwnProperty(edgeId)) {
  11500. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  11501. }
  11502. }
  11503. // merge the nodeIndices
  11504. for (var i = 0; i < this.nodeIndices.length; i++) {
  11505. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  11506. }
  11507. },
  11508. /**
  11509. * This clusters the sector to one cluster. It was a single cluster before this process started so
  11510. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  11511. *
  11512. * @private
  11513. */
  11514. _collapseThisToSingleCluster : function() {
  11515. this.clusterToFit(1,false);
  11516. },
  11517. /**
  11518. * We create a new active sector from the node that we want to open.
  11519. *
  11520. * @param node
  11521. * @private
  11522. */
  11523. _addSector : function(node) {
  11524. // this is the currently active sector
  11525. var sector = this._sector();
  11526. // // this should allow me to select nodes from a frozen set.
  11527. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  11528. // console.log("the node is part of the active sector");
  11529. // }
  11530. // else {
  11531. // console.log("I dont know what the fuck happened!!");
  11532. // }
  11533. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  11534. delete this.nodes[node.id];
  11535. var unqiueIdentifier = util.randomUUID();
  11536. // we fully freeze the currently active sector
  11537. this._freezeSector(sector);
  11538. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  11539. this._createNewSector(unqiueIdentifier);
  11540. // we add the active sector to the sectors array to be able to revert these steps later on
  11541. this._setActiveSector(unqiueIdentifier);
  11542. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  11543. this._switchToSector(this._sector());
  11544. // finally we add the node we removed from our previous active sector to the new active sector
  11545. this.nodes[node.id] = node;
  11546. },
  11547. /**
  11548. * We close the sector that is currently open and revert back to the one before.
  11549. * If the active sector is the "default" sector, nothing happens.
  11550. *
  11551. * @private
  11552. */
  11553. _collapseSector : function() {
  11554. // the currently active sector
  11555. var sector = this._sector();
  11556. // we cannot collapse the default sector
  11557. if (sector != "default") {
  11558. if ((this.nodeIndices.length == 1) ||
  11559. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11560. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11561. var previousSector = this._previousSector();
  11562. // we collapse the sector back to a single cluster
  11563. this._collapseThisToSingleCluster();
  11564. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  11565. // This previous sector is the one we will reactivate
  11566. this._mergeThisWithFrozen(previousSector);
  11567. // the previously active (frozen) sector now has all the data from the currently active sector.
  11568. // we can now delete the active sector.
  11569. this._deleteActiveSector(sector);
  11570. // we activate the previously active (and currently frozen) sector.
  11571. this._activateSector(previousSector);
  11572. // we load the references from the newly active sector into the global references
  11573. this._switchToSector(previousSector);
  11574. // we forget the previously active sector because we reverted to the one before
  11575. this._forgetLastSector();
  11576. // finally, we update the node index list.
  11577. this._updateNodeIndexList();
  11578. // we refresh the list with calulation nodes and calculation node indices.
  11579. this._updateCalculationNodes();
  11580. }
  11581. }
  11582. },
  11583. /**
  11584. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11585. *
  11586. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11587. * | we dont pass the function itself because then the "this" is the window object
  11588. * | instead of the Graph object
  11589. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11590. * @private
  11591. */
  11592. _doInAllActiveSectors : function(runFunction,argument) {
  11593. if (argument === undefined) {
  11594. for (var sector in this.sectors["active"]) {
  11595. if (this.sectors["active"].hasOwnProperty(sector)) {
  11596. // switch the global references to those of this sector
  11597. this._switchToActiveSector(sector);
  11598. this[runFunction]();
  11599. }
  11600. }
  11601. }
  11602. else {
  11603. for (var sector in this.sectors["active"]) {
  11604. if (this.sectors["active"].hasOwnProperty(sector)) {
  11605. // switch the global references to those of this sector
  11606. this._switchToActiveSector(sector);
  11607. var args = Array.prototype.splice.call(arguments, 1);
  11608. if (args.length > 1) {
  11609. this[runFunction](args[0],args[1]);
  11610. }
  11611. else {
  11612. this[runFunction](argument);
  11613. }
  11614. }
  11615. }
  11616. }
  11617. // we revert the global references back to our active sector
  11618. this._loadLatestSector();
  11619. },
  11620. /**
  11621. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11622. *
  11623. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11624. * | we dont pass the function itself because then the "this" is the window object
  11625. * | instead of the Graph object
  11626. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11627. * @private
  11628. */
  11629. _doInSupportSector : function(runFunction,argument) {
  11630. if (argument === undefined) {
  11631. this._switchToSupportSector();
  11632. this[runFunction]();
  11633. }
  11634. else {
  11635. this._switchToSupportSector();
  11636. var args = Array.prototype.splice.call(arguments, 1);
  11637. if (args.length > 1) {
  11638. this[runFunction](args[0],args[1]);
  11639. }
  11640. else {
  11641. this[runFunction](argument);
  11642. }
  11643. }
  11644. // we revert the global references back to our active sector
  11645. this._loadLatestSector();
  11646. },
  11647. /**
  11648. * This runs a function in all frozen sectors. This is used in the _redraw().
  11649. *
  11650. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11651. * | we don't pass the function itself because then the "this" is the window object
  11652. * | instead of the Graph object
  11653. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11654. * @private
  11655. */
  11656. _doInAllFrozenSectors : function(runFunction,argument) {
  11657. if (argument === undefined) {
  11658. for (var sector in this.sectors["frozen"]) {
  11659. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11660. // switch the global references to those of this sector
  11661. this._switchToFrozenSector(sector);
  11662. this[runFunction]();
  11663. }
  11664. }
  11665. }
  11666. else {
  11667. for (var sector in this.sectors["frozen"]) {
  11668. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11669. // switch the global references to those of this sector
  11670. this._switchToFrozenSector(sector);
  11671. var args = Array.prototype.splice.call(arguments, 1);
  11672. if (args.length > 1) {
  11673. this[runFunction](args[0],args[1]);
  11674. }
  11675. else {
  11676. this[runFunction](argument);
  11677. }
  11678. }
  11679. }
  11680. }
  11681. this._loadLatestSector();
  11682. },
  11683. /**
  11684. * This runs a function in all sectors. This is used in the _redraw().
  11685. *
  11686. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11687. * | we don't pass the function itself because then the "this" is the window object
  11688. * | instead of the Graph object
  11689. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11690. * @private
  11691. */
  11692. _doInAllSectors : function(runFunction,argument) {
  11693. var args = Array.prototype.splice.call(arguments, 1);
  11694. if (argument === undefined) {
  11695. this._doInAllActiveSectors(runFunction);
  11696. this._doInAllFrozenSectors(runFunction);
  11697. }
  11698. else {
  11699. if (args.length > 1) {
  11700. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  11701. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  11702. }
  11703. else {
  11704. this._doInAllActiveSectors(runFunction,argument);
  11705. this._doInAllFrozenSectors(runFunction,argument);
  11706. }
  11707. }
  11708. },
  11709. /**
  11710. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  11711. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  11712. *
  11713. * @private
  11714. */
  11715. _clearNodeIndexList : function() {
  11716. var sector = this._sector();
  11717. this.sectors["active"][sector]["nodeIndices"] = [];
  11718. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  11719. },
  11720. /**
  11721. * Draw the encompassing sector node
  11722. *
  11723. * @param ctx
  11724. * @param sectorType
  11725. * @private
  11726. */
  11727. _drawSectorNodes : function(ctx,sectorType) {
  11728. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  11729. for (var sector in this.sectors[sectorType]) {
  11730. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  11731. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  11732. this._switchToSector(sector,sectorType);
  11733. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  11734. for (var nodeId in this.nodes) {
  11735. if (this.nodes.hasOwnProperty(nodeId)) {
  11736. node = this.nodes[nodeId];
  11737. node.resize(ctx);
  11738. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  11739. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  11740. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  11741. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  11742. }
  11743. }
  11744. node = this.sectors[sectorType][sector]["drawingNode"];
  11745. node.x = 0.5 * (maxX + minX);
  11746. node.y = 0.5 * (maxY + minY);
  11747. node.width = 2 * (node.x - minX);
  11748. node.height = 2 * (node.y - minY);
  11749. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  11750. node.setScale(this.scale);
  11751. node._drawCircle(ctx);
  11752. }
  11753. }
  11754. }
  11755. },
  11756. _drawAllSectorNodes : function(ctx) {
  11757. this._drawSectorNodes(ctx,"frozen");
  11758. this._drawSectorNodes(ctx,"active");
  11759. this._loadLatestSector();
  11760. }
  11761. };
  11762. /**
  11763. * Creation of the ClusterMixin var.
  11764. *
  11765. * This contains all the functions the Graph object can use to employ clustering
  11766. *
  11767. * Alex de Mulder
  11768. * 21-01-2013
  11769. */
  11770. var ClusterMixin = {
  11771. /**
  11772. * This is only called in the constructor of the graph object
  11773. *
  11774. */
  11775. startWithClustering : function() {
  11776. // cluster if the data set is big
  11777. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  11778. // updates the lables after clustering
  11779. this.updateLabels();
  11780. // this is called here because if clusterin is disabled, the start and stabilize are called in
  11781. // the setData function.
  11782. if (this.stabilize) {
  11783. this._stabilize();
  11784. }
  11785. this.start();
  11786. },
  11787. /**
  11788. * This function clusters until the initialMaxNodes has been reached
  11789. *
  11790. * @param {Number} maxNumberOfNodes
  11791. * @param {Boolean} reposition
  11792. */
  11793. clusterToFit : function(maxNumberOfNodes, reposition) {
  11794. var numberOfNodes = this.nodeIndices.length;
  11795. var maxLevels = 50;
  11796. var level = 0;
  11797. // we first cluster the hubs, then we pull in the outliers, repeat
  11798. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  11799. if (level % 3 == 0) {
  11800. this.forceAggregateHubs(true);
  11801. this.normalizeClusterLevels();
  11802. }
  11803. else {
  11804. this.increaseClusterLevel(); // this also includes a cluster normalization
  11805. }
  11806. numberOfNodes = this.nodeIndices.length;
  11807. level += 1;
  11808. }
  11809. // after the clustering we reposition the nodes to reduce the initial chaos
  11810. if (level > 0 && reposition == true) {
  11811. this.repositionNodes();
  11812. }
  11813. this._updateCalculationNodes();
  11814. },
  11815. /**
  11816. * This function can be called to open up a specific cluster. It is only called by
  11817. * It will unpack the cluster back one level.
  11818. *
  11819. * @param node | Node object: cluster to open.
  11820. */
  11821. openCluster : function(node) {
  11822. var isMovingBeforeClustering = this.moving;
  11823. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  11824. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  11825. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  11826. this._addSector(node);
  11827. var level = 0;
  11828. // we decluster until we reach a decent number of nodes
  11829. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  11830. this.decreaseClusterLevel();
  11831. level += 1;
  11832. }
  11833. }
  11834. else {
  11835. this._expandClusterNode(node,false,true);
  11836. // update the index list, dynamic edges and labels
  11837. this._updateNodeIndexList();
  11838. this._updateDynamicEdges();
  11839. this._updateCalculationNodes();
  11840. this.updateLabels();
  11841. }
  11842. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11843. if (this.moving != isMovingBeforeClustering) {
  11844. this.start();
  11845. }
  11846. },
  11847. /**
  11848. * This calls the updateClustes with default arguments
  11849. */
  11850. updateClustersDefault : function() {
  11851. if (this.constants.clustering.enabled == true) {
  11852. this.updateClusters(0,false,false);
  11853. }
  11854. },
  11855. /**
  11856. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  11857. * be clustered with their connected node. This can be repeated as many times as needed.
  11858. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  11859. */
  11860. increaseClusterLevel : function() {
  11861. this.updateClusters(-1,false,true);
  11862. },
  11863. /**
  11864. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  11865. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  11866. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  11867. */
  11868. decreaseClusterLevel : function() {
  11869. this.updateClusters(1,false,true);
  11870. },
  11871. /**
  11872. * This is the main clustering function. It clusters and declusters on zoom or forced
  11873. * This function clusters on zoom, it can be called with a predefined zoom direction
  11874. * If out, check if we can form clusters, if in, check if we can open clusters.
  11875. * This function is only called from _zoom()
  11876. *
  11877. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  11878. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  11879. * @param {Boolean} force | enabled or disable forcing
  11880. * @param {Boolean} doNotStart | if true do not call start
  11881. *
  11882. */
  11883. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  11884. var isMovingBeforeClustering = this.moving;
  11885. var amountOfNodes = this.nodeIndices.length;
  11886. // on zoom out collapse the sector if the scale is at the level the sector was made
  11887. if (this.previousScale > this.scale && zoomDirection == 0) {
  11888. this._collapseSector();
  11889. }
  11890. // check if we zoom in or out
  11891. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11892. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  11893. // outer nodes determines if it is being clustered
  11894. this._formClusters(force);
  11895. }
  11896. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  11897. if (force == true) {
  11898. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  11899. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  11900. this._openClusters(recursive,force);
  11901. }
  11902. else {
  11903. // if a cluster takes up a set percentage of the active window
  11904. this._openClustersBySize();
  11905. }
  11906. }
  11907. this._updateNodeIndexList();
  11908. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  11909. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  11910. this._aggregateHubs(force);
  11911. this._updateNodeIndexList();
  11912. }
  11913. // we now reduce chains.
  11914. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11915. this.handleChains();
  11916. this._updateNodeIndexList();
  11917. }
  11918. this.previousScale = this.scale;
  11919. // rest of the update the index list, dynamic edges and labels
  11920. this._updateDynamicEdges();
  11921. this.updateLabels();
  11922. // if a cluster was formed, we increase the clusterSession
  11923. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  11924. this.clusterSession += 1;
  11925. // if clusters have been made, we normalize the cluster level
  11926. this.normalizeClusterLevels();
  11927. }
  11928. if (doNotStart == false || doNotStart === undefined) {
  11929. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11930. if (this.moving != isMovingBeforeClustering) {
  11931. this.start();
  11932. }
  11933. }
  11934. this._updateCalculationNodes();
  11935. },
  11936. /**
  11937. * This function handles the chains. It is called on every updateClusters().
  11938. */
  11939. handleChains : function() {
  11940. // after clustering we check how many chains there are
  11941. var chainPercentage = this._getChainFraction();
  11942. if (chainPercentage > this.constants.clustering.chainThreshold) {
  11943. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  11944. }
  11945. },
  11946. /**
  11947. * this functions starts clustering by hubs
  11948. * The minimum hub threshold is set globally
  11949. *
  11950. * @private
  11951. */
  11952. _aggregateHubs : function(force) {
  11953. this._getHubSize();
  11954. this._formClustersByHub(force,false);
  11955. },
  11956. /**
  11957. * This function is fired by keypress. It forces hubs to form.
  11958. *
  11959. */
  11960. forceAggregateHubs : function(doNotStart) {
  11961. var isMovingBeforeClustering = this.moving;
  11962. var amountOfNodes = this.nodeIndices.length;
  11963. this._aggregateHubs(true);
  11964. // update the index list, dynamic edges and labels
  11965. this._updateNodeIndexList();
  11966. this._updateDynamicEdges();
  11967. this.updateLabels();
  11968. // if a cluster was formed, we increase the clusterSession
  11969. if (this.nodeIndices.length != amountOfNodes) {
  11970. this.clusterSession += 1;
  11971. }
  11972. if (doNotStart == false || doNotStart === undefined) {
  11973. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11974. if (this.moving != isMovingBeforeClustering) {
  11975. this.start();
  11976. }
  11977. }
  11978. },
  11979. /**
  11980. * If a cluster takes up more than a set percentage of the screen, open the cluster
  11981. *
  11982. * @private
  11983. */
  11984. _openClustersBySize : function() {
  11985. for (var nodeId in this.nodes) {
  11986. if (this.nodes.hasOwnProperty(nodeId)) {
  11987. var node = this.nodes[nodeId];
  11988. if (node.inView() == true) {
  11989. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11990. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11991. this.openCluster(node);
  11992. }
  11993. }
  11994. }
  11995. }
  11996. },
  11997. /**
  11998. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  11999. * has to be opened based on the current zoom level.
  12000. *
  12001. * @private
  12002. */
  12003. _openClusters : function(recursive,force) {
  12004. for (var i = 0; i < this.nodeIndices.length; i++) {
  12005. var node = this.nodes[this.nodeIndices[i]];
  12006. this._expandClusterNode(node,recursive,force);
  12007. this._updateCalculationNodes();
  12008. }
  12009. },
  12010. /**
  12011. * This function checks if a node has to be opened. This is done by checking the zoom level.
  12012. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  12013. * This recursive behaviour is optional and can be set by the recursive argument.
  12014. *
  12015. * @param {Node} parentNode | to check for cluster and expand
  12016. * @param {Boolean} recursive | enabled or disable recursive calling
  12017. * @param {Boolean} force | enabled or disable forcing
  12018. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  12019. * @private
  12020. */
  12021. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  12022. // first check if node is a cluster
  12023. if (parentNode.clusterSize > 1) {
  12024. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  12025. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  12026. openAll = true;
  12027. }
  12028. recursive = openAll ? true : recursive;
  12029. // if the last child has been added on a smaller scale than current scale decluster
  12030. if (parentNode.formationScale < this.scale || force == true) {
  12031. // we will check if any of the contained child nodes should be removed from the cluster
  12032. for (var containedNodeId in parentNode.containedNodes) {
  12033. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  12034. var childNode = parentNode.containedNodes[containedNodeId];
  12035. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  12036. // the largest cluster is the one that comes from outside
  12037. if (force == true) {
  12038. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  12039. || openAll) {
  12040. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12041. }
  12042. }
  12043. else {
  12044. if (this._nodeInActiveArea(parentNode)) {
  12045. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12046. }
  12047. }
  12048. }
  12049. }
  12050. }
  12051. }
  12052. },
  12053. /**
  12054. * ONLY CALLED FROM _expandClusterNode
  12055. *
  12056. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  12057. * the child node from the parent contained_node object and put it back into the global nodes object.
  12058. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  12059. *
  12060. * @param {Node} parentNode | the parent node
  12061. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  12062. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  12063. * With force and recursive both true, the entire cluster is unpacked
  12064. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  12065. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  12066. * @private
  12067. */
  12068. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  12069. var childNode = parentNode.containedNodes[containedNodeId];
  12070. // if child node has been added on smaller scale than current, kick out
  12071. if (childNode.formationScale < this.scale || force == true) {
  12072. // unselect all selected items
  12073. this._unselectAll();
  12074. // put the child node back in the global nodes object
  12075. this.nodes[containedNodeId] = childNode;
  12076. // release the contained edges from this childNode back into the global edges
  12077. this._releaseContainedEdges(parentNode,childNode);
  12078. // reconnect rerouted edges to the childNode
  12079. this._connectEdgeBackToChild(parentNode,childNode);
  12080. // validate all edges in dynamicEdges
  12081. this._validateEdges(parentNode);
  12082. // undo the changes from the clustering operation on the parent node
  12083. parentNode.mass -= childNode.mass;
  12084. parentNode.clusterSize -= childNode.clusterSize;
  12085. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12086. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  12087. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  12088. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  12089. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  12090. // remove node from the list
  12091. delete parentNode.containedNodes[containedNodeId];
  12092. // check if there are other childs with this clusterSession in the parent.
  12093. var othersPresent = false;
  12094. for (var childNodeId in parentNode.containedNodes) {
  12095. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  12096. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  12097. othersPresent = true;
  12098. break;
  12099. }
  12100. }
  12101. }
  12102. // if there are no others, remove the cluster session from the list
  12103. if (othersPresent == false) {
  12104. parentNode.clusterSessions.pop();
  12105. }
  12106. this._repositionBezierNodes(childNode);
  12107. // this._repositionBezierNodes(parentNode);
  12108. // remove the clusterSession from the child node
  12109. childNode.clusterSession = 0;
  12110. // recalculate the size of the node on the next time the node is rendered
  12111. parentNode.clearSizeCache();
  12112. // restart the simulation to reorganise all nodes
  12113. this.moving = true;
  12114. }
  12115. // check if a further expansion step is possible if recursivity is enabled
  12116. if (recursive == true) {
  12117. this._expandClusterNode(childNode,recursive,force,openAll);
  12118. }
  12119. },
  12120. /**
  12121. * position the bezier nodes at the center of the edges
  12122. *
  12123. * @param node
  12124. * @private
  12125. */
  12126. _repositionBezierNodes : function(node) {
  12127. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12128. node.dynamicEdges[i].positionBezierNode();
  12129. }
  12130. },
  12131. /**
  12132. * This function checks if any nodes at the end of their trees have edges below a threshold length
  12133. * This function is called only from updateClusters()
  12134. * forceLevelCollapse ignores the length of the edge and collapses one level
  12135. * This means that a node with only one edge will be clustered with its connected node
  12136. *
  12137. * @private
  12138. * @param {Boolean} force
  12139. */
  12140. _formClusters : function(force) {
  12141. if (force == false) {
  12142. this._formClustersByZoom();
  12143. }
  12144. else {
  12145. this._forceClustersByZoom();
  12146. }
  12147. },
  12148. /**
  12149. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  12150. *
  12151. * @private
  12152. */
  12153. _formClustersByZoom : function() {
  12154. var dx,dy,length,
  12155. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12156. // check if any edges are shorter than minLength and start the clustering
  12157. // the clustering favours the node with the larger mass
  12158. for (var edgeId in this.edges) {
  12159. if (this.edges.hasOwnProperty(edgeId)) {
  12160. var edge = this.edges[edgeId];
  12161. if (edge.connected) {
  12162. if (edge.toId != edge.fromId) {
  12163. dx = (edge.to.x - edge.from.x);
  12164. dy = (edge.to.y - edge.from.y);
  12165. length = Math.sqrt(dx * dx + dy * dy);
  12166. if (length < minLength) {
  12167. // first check which node is larger
  12168. var parentNode = edge.from;
  12169. var childNode = edge.to;
  12170. if (edge.to.mass > edge.from.mass) {
  12171. parentNode = edge.to;
  12172. childNode = edge.from;
  12173. }
  12174. if (childNode.dynamicEdgesLength == 1) {
  12175. this._addToCluster(parentNode,childNode,false);
  12176. }
  12177. else if (parentNode.dynamicEdgesLength == 1) {
  12178. this._addToCluster(childNode,parentNode,false);
  12179. }
  12180. }
  12181. }
  12182. }
  12183. }
  12184. }
  12185. },
  12186. /**
  12187. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12188. * connected node.
  12189. *
  12190. * @private
  12191. */
  12192. _forceClustersByZoom : function() {
  12193. for (var nodeId in this.nodes) {
  12194. // another node could have absorbed this child.
  12195. if (this.nodes.hasOwnProperty(nodeId)) {
  12196. var childNode = this.nodes[nodeId];
  12197. // the edges can be swallowed by another decrease
  12198. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12199. var edge = childNode.dynamicEdges[0];
  12200. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12201. // group to the largest node
  12202. if (childNode.id != parentNode.id) {
  12203. if (parentNode.mass > childNode.mass) {
  12204. this._addToCluster(parentNode,childNode,true);
  12205. }
  12206. else {
  12207. this._addToCluster(childNode,parentNode,true);
  12208. }
  12209. }
  12210. }
  12211. }
  12212. }
  12213. },
  12214. /**
  12215. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12216. * This function clusters a node to its smallest connected neighbour.
  12217. *
  12218. * @param node
  12219. * @private
  12220. */
  12221. _clusterToSmallestNeighbour : function(node) {
  12222. var smallestNeighbour = -1;
  12223. var smallestNeighbourNode = null;
  12224. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12225. if (node.dynamicEdges[i] !== undefined) {
  12226. var neighbour = null;
  12227. if (node.dynamicEdges[i].fromId != node.id) {
  12228. neighbour = node.dynamicEdges[i].from;
  12229. }
  12230. else if (node.dynamicEdges[i].toId != node.id) {
  12231. neighbour = node.dynamicEdges[i].to;
  12232. }
  12233. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12234. smallestNeighbour = neighbour.clusterSessions.length;
  12235. smallestNeighbourNode = neighbour;
  12236. }
  12237. }
  12238. }
  12239. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12240. this._addToCluster(neighbour, node, true);
  12241. }
  12242. },
  12243. /**
  12244. * This function forms clusters from hubs, it loops over all nodes
  12245. *
  12246. * @param {Boolean} force | Disregard zoom level
  12247. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12248. * @private
  12249. */
  12250. _formClustersByHub : function(force, onlyEqual) {
  12251. // we loop over all nodes in the list
  12252. for (var nodeId in this.nodes) {
  12253. // we check if it is still available since it can be used by the clustering in this loop
  12254. if (this.nodes.hasOwnProperty(nodeId)) {
  12255. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12256. }
  12257. }
  12258. },
  12259. /**
  12260. * This function forms a cluster from a specific preselected hub node
  12261. *
  12262. * @param {Node} hubNode | the node we will cluster as a hub
  12263. * @param {Boolean} force | Disregard zoom level
  12264. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12265. * @param {Number} [absorptionSizeOffset] |
  12266. * @private
  12267. */
  12268. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12269. if (absorptionSizeOffset === undefined) {
  12270. absorptionSizeOffset = 0;
  12271. }
  12272. // we decide if the node is a hub
  12273. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12274. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12275. // initialize variables
  12276. var dx,dy,length;
  12277. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12278. var allowCluster = false;
  12279. // we create a list of edges because the dynamicEdges change over the course of this loop
  12280. var edgesIdarray = [];
  12281. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12282. for (var j = 0; j < amountOfInitialEdges; j++) {
  12283. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12284. }
  12285. // if the hub clustering is not forces, we check if one of the edges connected
  12286. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12287. if (force == false) {
  12288. allowCluster = false;
  12289. for (j = 0; j < amountOfInitialEdges; j++) {
  12290. var edge = this.edges[edgesIdarray[j]];
  12291. if (edge !== undefined) {
  12292. if (edge.connected) {
  12293. if (edge.toId != edge.fromId) {
  12294. dx = (edge.to.x - edge.from.x);
  12295. dy = (edge.to.y - edge.from.y);
  12296. length = Math.sqrt(dx * dx + dy * dy);
  12297. if (length < minLength) {
  12298. allowCluster = true;
  12299. break;
  12300. }
  12301. }
  12302. }
  12303. }
  12304. }
  12305. }
  12306. // start the clustering if allowed
  12307. if ((!force && allowCluster) || force) {
  12308. // we loop over all edges INITIALLY connected to this hub
  12309. for (j = 0; j < amountOfInitialEdges; j++) {
  12310. edge = this.edges[edgesIdarray[j]];
  12311. // the edge can be clustered by this function in a previous loop
  12312. if (edge !== undefined) {
  12313. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12314. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12315. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12316. (childNode.id != hubNode.id)) {
  12317. this._addToCluster(hubNode,childNode,force);
  12318. }
  12319. }
  12320. }
  12321. }
  12322. }
  12323. },
  12324. /**
  12325. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12326. *
  12327. * @param {Node} parentNode | this is the node that will house the child node
  12328. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12329. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12330. * @private
  12331. */
  12332. _addToCluster : function(parentNode, childNode, force) {
  12333. // join child node in the parent node
  12334. parentNode.containedNodes[childNode.id] = childNode;
  12335. // manage all the edges connected to the child and parent nodes
  12336. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12337. var edge = childNode.dynamicEdges[i];
  12338. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12339. this._addToContainedEdges(parentNode,childNode,edge);
  12340. }
  12341. else {
  12342. this._connectEdgeToCluster(parentNode,childNode,edge);
  12343. }
  12344. }
  12345. // a contained node has no dynamic edges.
  12346. childNode.dynamicEdges = [];
  12347. // remove circular edges from clusters
  12348. this._containCircularEdgesFromNode(parentNode,childNode);
  12349. // remove the childNode from the global nodes object
  12350. delete this.nodes[childNode.id];
  12351. // update the properties of the child and parent
  12352. var massBefore = parentNode.mass;
  12353. childNode.clusterSession = this.clusterSession;
  12354. parentNode.mass += childNode.mass;
  12355. parentNode.clusterSize += childNode.clusterSize;
  12356. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12357. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12358. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12359. parentNode.clusterSessions.push(this.clusterSession);
  12360. }
  12361. // forced clusters only open from screen size and double tap
  12362. if (force == true) {
  12363. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12364. parentNode.formationScale = 0;
  12365. }
  12366. else {
  12367. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12368. }
  12369. // recalculate the size of the node on the next time the node is rendered
  12370. parentNode.clearSizeCache();
  12371. // set the pop-out scale for the childnode
  12372. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12373. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12374. childNode.clearVelocity();
  12375. // the mass has altered, preservation of energy dictates the velocity to be updated
  12376. parentNode.updateVelocity(massBefore);
  12377. // restart the simulation to reorganise all nodes
  12378. this.moving = true;
  12379. },
  12380. /**
  12381. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12382. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12383. * It has to be called if a level is collapsed. It is called by _formClusters().
  12384. * @private
  12385. */
  12386. _updateDynamicEdges : function() {
  12387. for (var i = 0; i < this.nodeIndices.length; i++) {
  12388. var node = this.nodes[this.nodeIndices[i]];
  12389. node.dynamicEdgesLength = node.dynamicEdges.length;
  12390. // this corrects for multiple edges pointing at the same other node
  12391. var correction = 0;
  12392. if (node.dynamicEdgesLength > 1) {
  12393. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12394. var edgeToId = node.dynamicEdges[j].toId;
  12395. var edgeFromId = node.dynamicEdges[j].fromId;
  12396. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12397. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12398. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12399. correction += 1;
  12400. }
  12401. }
  12402. }
  12403. }
  12404. node.dynamicEdgesLength -= correction;
  12405. }
  12406. },
  12407. /**
  12408. * This adds an edge from the childNode to the contained edges of the parent node
  12409. *
  12410. * @param parentNode | Node object
  12411. * @param childNode | Node object
  12412. * @param edge | Edge object
  12413. * @private
  12414. */
  12415. _addToContainedEdges : function(parentNode, childNode, edge) {
  12416. // create an array object if it does not yet exist for this childNode
  12417. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12418. parentNode.containedEdges[childNode.id] = []
  12419. }
  12420. // add this edge to the list
  12421. parentNode.containedEdges[childNode.id].push(edge);
  12422. // remove the edge from the global edges object
  12423. delete this.edges[edge.id];
  12424. // remove the edge from the parent object
  12425. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12426. if (parentNode.dynamicEdges[i].id == edge.id) {
  12427. parentNode.dynamicEdges.splice(i,1);
  12428. break;
  12429. }
  12430. }
  12431. },
  12432. /**
  12433. * This function connects an edge that was connected to a child node to the parent node.
  12434. * It keeps track of which nodes it has been connected to with the originalId array.
  12435. *
  12436. * @param {Node} parentNode | Node object
  12437. * @param {Node} childNode | Node object
  12438. * @param {Edge} edge | Edge object
  12439. * @private
  12440. */
  12441. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12442. // handle circular edges
  12443. if (edge.toId == edge.fromId) {
  12444. this._addToContainedEdges(parentNode, childNode, edge);
  12445. }
  12446. else {
  12447. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12448. edge.originalToId.push(childNode.id);
  12449. edge.to = parentNode;
  12450. edge.toId = parentNode.id;
  12451. }
  12452. else { // edge connected to other node with the "from" side
  12453. edge.originalFromId.push(childNode.id);
  12454. edge.from = parentNode;
  12455. edge.fromId = parentNode.id;
  12456. }
  12457. this._addToReroutedEdges(parentNode,childNode,edge);
  12458. }
  12459. },
  12460. /**
  12461. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12462. * these edges inside of the cluster.
  12463. *
  12464. * @param parentNode
  12465. * @param childNode
  12466. * @private
  12467. */
  12468. _containCircularEdgesFromNode : function(parentNode, childNode) {
  12469. // manage all the edges connected to the child and parent nodes
  12470. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12471. var edge = parentNode.dynamicEdges[i];
  12472. // handle circular edges
  12473. if (edge.toId == edge.fromId) {
  12474. this._addToContainedEdges(parentNode, childNode, edge);
  12475. }
  12476. }
  12477. },
  12478. /**
  12479. * This adds an edge from the childNode to the rerouted edges of the parent node
  12480. *
  12481. * @param parentNode | Node object
  12482. * @param childNode | Node object
  12483. * @param edge | Edge object
  12484. * @private
  12485. */
  12486. _addToReroutedEdges : function(parentNode, childNode, edge) {
  12487. // create an array object if it does not yet exist for this childNode
  12488. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  12489. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  12490. parentNode.reroutedEdges[childNode.id] = [];
  12491. }
  12492. parentNode.reroutedEdges[childNode.id].push(edge);
  12493. // this edge becomes part of the dynamicEdges of the cluster node
  12494. parentNode.dynamicEdges.push(edge);
  12495. },
  12496. /**
  12497. * This function connects an edge that was connected to a cluster node back to the child node.
  12498. *
  12499. * @param parentNode | Node object
  12500. * @param childNode | Node object
  12501. * @private
  12502. */
  12503. _connectEdgeBackToChild : function(parentNode, childNode) {
  12504. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  12505. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  12506. var edge = parentNode.reroutedEdges[childNode.id][i];
  12507. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  12508. edge.originalFromId.pop();
  12509. edge.fromId = childNode.id;
  12510. edge.from = childNode;
  12511. }
  12512. else {
  12513. edge.originalToId.pop();
  12514. edge.toId = childNode.id;
  12515. edge.to = childNode;
  12516. }
  12517. // append this edge to the list of edges connecting to the childnode
  12518. childNode.dynamicEdges.push(edge);
  12519. // remove the edge from the parent object
  12520. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  12521. if (parentNode.dynamicEdges[j].id == edge.id) {
  12522. parentNode.dynamicEdges.splice(j,1);
  12523. break;
  12524. }
  12525. }
  12526. }
  12527. // remove the entry from the rerouted edges
  12528. delete parentNode.reroutedEdges[childNode.id];
  12529. }
  12530. },
  12531. /**
  12532. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  12533. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  12534. * parentNode
  12535. *
  12536. * @param parentNode | Node object
  12537. * @private
  12538. */
  12539. _validateEdges : function(parentNode) {
  12540. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12541. var edge = parentNode.dynamicEdges[i];
  12542. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  12543. parentNode.dynamicEdges.splice(i,1);
  12544. }
  12545. }
  12546. },
  12547. /**
  12548. * This function released the contained edges back into the global domain and puts them back into the
  12549. * dynamic edges of both parent and child.
  12550. *
  12551. * @param {Node} parentNode |
  12552. * @param {Node} childNode |
  12553. * @private
  12554. */
  12555. _releaseContainedEdges : function(parentNode, childNode) {
  12556. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  12557. var edge = parentNode.containedEdges[childNode.id][i];
  12558. // put the edge back in the global edges object
  12559. this.edges[edge.id] = edge;
  12560. // put the edge back in the dynamic edges of the child and parent
  12561. childNode.dynamicEdges.push(edge);
  12562. parentNode.dynamicEdges.push(edge);
  12563. }
  12564. // remove the entry from the contained edges
  12565. delete parentNode.containedEdges[childNode.id];
  12566. },
  12567. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  12568. /**
  12569. * This updates the node labels for all nodes (for debugging purposes)
  12570. */
  12571. updateLabels : function() {
  12572. var nodeId;
  12573. // update node labels
  12574. for (nodeId in this.nodes) {
  12575. if (this.nodes.hasOwnProperty(nodeId)) {
  12576. var node = this.nodes[nodeId];
  12577. if (node.clusterSize > 1) {
  12578. node.label = "[".concat(String(node.clusterSize),"]");
  12579. }
  12580. }
  12581. }
  12582. // update node labels
  12583. for (nodeId in this.nodes) {
  12584. if (this.nodes.hasOwnProperty(nodeId)) {
  12585. node = this.nodes[nodeId];
  12586. if (node.clusterSize == 1) {
  12587. if (node.originalLabel !== undefined) {
  12588. node.label = node.originalLabel;
  12589. }
  12590. else {
  12591. node.label = String(node.id);
  12592. }
  12593. }
  12594. }
  12595. }
  12596. // /* Debug Override */
  12597. // for (nodeId in this.nodes) {
  12598. // if (this.nodes.hasOwnProperty(nodeId)) {
  12599. // node = this.nodes[nodeId];
  12600. // node.label = String(node.level);
  12601. // }
  12602. // }
  12603. },
  12604. /**
  12605. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  12606. * if the rest of the nodes are already a few cluster levels in.
  12607. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  12608. * clustered enough to the clusterToSmallestNeighbours function.
  12609. */
  12610. normalizeClusterLevels : function() {
  12611. var maxLevel = 0;
  12612. var minLevel = 1e9;
  12613. var clusterLevel = 0;
  12614. var nodeId;
  12615. // we loop over all nodes in the list
  12616. for (nodeId in this.nodes) {
  12617. if (this.nodes.hasOwnProperty(nodeId)) {
  12618. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  12619. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  12620. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  12621. }
  12622. }
  12623. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  12624. var amountOfNodes = this.nodeIndices.length;
  12625. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  12626. // we loop over all nodes in the list
  12627. for (nodeId in this.nodes) {
  12628. if (this.nodes.hasOwnProperty(nodeId)) {
  12629. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  12630. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  12631. }
  12632. }
  12633. }
  12634. this._updateNodeIndexList();
  12635. this._updateDynamicEdges();
  12636. // if a cluster was formed, we increase the clusterSession
  12637. if (this.nodeIndices.length != amountOfNodes) {
  12638. this.clusterSession += 1;
  12639. }
  12640. }
  12641. },
  12642. /**
  12643. * This function determines if the cluster we want to decluster is in the active area
  12644. * this means around the zoom center
  12645. *
  12646. * @param {Node} node
  12647. * @returns {boolean}
  12648. * @private
  12649. */
  12650. _nodeInActiveArea : function(node) {
  12651. return (
  12652. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12653. &&
  12654. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12655. )
  12656. },
  12657. /**
  12658. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  12659. * It puts large clusters away from the center and randomizes the order.
  12660. *
  12661. */
  12662. repositionNodes : function() {
  12663. for (var i = 0; i < this.nodeIndices.length; i++) {
  12664. var node = this.nodes[this.nodeIndices[i]];
  12665. if ((node.xFixed == false || node.yFixed == false)) {
  12666. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  12667. var angle = 2 * Math.PI * Math.random();
  12668. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  12669. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  12670. this._repositionBezierNodes(node);
  12671. }
  12672. }
  12673. },
  12674. /**
  12675. * We determine how many connections denote an important hub.
  12676. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  12677. *
  12678. * @private
  12679. */
  12680. _getHubSize : function() {
  12681. var average = 0;
  12682. var averageSquared = 0;
  12683. var hubCounter = 0;
  12684. var largestHub = 0;
  12685. for (var i = 0; i < this.nodeIndices.length; i++) {
  12686. var node = this.nodes[this.nodeIndices[i]];
  12687. if (node.dynamicEdgesLength > largestHub) {
  12688. largestHub = node.dynamicEdgesLength;
  12689. }
  12690. average += node.dynamicEdgesLength;
  12691. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  12692. hubCounter += 1;
  12693. }
  12694. average = average / hubCounter;
  12695. averageSquared = averageSquared / hubCounter;
  12696. var variance = averageSquared - Math.pow(average,2);
  12697. var standardDeviation = Math.sqrt(variance);
  12698. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  12699. // always have at least one to cluster
  12700. if (this.hubThreshold > largestHub) {
  12701. this.hubThreshold = largestHub;
  12702. }
  12703. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  12704. // console.log("hubThreshold:",this.hubThreshold);
  12705. },
  12706. /**
  12707. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12708. * with this amount we can cluster specifically on these chains.
  12709. *
  12710. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  12711. * @private
  12712. */
  12713. _reduceAmountOfChains : function(fraction) {
  12714. this.hubThreshold = 2;
  12715. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  12716. for (var nodeId in this.nodes) {
  12717. if (this.nodes.hasOwnProperty(nodeId)) {
  12718. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12719. if (reduceAmount > 0) {
  12720. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  12721. reduceAmount -= 1;
  12722. }
  12723. }
  12724. }
  12725. }
  12726. },
  12727. /**
  12728. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12729. * with this amount we can cluster specifically on these chains.
  12730. *
  12731. * @private
  12732. */
  12733. _getChainFraction : function() {
  12734. var chains = 0;
  12735. var total = 0;
  12736. for (var nodeId in this.nodes) {
  12737. if (this.nodes.hasOwnProperty(nodeId)) {
  12738. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12739. chains += 1;
  12740. }
  12741. total += 1;
  12742. }
  12743. }
  12744. return chains/total;
  12745. }
  12746. };
  12747. var SelectionMixin = {
  12748. /**
  12749. * This function can be called from the _doInAllSectors function
  12750. *
  12751. * @param object
  12752. * @param overlappingNodes
  12753. * @private
  12754. */
  12755. _getNodesOverlappingWith : function(object, overlappingNodes) {
  12756. var nodes = this.nodes;
  12757. for (var nodeId in nodes) {
  12758. if (nodes.hasOwnProperty(nodeId)) {
  12759. if (nodes[nodeId].isOverlappingWith(object)) {
  12760. overlappingNodes.push(nodeId);
  12761. }
  12762. }
  12763. }
  12764. },
  12765. /**
  12766. * retrieve all nodes overlapping with given object
  12767. * @param {Object} object An object with parameters left, top, right, bottom
  12768. * @return {Number[]} An array with id's of the overlapping nodes
  12769. * @private
  12770. */
  12771. _getAllNodesOverlappingWith : function (object) {
  12772. var overlappingNodes = [];
  12773. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  12774. return overlappingNodes;
  12775. },
  12776. /**
  12777. * Return a position object in canvasspace from a single point in screenspace
  12778. *
  12779. * @param pointer
  12780. * @returns {{left: number, top: number, right: number, bottom: number}}
  12781. * @private
  12782. */
  12783. _pointerToPositionObject : function(pointer) {
  12784. var x = this._canvasToX(pointer.x);
  12785. var y = this._canvasToY(pointer.y);
  12786. return {left: x,
  12787. top: y,
  12788. right: x,
  12789. bottom: y};
  12790. },
  12791. /**
  12792. * Get the top node at the a specific point (like a click)
  12793. *
  12794. * @param {{x: Number, y: Number}} pointer
  12795. * @return {Node | null} node
  12796. * @private
  12797. */
  12798. _getNodeAt : function (pointer) {
  12799. // we first check if this is an navigation controls element
  12800. var positionObject = this._pointerToPositionObject(pointer);
  12801. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  12802. // if there are overlapping nodes, select the last one, this is the
  12803. // one which is drawn on top of the others
  12804. if (overlappingNodes.length > 0) {
  12805. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  12806. }
  12807. else {
  12808. return null;
  12809. }
  12810. },
  12811. /**
  12812. * retrieve all edges overlapping with given object, selector is around center
  12813. * @param {Object} object An object with parameters left, top, right, bottom
  12814. * @return {Number[]} An array with id's of the overlapping nodes
  12815. * @private
  12816. */
  12817. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  12818. var edges = this.edges;
  12819. for (var edgeId in edges) {
  12820. if (edges.hasOwnProperty(edgeId)) {
  12821. if (edges[edgeId].isOverlappingWith(object)) {
  12822. overlappingEdges.push(edgeId);
  12823. }
  12824. }
  12825. }
  12826. },
  12827. /**
  12828. * retrieve all nodes overlapping with given object
  12829. * @param {Object} object An object with parameters left, top, right, bottom
  12830. * @return {Number[]} An array with id's of the overlapping nodes
  12831. * @private
  12832. */
  12833. _getAllEdgesOverlappingWith : function (object) {
  12834. var overlappingEdges = [];
  12835. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  12836. return overlappingEdges;
  12837. },
  12838. /**
  12839. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  12840. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  12841. *
  12842. * @param pointer
  12843. * @returns {null}
  12844. * @private
  12845. */
  12846. _getEdgeAt : function(pointer) {
  12847. var positionObject = this._pointerToPositionObject(pointer);
  12848. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  12849. if (overlappingEdges.length > 0) {
  12850. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  12851. }
  12852. else {
  12853. return null;
  12854. }
  12855. },
  12856. /**
  12857. * Add object to the selection array.
  12858. *
  12859. * @param obj
  12860. * @private
  12861. */
  12862. _addToSelection : function(obj) {
  12863. if (obj instanceof Node) {
  12864. this.selectionObj.nodes[obj.id] = obj;
  12865. }
  12866. else {
  12867. this.selectionObj.edges[obj.id] = obj;
  12868. }
  12869. },
  12870. /**
  12871. * Remove a single option from selection.
  12872. *
  12873. * @param {Object} obj
  12874. * @private
  12875. */
  12876. _removeFromSelection : function(obj) {
  12877. if (obj instanceof Node) {
  12878. delete this.selectionObj.nodes[obj.id];
  12879. }
  12880. else {
  12881. delete this.selectionObj.edges[obj.id];
  12882. }
  12883. },
  12884. /**
  12885. * Unselect all. The selectionObj is useful for this.
  12886. *
  12887. * @param {Boolean} [doNotTrigger] | ignore trigger
  12888. * @private
  12889. */
  12890. _unselectAll : function(doNotTrigger) {
  12891. if (doNotTrigger === undefined) {
  12892. doNotTrigger = false;
  12893. }
  12894. for(var nodeId in this.selectionObj.nodes) {
  12895. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12896. this.selectionObj.nodes[nodeId].unselect();
  12897. }
  12898. }
  12899. for(var edgeId in this.selectionObj.edges) {
  12900. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12901. this.selectionObj.edges[edgeId].unselect();
  12902. }
  12903. }
  12904. this.selectionObj = {nodes:{},edges:{}};
  12905. if (doNotTrigger == false) {
  12906. this.emit('select', this.getSelection());
  12907. }
  12908. },
  12909. /**
  12910. * Unselect all clusters. The selectionObj is useful for this.
  12911. *
  12912. * @param {Boolean} [doNotTrigger] | ignore trigger
  12913. * @private
  12914. */
  12915. _unselectClusters : function(doNotTrigger) {
  12916. if (doNotTrigger === undefined) {
  12917. doNotTrigger = false;
  12918. }
  12919. for (var nodeId in this.selectionObj.nodes) {
  12920. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12921. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  12922. this.selectionObj.nodes[nodeId].unselect();
  12923. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  12924. }
  12925. }
  12926. }
  12927. if (doNotTrigger == false) {
  12928. this.emit('select', this.getSelection());
  12929. }
  12930. },
  12931. /**
  12932. * return the number of selected nodes
  12933. *
  12934. * @returns {number}
  12935. * @private
  12936. */
  12937. _getSelectedNodeCount : function() {
  12938. var count = 0;
  12939. for (var nodeId in this.selectionObj.nodes) {
  12940. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12941. count += 1;
  12942. }
  12943. }
  12944. return count;
  12945. },
  12946. /**
  12947. * return the number of selected nodes
  12948. *
  12949. * @returns {number}
  12950. * @private
  12951. */
  12952. _getSelectedNode : function() {
  12953. for (var nodeId in this.selectionObj.nodes) {
  12954. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12955. return this.selectionObj.nodes[nodeId];
  12956. }
  12957. }
  12958. return null;
  12959. },
  12960. /**
  12961. * return the number of selected edges
  12962. *
  12963. * @returns {number}
  12964. * @private
  12965. */
  12966. _getSelectedEdgeCount : function() {
  12967. var count = 0;
  12968. for (var edgeId in this.selectionObj.edges) {
  12969. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12970. count += 1;
  12971. }
  12972. }
  12973. return count;
  12974. },
  12975. /**
  12976. * return the number of selected objects.
  12977. *
  12978. * @returns {number}
  12979. * @private
  12980. */
  12981. _getSelectedObjectCount : function() {
  12982. var count = 0;
  12983. for(var nodeId in this.selectionObj.nodes) {
  12984. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12985. count += 1;
  12986. }
  12987. }
  12988. for(var edgeId in this.selectionObj.edges) {
  12989. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12990. count += 1;
  12991. }
  12992. }
  12993. return count;
  12994. },
  12995. /**
  12996. * Check if anything is selected
  12997. *
  12998. * @returns {boolean}
  12999. * @private
  13000. */
  13001. _selectionIsEmpty : function() {
  13002. for(var nodeId in this.selectionObj.nodes) {
  13003. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13004. return false;
  13005. }
  13006. }
  13007. for(var edgeId in this.selectionObj.edges) {
  13008. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13009. return false;
  13010. }
  13011. }
  13012. return true;
  13013. },
  13014. /**
  13015. * check if one of the selected nodes is a cluster.
  13016. *
  13017. * @returns {boolean}
  13018. * @private
  13019. */
  13020. _clusterInSelection : function() {
  13021. for(var nodeId in this.selectionObj.nodes) {
  13022. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13023. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13024. return true;
  13025. }
  13026. }
  13027. }
  13028. return false;
  13029. },
  13030. /**
  13031. * select the edges connected to the node that is being selected
  13032. *
  13033. * @param {Node} node
  13034. * @private
  13035. */
  13036. _selectConnectedEdges : function(node) {
  13037. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13038. var edge = node.dynamicEdges[i];
  13039. edge.select();
  13040. this._addToSelection(edge);
  13041. }
  13042. },
  13043. /**
  13044. * unselect the edges connected to the node that is being selected
  13045. *
  13046. * @param {Node} node
  13047. * @private
  13048. */
  13049. _unselectConnectedEdges : function(node) {
  13050. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13051. var edge = node.dynamicEdges[i];
  13052. edge.unselect();
  13053. this._removeFromSelection(edge);
  13054. }
  13055. },
  13056. /**
  13057. * This is called when someone clicks on a node. either select or deselect it.
  13058. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13059. *
  13060. * @param {Node || Edge} object
  13061. * @param {Boolean} append
  13062. * @param {Boolean} [doNotTrigger] | ignore trigger
  13063. * @private
  13064. */
  13065. _selectObject : function(object, append, doNotTrigger) {
  13066. if (doNotTrigger === undefined) {
  13067. doNotTrigger = false;
  13068. }
  13069. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  13070. this._unselectAll(true);
  13071. }
  13072. if (object.selected == false) {
  13073. object.select();
  13074. this._addToSelection(object);
  13075. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  13076. this._selectConnectedEdges(object);
  13077. }
  13078. }
  13079. else {
  13080. object.unselect();
  13081. this._removeFromSelection(object);
  13082. }
  13083. if (doNotTrigger == false) {
  13084. this.emit('select', this.getSelection());
  13085. }
  13086. },
  13087. /**
  13088. * handles the selection part of the touch, only for navigation controls elements;
  13089. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  13090. * This is the most responsive solution
  13091. *
  13092. * @param {Object} pointer
  13093. * @private
  13094. */
  13095. _handleTouch : function(pointer) {
  13096. },
  13097. /**
  13098. * handles the selection part of the tap;
  13099. *
  13100. * @param {Object} pointer
  13101. * @private
  13102. */
  13103. _handleTap : function(pointer) {
  13104. var node = this._getNodeAt(pointer);
  13105. if (node != null) {
  13106. this._selectObject(node,false);
  13107. }
  13108. else {
  13109. var edge = this._getEdgeAt(pointer);
  13110. if (edge != null) {
  13111. this._selectObject(edge,false);
  13112. }
  13113. else {
  13114. this._unselectAll();
  13115. }
  13116. }
  13117. this.emit("click", this.getSelection());
  13118. this._redraw();
  13119. },
  13120. /**
  13121. * handles the selection part of the double tap and opens a cluster if needed
  13122. *
  13123. * @param {Object} pointer
  13124. * @private
  13125. */
  13126. _handleDoubleTap : function(pointer) {
  13127. var node = this._getNodeAt(pointer);
  13128. if (node != null && node !== undefined) {
  13129. // we reset the areaCenter here so the opening of the node will occur
  13130. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  13131. "y" : this._canvasToY(pointer.y)};
  13132. this.openCluster(node);
  13133. }
  13134. this.emit("doubleClick", this.getSelection());
  13135. },
  13136. /**
  13137. * Handle the onHold selection part
  13138. *
  13139. * @param pointer
  13140. * @private
  13141. */
  13142. _handleOnHold : function(pointer) {
  13143. var node = this._getNodeAt(pointer);
  13144. if (node != null) {
  13145. this._selectObject(node,true);
  13146. }
  13147. else {
  13148. var edge = this._getEdgeAt(pointer);
  13149. if (edge != null) {
  13150. this._selectObject(edge,true);
  13151. }
  13152. }
  13153. this._redraw();
  13154. },
  13155. /**
  13156. * handle the onRelease event. These functions are here for the navigation controls module.
  13157. *
  13158. * @private
  13159. */
  13160. _handleOnRelease : function(pointer) {
  13161. },
  13162. /**
  13163. *
  13164. * retrieve the currently selected objects
  13165. * @return {Number[] | String[]} selection An array with the ids of the
  13166. * selected nodes.
  13167. */
  13168. getSelection : function() {
  13169. var nodeIds = this.getSelectedNodes();
  13170. var edgeIds = this.getSelectedEdges();
  13171. return {nodes:nodeIds, edges:edgeIds};
  13172. },
  13173. /**
  13174. *
  13175. * retrieve the currently selected nodes
  13176. * @return {String} selection An array with the ids of the
  13177. * selected nodes.
  13178. */
  13179. getSelectedNodes : function() {
  13180. var idArray = [];
  13181. for(var nodeId in this.selectionObj.nodes) {
  13182. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13183. idArray.push(nodeId);
  13184. }
  13185. }
  13186. return idArray
  13187. },
  13188. /**
  13189. *
  13190. * retrieve the currently selected edges
  13191. * @return {Array} selection An array with the ids of the
  13192. * selected nodes.
  13193. */
  13194. getSelectedEdges : function() {
  13195. var idArray = [];
  13196. for(var edgeId in this.selectionObj.edges) {
  13197. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13198. idArray.push(edgeId);
  13199. }
  13200. }
  13201. return idArray;
  13202. },
  13203. /**
  13204. * select zero or more nodes
  13205. * @param {Number[] | String[]} selection An array with the ids of the
  13206. * selected nodes.
  13207. */
  13208. setSelection : function(selection) {
  13209. var i, iMax, id;
  13210. if (!selection || (selection.length == undefined))
  13211. throw 'Selection must be an array with ids';
  13212. // first unselect any selected node
  13213. this._unselectAll(true);
  13214. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13215. id = selection[i];
  13216. var node = this.nodes[id];
  13217. if (!node) {
  13218. throw new RangeError('Node with id "' + id + '" not found');
  13219. }
  13220. this._selectObject(node,true,true);
  13221. }
  13222. this.redraw();
  13223. },
  13224. /**
  13225. * Validate the selection: remove ids of nodes which no longer exist
  13226. * @private
  13227. */
  13228. _updateSelection : function () {
  13229. for(var nodeId in this.selectionObj.nodes) {
  13230. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13231. if (!this.nodes.hasOwnProperty(nodeId)) {
  13232. delete this.selectionObj.nodes[nodeId];
  13233. }
  13234. }
  13235. }
  13236. for(var edgeId in this.selectionObj.edges) {
  13237. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13238. if (!this.edges.hasOwnProperty(edgeId)) {
  13239. delete this.selectionObj.edges[edgeId];
  13240. }
  13241. }
  13242. }
  13243. }
  13244. };
  13245. /**
  13246. * Created by Alex on 1/22/14.
  13247. */
  13248. var NavigationMixin = {
  13249. _cleanNavigation : function() {
  13250. // clean up previosu navigation items
  13251. var wrapper = document.getElementById('graph-navigation_wrapper');
  13252. if (wrapper != null) {
  13253. this.containerElement.removeChild(wrapper);
  13254. }
  13255. document.onmouseup = null;
  13256. },
  13257. /**
  13258. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13259. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13260. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13261. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13262. *
  13263. * @private
  13264. */
  13265. _loadNavigationElements : function() {
  13266. this._cleanNavigation();
  13267. this.navigationDivs = {};
  13268. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13269. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13270. this.navigationDivs['wrapper'] = document.createElement('div');
  13271. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13272. this.navigationDivs['wrapper'].style.position = "absolute";
  13273. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  13274. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  13275. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13276. for (var i = 0; i < navigationDivs.length; i++) {
  13277. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13278. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13279. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13280. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13281. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13282. }
  13283. document.onmouseup = this._stopMovement.bind(this);
  13284. },
  13285. /**
  13286. * this stops all movement induced by the navigation buttons
  13287. *
  13288. * @private
  13289. */
  13290. _stopMovement : function() {
  13291. this._xStopMoving();
  13292. this._yStopMoving();
  13293. this._stopZoom();
  13294. },
  13295. /**
  13296. * stops the actions performed by page up and down etc.
  13297. *
  13298. * @param event
  13299. * @private
  13300. */
  13301. _preventDefault : function(event) {
  13302. if (event !== undefined) {
  13303. if (event.preventDefault) {
  13304. event.preventDefault();
  13305. } else {
  13306. event.returnValue = false;
  13307. }
  13308. }
  13309. },
  13310. /**
  13311. * move the screen up
  13312. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13313. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13314. * To avoid this behaviour, we do the translation in the start loop.
  13315. *
  13316. * @private
  13317. */
  13318. _moveUp : function(event) {
  13319. this.yIncrement = this.constants.keyboard.speed.y;
  13320. this.start(); // if there is no node movement, the calculation wont be done
  13321. this._preventDefault(event);
  13322. if (this.navigationDivs) {
  13323. this.navigationDivs['up'].className += " active";
  13324. }
  13325. },
  13326. /**
  13327. * move the screen down
  13328. * @private
  13329. */
  13330. _moveDown : function(event) {
  13331. this.yIncrement = -this.constants.keyboard.speed.y;
  13332. this.start(); // if there is no node movement, the calculation wont be done
  13333. this._preventDefault(event);
  13334. if (this.navigationDivs) {
  13335. this.navigationDivs['down'].className += " active";
  13336. }
  13337. },
  13338. /**
  13339. * move the screen left
  13340. * @private
  13341. */
  13342. _moveLeft : function(event) {
  13343. this.xIncrement = this.constants.keyboard.speed.x;
  13344. this.start(); // if there is no node movement, the calculation wont be done
  13345. this._preventDefault(event);
  13346. if (this.navigationDivs) {
  13347. this.navigationDivs['left'].className += " active";
  13348. }
  13349. },
  13350. /**
  13351. * move the screen right
  13352. * @private
  13353. */
  13354. _moveRight : function(event) {
  13355. this.xIncrement = -this.constants.keyboard.speed.y;
  13356. this.start(); // if there is no node movement, the calculation wont be done
  13357. this._preventDefault(event);
  13358. if (this.navigationDivs) {
  13359. this.navigationDivs['right'].className += " active";
  13360. }
  13361. },
  13362. /**
  13363. * Zoom in, using the same method as the movement.
  13364. * @private
  13365. */
  13366. _zoomIn : function(event) {
  13367. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13368. this.start(); // if there is no node movement, the calculation wont be done
  13369. this._preventDefault(event);
  13370. if (this.navigationDivs) {
  13371. this.navigationDivs['zoomIn'].className += " active";
  13372. }
  13373. },
  13374. /**
  13375. * Zoom out
  13376. * @private
  13377. */
  13378. _zoomOut : function() {
  13379. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13380. this.start(); // if there is no node movement, the calculation wont be done
  13381. this._preventDefault(event);
  13382. if (this.navigationDivs) {
  13383. this.navigationDivs['zoomOut'].className += " active";
  13384. }
  13385. },
  13386. /**
  13387. * Stop zooming and unhighlight the zoom controls
  13388. * @private
  13389. */
  13390. _stopZoom : function() {
  13391. this.zoomIncrement = 0;
  13392. if (this.navigationDivs) {
  13393. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  13394. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  13395. }
  13396. },
  13397. /**
  13398. * Stop moving in the Y direction and unHighlight the up and down
  13399. * @private
  13400. */
  13401. _yStopMoving : function() {
  13402. this.yIncrement = 0;
  13403. if (this.navigationDivs) {
  13404. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  13405. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  13406. }
  13407. },
  13408. /**
  13409. * Stop moving in the X direction and unHighlight left and right.
  13410. * @private
  13411. */
  13412. _xStopMoving : function() {
  13413. this.xIncrement = 0;
  13414. if (this.navigationDivs) {
  13415. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  13416. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  13417. }
  13418. }
  13419. };
  13420. /**
  13421. * Created by Alex on 2/10/14.
  13422. */
  13423. var graphMixinLoaders = {
  13424. /**
  13425. * Load a mixin into the graph object
  13426. *
  13427. * @param {Object} sourceVariable | this object has to contain functions.
  13428. * @private
  13429. */
  13430. _loadMixin: function (sourceVariable) {
  13431. for (var mixinFunction in sourceVariable) {
  13432. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13433. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13434. }
  13435. }
  13436. },
  13437. /**
  13438. * removes a mixin from the graph object.
  13439. *
  13440. * @param {Object} sourceVariable | this object has to contain functions.
  13441. * @private
  13442. */
  13443. _clearMixin: function (sourceVariable) {
  13444. for (var mixinFunction in sourceVariable) {
  13445. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13446. Graph.prototype[mixinFunction] = undefined;
  13447. }
  13448. }
  13449. },
  13450. /**
  13451. * Mixin the physics system and initialize the parameters required.
  13452. *
  13453. * @private
  13454. */
  13455. _loadPhysicsSystem: function () {
  13456. this._loadMixin(physicsMixin);
  13457. this._loadSelectedForceSolver();
  13458. if (this.constants.configurePhysics == true) {
  13459. this._loadPhysicsConfiguration();
  13460. }
  13461. },
  13462. /**
  13463. * Mixin the cluster system and initialize the parameters required.
  13464. *
  13465. * @private
  13466. */
  13467. _loadClusterSystem: function () {
  13468. this.clusterSession = 0;
  13469. this.hubThreshold = 5;
  13470. this._loadMixin(ClusterMixin);
  13471. },
  13472. /**
  13473. * Mixin the sector system and initialize the parameters required
  13474. *
  13475. * @private
  13476. */
  13477. _loadSectorSystem: function () {
  13478. this.sectors = {};
  13479. this.activeSector = ["default"];
  13480. this.sectors["active"] = {};
  13481. this.sectors["active"]["default"] = {"nodes": {},
  13482. "edges": {},
  13483. "nodeIndices": [],
  13484. "formationScale": 1.0,
  13485. "drawingNode": undefined };
  13486. this.sectors["frozen"] = {};
  13487. this.sectors["support"] = {"nodes": {},
  13488. "edges": {},
  13489. "nodeIndices": [],
  13490. "formationScale": 1.0,
  13491. "drawingNode": undefined };
  13492. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13493. this._loadMixin(SectorMixin);
  13494. },
  13495. /**
  13496. * Mixin the selection system and initialize the parameters required
  13497. *
  13498. * @private
  13499. */
  13500. _loadSelectionSystem: function () {
  13501. this.selectionObj = {nodes: {}, edges: {}};
  13502. this._loadMixin(SelectionMixin);
  13503. },
  13504. /**
  13505. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13506. *
  13507. * @private
  13508. */
  13509. _loadManipulationSystem: function () {
  13510. // reset global variables -- these are used by the selection of nodes and edges.
  13511. this.blockConnectingEdgeSelection = false;
  13512. this.forceAppendSelection = false;
  13513. if (this.constants.dataManipulation.enabled == true) {
  13514. // load the manipulator HTML elements. All styling done in css.
  13515. if (this.manipulationDiv === undefined) {
  13516. this.manipulationDiv = document.createElement('div');
  13517. this.manipulationDiv.className = 'graph-manipulationDiv';
  13518. this.manipulationDiv.id = 'graph-manipulationDiv';
  13519. if (this.editMode == true) {
  13520. this.manipulationDiv.style.display = "block";
  13521. }
  13522. else {
  13523. this.manipulationDiv.style.display = "none";
  13524. }
  13525. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  13526. }
  13527. if (this.editModeDiv === undefined) {
  13528. this.editModeDiv = document.createElement('div');
  13529. this.editModeDiv.className = 'graph-manipulation-editMode';
  13530. this.editModeDiv.id = 'graph-manipulation-editMode';
  13531. if (this.editMode == true) {
  13532. this.editModeDiv.style.display = "none";
  13533. }
  13534. else {
  13535. this.editModeDiv.style.display = "block";
  13536. }
  13537. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  13538. }
  13539. if (this.closeDiv === undefined) {
  13540. this.closeDiv = document.createElement('div');
  13541. this.closeDiv.className = 'graph-manipulation-closeDiv';
  13542. this.closeDiv.id = 'graph-manipulation-closeDiv';
  13543. this.closeDiv.style.display = this.manipulationDiv.style.display;
  13544. this.containerElement.insertBefore(this.closeDiv, this.frame);
  13545. }
  13546. // load the manipulation functions
  13547. this._loadMixin(manipulationMixin);
  13548. // create the manipulator toolbar
  13549. this._createManipulatorBar();
  13550. }
  13551. else {
  13552. if (this.manipulationDiv !== undefined) {
  13553. // removes all the bindings and overloads
  13554. this._createManipulatorBar();
  13555. // remove the manipulation divs
  13556. this.containerElement.removeChild(this.manipulationDiv);
  13557. this.containerElement.removeChild(this.editModeDiv);
  13558. this.containerElement.removeChild(this.closeDiv);
  13559. this.manipulationDiv = undefined;
  13560. this.editModeDiv = undefined;
  13561. this.closeDiv = undefined;
  13562. // remove the mixin functions
  13563. this._clearMixin(manipulationMixin);
  13564. }
  13565. }
  13566. },
  13567. /**
  13568. * Mixin the navigation (User Interface) system and initialize the parameters required
  13569. *
  13570. * @private
  13571. */
  13572. _loadNavigationControls: function () {
  13573. this._loadMixin(NavigationMixin);
  13574. // the clean function removes the button divs, this is done to remove the bindings.
  13575. this._cleanNavigation();
  13576. if (this.constants.navigation.enabled == true) {
  13577. this._loadNavigationElements();
  13578. }
  13579. },
  13580. /**
  13581. * Mixin the hierarchical layout system.
  13582. *
  13583. * @private
  13584. */
  13585. _loadHierarchySystem: function () {
  13586. this._loadMixin(HierarchicalLayoutMixin);
  13587. }
  13588. };
  13589. /**
  13590. * @constructor Graph
  13591. * Create a graph visualization, displaying nodes and edges.
  13592. *
  13593. * @param {Element} container The DOM element in which the Graph will
  13594. * be created. Normally a div element.
  13595. * @param {Object} data An object containing parameters
  13596. * {Array} nodes
  13597. * {Array} edges
  13598. * @param {Object} options Options
  13599. */
  13600. function Graph (container, data, options) {
  13601. this._initializeMixinLoaders();
  13602. // create variables and set default values
  13603. this.containerElement = container;
  13604. this.width = '100%';
  13605. this.height = '100%';
  13606. // render and calculation settings
  13607. this.renderRefreshRate = 60; // hz (fps)
  13608. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  13609. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  13610. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  13611. this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
  13612. this.stabilize = true; // stabilize before displaying the graph
  13613. this.selectable = true;
  13614. this.initializing = true;
  13615. // these functions are triggered when the dataset is edited
  13616. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  13617. // set constant values
  13618. this.constants = {
  13619. nodes: {
  13620. radiusMin: 5,
  13621. radiusMax: 20,
  13622. radius: 5,
  13623. shape: 'ellipse',
  13624. image: undefined,
  13625. widthMin: 16, // px
  13626. widthMax: 64, // px
  13627. fixed: false,
  13628. fontColor: 'black',
  13629. fontSize: 14, // px
  13630. fontFace: 'verdana',
  13631. level: -1,
  13632. color: {
  13633. border: '#2B7CE9',
  13634. background: '#97C2FC',
  13635. highlight: {
  13636. border: '#2B7CE9',
  13637. background: '#D2E5FF'
  13638. }
  13639. },
  13640. borderColor: '#2B7CE9',
  13641. backgroundColor: '#97C2FC',
  13642. highlightColor: '#D2E5FF',
  13643. group: undefined
  13644. },
  13645. edges: {
  13646. widthMin: 1,
  13647. widthMax: 15,
  13648. width: 1,
  13649. style: 'line',
  13650. color: {
  13651. color:'#848484',
  13652. highlight:'#848484'
  13653. },
  13654. fontColor: '#343434',
  13655. fontSize: 14, // px
  13656. fontFace: 'arial',
  13657. fontFill: 'white',
  13658. arrowScaleFactor: 1,
  13659. dash: {
  13660. length: 10,
  13661. gap: 5,
  13662. altLength: undefined
  13663. }
  13664. },
  13665. configurePhysics:false,
  13666. physics: {
  13667. barnesHut: {
  13668. enabled: true,
  13669. theta: 1 / 0.6, // inverted to save time during calculation
  13670. gravitationalConstant: -2000,
  13671. centralGravity: 0.3,
  13672. springLength: 95,
  13673. springConstant: 0.04,
  13674. damping: 0.09
  13675. },
  13676. repulsion: {
  13677. centralGravity: 0.1,
  13678. springLength: 200,
  13679. springConstant: 0.05,
  13680. nodeDistance: 100,
  13681. damping: 0.09
  13682. },
  13683. hierarchicalRepulsion: {
  13684. enabled: false,
  13685. centralGravity: 0.0,
  13686. springLength: 100,
  13687. springConstant: 0.01,
  13688. nodeDistance: 60,
  13689. damping: 0.09
  13690. },
  13691. damping: null,
  13692. centralGravity: null,
  13693. springLength: null,
  13694. springConstant: null
  13695. },
  13696. clustering: { // Per Node in Cluster = PNiC
  13697. enabled: false, // (Boolean) | global on/off switch for clustering.
  13698. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  13699. 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
  13700. 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
  13701. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  13702. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  13703. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  13704. 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.
  13705. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  13706. maxFontSize: 1000,
  13707. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  13708. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  13709. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  13710. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  13711. height: 1, // (px PNiC) | growth of the height per node in cluster.
  13712. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  13713. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  13714. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  13715. clusterLevelDifference: 2
  13716. },
  13717. navigation: {
  13718. enabled: false
  13719. },
  13720. keyboard: {
  13721. enabled: false,
  13722. speed: {x: 10, y: 10, zoom: 0.02}
  13723. },
  13724. dataManipulation: {
  13725. enabled: false,
  13726. initiallyVisible: false
  13727. },
  13728. hierarchicalLayout: {
  13729. enabled:false,
  13730. levelSeparation: 150,
  13731. nodeSpacing: 100,
  13732. direction: "UD" // UD, DU, LR, RL
  13733. },
  13734. freezeForStabilization: false,
  13735. smoothCurves: true,
  13736. maxVelocity: 10,
  13737. minVelocity: 0.1, // px/s
  13738. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  13739. labels:{
  13740. add:"Add Node",
  13741. edit:"Edit",
  13742. link:"Add Link",
  13743. del:"Delete selected",
  13744. editNode:"Edit Node",
  13745. back:"Back",
  13746. addDescription:"Click in an empty space to place a new node.",
  13747. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  13748. addError:"The function for add does not support two arguments (data,callback).",
  13749. linkError:"The function for connect does not support two arguments (data,callback).",
  13750. editError:"The function for edit does not support two arguments (data, callback).",
  13751. editBoundError:"No edit function has been bound to this button.",
  13752. deleteError:"The function for delete does not support two arguments (data, callback).",
  13753. deleteClusterError:"Clusters cannot be deleted."
  13754. },
  13755. tooltip: {
  13756. delay: 300,
  13757. fontColor: 'black',
  13758. fontSize: 14, // px
  13759. fontFace: 'verdana',
  13760. color: {
  13761. border: '#666',
  13762. background: '#FFFFC6'
  13763. }
  13764. }
  13765. };
  13766. this.editMode = this.constants.dataManipulation.initiallyVisible;
  13767. // Node variables
  13768. var graph = this;
  13769. this.groups = new Groups(); // object with groups
  13770. this.images = new Images(); // object with images
  13771. this.images.setOnloadCallback(function () {
  13772. graph._redraw();
  13773. });
  13774. // keyboard navigation variables
  13775. this.xIncrement = 0;
  13776. this.yIncrement = 0;
  13777. this.zoomIncrement = 0;
  13778. // loading all the mixins:
  13779. // load the force calculation functions, grouped under the physics system.
  13780. this._loadPhysicsSystem();
  13781. // create a frame and canvas
  13782. this._create();
  13783. // load the sector system. (mandatory, fully integrated with Graph)
  13784. this._loadSectorSystem();
  13785. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  13786. this._loadClusterSystem();
  13787. // load the selection system. (mandatory, required by Graph)
  13788. this._loadSelectionSystem();
  13789. // load the selection system. (mandatory, required by Graph)
  13790. this._loadHierarchySystem();
  13791. // apply options
  13792. this.setOptions(options);
  13793. // other vars
  13794. this.freezeSimulation = false;// freeze the simulation
  13795. this.cachedFunctions = {};
  13796. // containers for nodes and edges
  13797. this.calculationNodes = {};
  13798. this.calculationNodeIndices = [];
  13799. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  13800. this.nodes = {}; // object with Node objects
  13801. this.edges = {}; // object with Edge objects
  13802. // position and scale variables and objects
  13803. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  13804. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13805. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13806. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  13807. this.scale = 1; // defining the global scale variable in the constructor
  13808. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  13809. // datasets or dataviews
  13810. this.nodesData = null; // A DataSet or DataView
  13811. this.edgesData = null; // A DataSet or DataView
  13812. // create event listeners used to subscribe on the DataSets of the nodes and edges
  13813. this.nodesListeners = {
  13814. 'add': function (event, params) {
  13815. graph._addNodes(params.items);
  13816. graph.start();
  13817. },
  13818. 'update': function (event, params) {
  13819. graph._updateNodes(params.items);
  13820. graph.start();
  13821. },
  13822. 'remove': function (event, params) {
  13823. graph._removeNodes(params.items);
  13824. graph.start();
  13825. }
  13826. };
  13827. this.edgesListeners = {
  13828. 'add': function (event, params) {
  13829. graph._addEdges(params.items);
  13830. graph.start();
  13831. },
  13832. 'update': function (event, params) {
  13833. graph._updateEdges(params.items);
  13834. graph.start();
  13835. },
  13836. 'remove': function (event, params) {
  13837. graph._removeEdges(params.items);
  13838. graph.start();
  13839. }
  13840. };
  13841. // properties for the animation
  13842. this.moving = true;
  13843. this.timer = undefined; // Scheduling function. Is definded in this.start();
  13844. // load data (the disable start variable will be the same as the enabled clustering)
  13845. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  13846. // hierarchical layout
  13847. this.initializing = false;
  13848. if (this.constants.hierarchicalLayout.enabled == true) {
  13849. this._setupHierarchicalLayout();
  13850. }
  13851. else {
  13852. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  13853. if (this.stabilize == false) {
  13854. this.zoomExtent(true,this.constants.clustering.enabled);
  13855. }
  13856. }
  13857. // if clustering is disabled, the simulation will have started in the setData function
  13858. if (this.constants.clustering.enabled) {
  13859. this.startWithClustering();
  13860. }
  13861. }
  13862. // Extend Graph with an Emitter mixin
  13863. Emitter(Graph.prototype);
  13864. /**
  13865. * Get the script path where the vis.js library is located
  13866. *
  13867. * @returns {string | null} path Path or null when not found. Path does not
  13868. * end with a slash.
  13869. * @private
  13870. */
  13871. Graph.prototype._getScriptPath = function() {
  13872. var scripts = document.getElementsByTagName( 'script' );
  13873. // find script named vis.js or vis.min.js
  13874. for (var i = 0; i < scripts.length; i++) {
  13875. var src = scripts[i].src;
  13876. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  13877. if (match) {
  13878. // return path without the script name
  13879. return src.substring(0, src.length - match[0].length);
  13880. }
  13881. }
  13882. return null;
  13883. };
  13884. /**
  13885. * Find the center position of the graph
  13886. * @private
  13887. */
  13888. Graph.prototype._getRange = function() {
  13889. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  13890. for (var nodeId in this.nodes) {
  13891. if (this.nodes.hasOwnProperty(nodeId)) {
  13892. node = this.nodes[nodeId];
  13893. if (minX > (node.x)) {minX = node.x;}
  13894. if (maxX < (node.x)) {maxX = node.x;}
  13895. if (minY > (node.y)) {minY = node.y;}
  13896. if (maxY < (node.y)) {maxY = node.y;}
  13897. }
  13898. }
  13899. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  13900. minY = 0, maxY = 0, minX = 0, maxX = 0;
  13901. }
  13902. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13903. };
  13904. /**
  13905. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13906. * @returns {{x: number, y: number}}
  13907. * @private
  13908. */
  13909. Graph.prototype._findCenter = function(range) {
  13910. return {x: (0.5 * (range.maxX + range.minX)),
  13911. y: (0.5 * (range.maxY + range.minY))};
  13912. };
  13913. /**
  13914. * center the graph
  13915. *
  13916. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13917. */
  13918. Graph.prototype._centerGraph = function(range) {
  13919. var center = this._findCenter(range);
  13920. center.x *= this.scale;
  13921. center.y *= this.scale;
  13922. center.x -= 0.5 * this.frame.canvas.clientWidth;
  13923. center.y -= 0.5 * this.frame.canvas.clientHeight;
  13924. this._setTranslation(-center.x,-center.y); // set at 0,0
  13925. };
  13926. /**
  13927. * This function zooms out to fit all data on screen based on amount of nodes
  13928. *
  13929. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  13930. * @param {Boolean} [disableStart] | If true, start is not called.
  13931. */
  13932. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  13933. if (initialZoom === undefined) {
  13934. initialZoom = false;
  13935. }
  13936. if (disableStart === undefined) {
  13937. disableStart = false;
  13938. }
  13939. var range = this._getRange();
  13940. var zoomLevel;
  13941. if (initialZoom == true) {
  13942. var numberOfNodes = this.nodeIndices.length;
  13943. if (this.constants.smoothCurves == true) {
  13944. if (this.constants.clustering.enabled == true &&
  13945. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13946. 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.
  13947. }
  13948. else {
  13949. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13950. }
  13951. }
  13952. else {
  13953. if (this.constants.clustering.enabled == true &&
  13954. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13955. 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.
  13956. }
  13957. else {
  13958. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13959. }
  13960. }
  13961. // correct for larger canvasses.
  13962. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  13963. zoomLevel *= factor;
  13964. }
  13965. else {
  13966. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  13967. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  13968. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  13969. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  13970. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  13971. }
  13972. if (zoomLevel > 1.0) {
  13973. zoomLevel = 1.0;
  13974. }
  13975. this._setScale(zoomLevel);
  13976. this._centerGraph(range);
  13977. if (disableStart == false) {
  13978. this.moving = true;
  13979. this.start();
  13980. }
  13981. };
  13982. /**
  13983. * Update the this.nodeIndices with the most recent node index list
  13984. * @private
  13985. */
  13986. Graph.prototype._updateNodeIndexList = function() {
  13987. this._clearNodeIndexList();
  13988. for (var idx in this.nodes) {
  13989. if (this.nodes.hasOwnProperty(idx)) {
  13990. this.nodeIndices.push(idx);
  13991. }
  13992. }
  13993. };
  13994. /**
  13995. * Set nodes and edges, and optionally options as well.
  13996. *
  13997. * @param {Object} data Object containing parameters:
  13998. * {Array | DataSet | DataView} [nodes] Array with nodes
  13999. * {Array | DataSet | DataView} [edges] Array with edges
  14000. * {String} [dot] String containing data in DOT format
  14001. * {Options} [options] Object with options
  14002. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  14003. */
  14004. Graph.prototype.setData = function(data, disableStart) {
  14005. if (disableStart === undefined) {
  14006. disableStart = false;
  14007. }
  14008. if (data && data.dot && (data.nodes || data.edges)) {
  14009. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14010. ' parameter pair "nodes" and "edges", but not both.');
  14011. }
  14012. // set options
  14013. this.setOptions(data && data.options);
  14014. // set all data
  14015. if (data && data.dot) {
  14016. // parse DOT file
  14017. if(data && data.dot) {
  14018. var dotData = vis.util.DOTToGraph(data.dot);
  14019. this.setData(dotData);
  14020. return;
  14021. }
  14022. }
  14023. else {
  14024. this._setNodes(data && data.nodes);
  14025. this._setEdges(data && data.edges);
  14026. }
  14027. this._putDataInSector();
  14028. if (!disableStart) {
  14029. // find a stable position or start animating to a stable position
  14030. if (this.stabilize) {
  14031. this._stabilize();
  14032. }
  14033. this.start();
  14034. }
  14035. };
  14036. /**
  14037. * Set options
  14038. * @param {Object} options
  14039. */
  14040. Graph.prototype.setOptions = function (options) {
  14041. if (options) {
  14042. var prop;
  14043. // retrieve parameter values
  14044. if (options.width !== undefined) {this.width = options.width;}
  14045. if (options.height !== undefined) {this.height = options.height;}
  14046. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14047. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14048. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  14049. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  14050. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  14051. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  14052. if (options.labels !== undefined) {
  14053. for (prop in options.labels) {
  14054. if (options.labels.hasOwnProperty(prop)) {
  14055. this.constants.labels[prop] = options.labels[prop];
  14056. }
  14057. }
  14058. }
  14059. if (options.onAdd) {
  14060. this.triggerFunctions.add = options.onAdd;
  14061. }
  14062. if (options.onEdit) {
  14063. this.triggerFunctions.edit = options.onEdit;
  14064. }
  14065. if (options.onConnect) {
  14066. this.triggerFunctions.connect = options.onConnect;
  14067. }
  14068. if (options.onDelete) {
  14069. this.triggerFunctions.del = options.onDelete;
  14070. }
  14071. if (options.physics) {
  14072. if (options.physics.barnesHut) {
  14073. this.constants.physics.barnesHut.enabled = true;
  14074. for (prop in options.physics.barnesHut) {
  14075. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14076. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14077. }
  14078. }
  14079. }
  14080. if (options.physics.repulsion) {
  14081. this.constants.physics.barnesHut.enabled = false;
  14082. for (prop in options.physics.repulsion) {
  14083. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14084. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14085. }
  14086. }
  14087. }
  14088. }
  14089. if (options.hierarchicalLayout) {
  14090. this.constants.hierarchicalLayout.enabled = true;
  14091. for (prop in options.hierarchicalLayout) {
  14092. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14093. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14094. }
  14095. }
  14096. }
  14097. else if (options.hierarchicalLayout !== undefined) {
  14098. this.constants.hierarchicalLayout.enabled = false;
  14099. }
  14100. if (options.clustering) {
  14101. this.constants.clustering.enabled = true;
  14102. for (prop in options.clustering) {
  14103. if (options.clustering.hasOwnProperty(prop)) {
  14104. this.constants.clustering[prop] = options.clustering[prop];
  14105. }
  14106. }
  14107. }
  14108. else if (options.clustering !== undefined) {
  14109. this.constants.clustering.enabled = false;
  14110. }
  14111. if (options.navigation) {
  14112. this.constants.navigation.enabled = true;
  14113. for (prop in options.navigation) {
  14114. if (options.navigation.hasOwnProperty(prop)) {
  14115. this.constants.navigation[prop] = options.navigation[prop];
  14116. }
  14117. }
  14118. }
  14119. else if (options.navigation !== undefined) {
  14120. this.constants.navigation.enabled = false;
  14121. }
  14122. if (options.keyboard) {
  14123. this.constants.keyboard.enabled = true;
  14124. for (prop in options.keyboard) {
  14125. if (options.keyboard.hasOwnProperty(prop)) {
  14126. this.constants.keyboard[prop] = options.keyboard[prop];
  14127. }
  14128. }
  14129. }
  14130. else if (options.keyboard !== undefined) {
  14131. this.constants.keyboard.enabled = false;
  14132. }
  14133. if (options.dataManipulation) {
  14134. this.constants.dataManipulation.enabled = true;
  14135. for (prop in options.dataManipulation) {
  14136. if (options.dataManipulation.hasOwnProperty(prop)) {
  14137. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14138. }
  14139. }
  14140. }
  14141. else if (options.dataManipulation !== undefined) {
  14142. this.constants.dataManipulation.enabled = false;
  14143. }
  14144. // TODO: work out these options and document them
  14145. if (options.edges) {
  14146. for (prop in options.edges) {
  14147. if (options.edges.hasOwnProperty(prop)) {
  14148. if (typeof options.edges[prop] != "object") {
  14149. this.constants.edges[prop] = options.edges[prop];
  14150. }
  14151. }
  14152. }
  14153. if (options.edges.color !== undefined) {
  14154. if (util.isString(options.edges.color)) {
  14155. this.constants.edges.color = {};
  14156. this.constants.edges.color.color = options.edges.color;
  14157. this.constants.edges.color.highlight = options.edges.color;
  14158. }
  14159. else {
  14160. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  14161. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  14162. }
  14163. }
  14164. if (!options.edges.fontColor) {
  14165. if (options.edges.color !== undefined) {
  14166. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  14167. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  14168. }
  14169. }
  14170. // Added to support dashed lines
  14171. // David Jordan
  14172. // 2012-08-08
  14173. if (options.edges.dash) {
  14174. if (options.edges.dash.length !== undefined) {
  14175. this.constants.edges.dash.length = options.edges.dash.length;
  14176. }
  14177. if (options.edges.dash.gap !== undefined) {
  14178. this.constants.edges.dash.gap = options.edges.dash.gap;
  14179. }
  14180. if (options.edges.dash.altLength !== undefined) {
  14181. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14182. }
  14183. }
  14184. }
  14185. if (options.nodes) {
  14186. for (prop in options.nodes) {
  14187. if (options.nodes.hasOwnProperty(prop)) {
  14188. this.constants.nodes[prop] = options.nodes[prop];
  14189. }
  14190. }
  14191. if (options.nodes.color) {
  14192. this.constants.nodes.color = util.parseColor(options.nodes.color);
  14193. }
  14194. /*
  14195. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14196. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14197. */
  14198. }
  14199. if (options.groups) {
  14200. for (var groupname in options.groups) {
  14201. if (options.groups.hasOwnProperty(groupname)) {
  14202. var group = options.groups[groupname];
  14203. this.groups.add(groupname, group);
  14204. }
  14205. }
  14206. }
  14207. if (options.tooltip) {
  14208. for (prop in options.tooltip) {
  14209. if (options.tooltip.hasOwnProperty(prop)) {
  14210. this.constants.tooltip[prop] = options.tooltip[prop];
  14211. }
  14212. }
  14213. if (options.tooltip.color) {
  14214. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14215. }
  14216. }
  14217. }
  14218. // (Re)loading the mixins that can be enabled or disabled in the options.
  14219. // load the force calculation functions, grouped under the physics system.
  14220. this._loadPhysicsSystem();
  14221. // load the navigation system.
  14222. this._loadNavigationControls();
  14223. // load the data manipulation system
  14224. this._loadManipulationSystem();
  14225. // configure the smooth curves
  14226. this._configureSmoothCurves();
  14227. // bind keys. If disabled, this will not do anything;
  14228. this._createKeyBinds();
  14229. this.setSize(this.width, this.height);
  14230. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14231. this._setScale(1);
  14232. this._redraw();
  14233. };
  14234. /**
  14235. * Create the main frame for the Graph.
  14236. * This function is executed once when a Graph object is created. The frame
  14237. * contains a canvas, and this canvas contains all objects like the axis and
  14238. * nodes.
  14239. * @private
  14240. */
  14241. Graph.prototype._create = function () {
  14242. // remove all elements from the container element.
  14243. while (this.containerElement.hasChildNodes()) {
  14244. this.containerElement.removeChild(this.containerElement.firstChild);
  14245. }
  14246. this.frame = document.createElement('div');
  14247. this.frame.className = 'graph-frame';
  14248. this.frame.style.position = 'relative';
  14249. this.frame.style.overflow = 'hidden';
  14250. // create the graph canvas (HTML canvas element)
  14251. this.frame.canvas = document.createElement( 'canvas' );
  14252. this.frame.canvas.style.position = 'relative';
  14253. this.frame.appendChild(this.frame.canvas);
  14254. if (!this.frame.canvas.getContext) {
  14255. var noCanvas = document.createElement( 'DIV' );
  14256. noCanvas.style.color = 'red';
  14257. noCanvas.style.fontWeight = 'bold' ;
  14258. noCanvas.style.padding = '10px';
  14259. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14260. this.frame.canvas.appendChild(noCanvas);
  14261. }
  14262. var me = this;
  14263. this.drag = {};
  14264. this.pinch = {};
  14265. this.hammer = Hammer(this.frame.canvas, {
  14266. prevent_default: true
  14267. });
  14268. this.hammer.on('tap', me._onTap.bind(me) );
  14269. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14270. this.hammer.on('hold', me._onHold.bind(me) );
  14271. this.hammer.on('pinch', me._onPinch.bind(me) );
  14272. this.hammer.on('touch', me._onTouch.bind(me) );
  14273. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14274. this.hammer.on('drag', me._onDrag.bind(me) );
  14275. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14276. this.hammer.on('release', me._onRelease.bind(me) );
  14277. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14278. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14279. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14280. // add the frame to the container element
  14281. this.containerElement.appendChild(this.frame);
  14282. };
  14283. /**
  14284. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14285. * @private
  14286. */
  14287. Graph.prototype._createKeyBinds = function() {
  14288. var me = this;
  14289. this.mousetrap = mousetrap;
  14290. this.mousetrap.reset();
  14291. if (this.constants.keyboard.enabled == true) {
  14292. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14293. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14294. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14295. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14296. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14297. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14298. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14299. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14300. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14301. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14302. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14303. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14304. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14305. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14306. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14307. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14308. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14309. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14310. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14311. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14312. }
  14313. if (this.constants.dataManipulation.enabled == true) {
  14314. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14315. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14316. }
  14317. };
  14318. /**
  14319. * Get the pointer location from a touch location
  14320. * @param {{pageX: Number, pageY: Number}} touch
  14321. * @return {{x: Number, y: Number}} pointer
  14322. * @private
  14323. */
  14324. Graph.prototype._getPointer = function (touch) {
  14325. return {
  14326. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14327. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14328. };
  14329. };
  14330. /**
  14331. * On start of a touch gesture, store the pointer
  14332. * @param event
  14333. * @private
  14334. */
  14335. Graph.prototype._onTouch = function (event) {
  14336. this.drag.pointer = this._getPointer(event.gesture.center);
  14337. this.drag.pinched = false;
  14338. this.pinch.scale = this._getScale();
  14339. this._handleTouch(this.drag.pointer);
  14340. };
  14341. /**
  14342. * handle drag start event
  14343. * @private
  14344. */
  14345. Graph.prototype._onDragStart = function () {
  14346. this._handleDragStart();
  14347. };
  14348. /**
  14349. * This function is called by _onDragStart.
  14350. * It is separated out because we can then overload it for the datamanipulation system.
  14351. *
  14352. * @private
  14353. */
  14354. Graph.prototype._handleDragStart = function() {
  14355. var drag = this.drag;
  14356. var node = this._getNodeAt(drag.pointer);
  14357. // note: drag.pointer is set in _onTouch to get the initial touch location
  14358. drag.dragging = true;
  14359. drag.selection = [];
  14360. drag.translation = this._getTranslation();
  14361. drag.nodeId = null;
  14362. if (node != null) {
  14363. drag.nodeId = node.id;
  14364. // select the clicked node if not yet selected
  14365. if (!node.isSelected()) {
  14366. this._selectObject(node,false);
  14367. }
  14368. // create an array with the selected nodes and their original location and status
  14369. for (var objectId in this.selectionObj.nodes) {
  14370. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14371. var object = this.selectionObj.nodes[objectId];
  14372. var s = {
  14373. id: object.id,
  14374. node: object,
  14375. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14376. x: object.x,
  14377. y: object.y,
  14378. xFixed: object.xFixed,
  14379. yFixed: object.yFixed
  14380. };
  14381. object.xFixed = true;
  14382. object.yFixed = true;
  14383. drag.selection.push(s);
  14384. }
  14385. }
  14386. }
  14387. };
  14388. /**
  14389. * handle drag event
  14390. * @private
  14391. */
  14392. Graph.prototype._onDrag = function (event) {
  14393. this._handleOnDrag(event)
  14394. };
  14395. /**
  14396. * This function is called by _onDrag.
  14397. * It is separated out because we can then overload it for the datamanipulation system.
  14398. *
  14399. * @private
  14400. */
  14401. Graph.prototype._handleOnDrag = function(event) {
  14402. if (this.drag.pinched) {
  14403. return;
  14404. }
  14405. var pointer = this._getPointer(event.gesture.center);
  14406. var me = this,
  14407. drag = this.drag,
  14408. selection = drag.selection;
  14409. if (selection && selection.length) {
  14410. // calculate delta's and new location
  14411. var deltaX = pointer.x - drag.pointer.x,
  14412. deltaY = pointer.y - drag.pointer.y;
  14413. // update position of all selected nodes
  14414. selection.forEach(function (s) {
  14415. var node = s.node;
  14416. if (!s.xFixed) {
  14417. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14418. }
  14419. if (!s.yFixed) {
  14420. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14421. }
  14422. });
  14423. // start _animationStep if not yet running
  14424. if (!this.moving) {
  14425. this.moving = true;
  14426. this.start();
  14427. }
  14428. }
  14429. else {
  14430. // move the graph
  14431. var diffX = pointer.x - this.drag.pointer.x;
  14432. var diffY = pointer.y - this.drag.pointer.y;
  14433. this._setTranslation(
  14434. this.drag.translation.x + diffX,
  14435. this.drag.translation.y + diffY);
  14436. this._redraw();
  14437. this.moving = true;
  14438. }
  14439. };
  14440. /**
  14441. * handle drag start event
  14442. * @private
  14443. */
  14444. Graph.prototype._onDragEnd = function () {
  14445. this.drag.dragging = false;
  14446. var selection = this.drag.selection;
  14447. if (selection) {
  14448. selection.forEach(function (s) {
  14449. // restore original xFixed and yFixed
  14450. s.node.xFixed = s.xFixed;
  14451. s.node.yFixed = s.yFixed;
  14452. });
  14453. }
  14454. };
  14455. /**
  14456. * handle tap/click event: select/unselect a node
  14457. * @private
  14458. */
  14459. Graph.prototype._onTap = function (event) {
  14460. var pointer = this._getPointer(event.gesture.center);
  14461. this.pointerPosition = pointer;
  14462. this._handleTap(pointer);
  14463. };
  14464. /**
  14465. * handle doubletap event
  14466. * @private
  14467. */
  14468. Graph.prototype._onDoubleTap = function (event) {
  14469. var pointer = this._getPointer(event.gesture.center);
  14470. this._handleDoubleTap(pointer);
  14471. };
  14472. /**
  14473. * handle long tap event: multi select nodes
  14474. * @private
  14475. */
  14476. Graph.prototype._onHold = function (event) {
  14477. var pointer = this._getPointer(event.gesture.center);
  14478. this.pointerPosition = pointer;
  14479. this._handleOnHold(pointer);
  14480. };
  14481. /**
  14482. * handle the release of the screen
  14483. *
  14484. * @private
  14485. */
  14486. Graph.prototype._onRelease = function (event) {
  14487. var pointer = this._getPointer(event.gesture.center);
  14488. this._handleOnRelease(pointer);
  14489. };
  14490. /**
  14491. * Handle pinch event
  14492. * @param event
  14493. * @private
  14494. */
  14495. Graph.prototype._onPinch = function (event) {
  14496. var pointer = this._getPointer(event.gesture.center);
  14497. this.drag.pinched = true;
  14498. if (!('scale' in this.pinch)) {
  14499. this.pinch.scale = 1;
  14500. }
  14501. // TODO: enabled moving while pinching?
  14502. var scale = this.pinch.scale * event.gesture.scale;
  14503. this._zoom(scale, pointer)
  14504. };
  14505. /**
  14506. * Zoom the graph in or out
  14507. * @param {Number} scale a number around 1, and between 0.01 and 10
  14508. * @param {{x: Number, y: Number}} pointer Position on screen
  14509. * @return {Number} appliedScale scale is limited within the boundaries
  14510. * @private
  14511. */
  14512. Graph.prototype._zoom = function(scale, pointer) {
  14513. var scaleOld = this._getScale();
  14514. if (scale < 0.00001) {
  14515. scale = 0.00001;
  14516. }
  14517. if (scale > 10) {
  14518. scale = 10;
  14519. }
  14520. // + this.frame.canvas.clientHeight / 2
  14521. var translation = this._getTranslation();
  14522. var scaleFrac = scale / scaleOld;
  14523. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14524. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14525. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  14526. "y" : this._canvasToY(pointer.y)};
  14527. this._setScale(scale);
  14528. this._setTranslation(tx, ty);
  14529. this.updateClustersDefault();
  14530. this._redraw();
  14531. return scale;
  14532. };
  14533. /**
  14534. * Event handler for mouse wheel event, used to zoom the timeline
  14535. * See http://adomas.org/javascript-mouse-wheel/
  14536. * https://github.com/EightMedia/hammer.js/issues/256
  14537. * @param {MouseEvent} event
  14538. * @private
  14539. */
  14540. Graph.prototype._onMouseWheel = function(event) {
  14541. // retrieve delta
  14542. var delta = 0;
  14543. if (event.wheelDelta) { /* IE/Opera. */
  14544. delta = event.wheelDelta/120;
  14545. } else if (event.detail) { /* Mozilla case. */
  14546. // In Mozilla, sign of delta is different than in IE.
  14547. // Also, delta is multiple of 3.
  14548. delta = -event.detail/3;
  14549. }
  14550. // If delta is nonzero, handle it.
  14551. // Basically, delta is now positive if wheel was scrolled up,
  14552. // and negative, if wheel was scrolled down.
  14553. if (delta) {
  14554. // calculate the new scale
  14555. var scale = this._getScale();
  14556. var zoom = delta / 10;
  14557. if (delta < 0) {
  14558. zoom = zoom / (1 - zoom);
  14559. }
  14560. scale *= (1 + zoom);
  14561. // calculate the pointer location
  14562. var gesture = util.fakeGesture(this, event);
  14563. var pointer = this._getPointer(gesture.center);
  14564. // apply the new scale
  14565. this._zoom(scale, pointer);
  14566. }
  14567. // Prevent default actions caused by mouse wheel.
  14568. event.preventDefault();
  14569. };
  14570. /**
  14571. * Mouse move handler for checking whether the title moves over a node with a title.
  14572. * @param {Event} event
  14573. * @private
  14574. */
  14575. Graph.prototype._onMouseMoveTitle = function (event) {
  14576. var gesture = util.fakeGesture(this, event);
  14577. var pointer = this._getPointer(gesture.center);
  14578. // check if the previously selected node is still selected
  14579. if (this.popupNode) {
  14580. this._checkHidePopup(pointer);
  14581. }
  14582. // start a timeout that will check if the mouse is positioned above
  14583. // an element
  14584. var me = this;
  14585. var checkShow = function() {
  14586. me._checkShowPopup(pointer);
  14587. };
  14588. if (this.popupTimer) {
  14589. clearInterval(this.popupTimer); // stop any running calculationTimer
  14590. }
  14591. if (!this.drag.dragging) {
  14592. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  14593. }
  14594. };
  14595. /**
  14596. * Check if there is an element on the given position in the graph
  14597. * (a node or edge). If so, and if this element has a title,
  14598. * show a popup window with its title.
  14599. *
  14600. * @param {{x:Number, y:Number}} pointer
  14601. * @private
  14602. */
  14603. Graph.prototype._checkShowPopup = function (pointer) {
  14604. var obj = {
  14605. left: this._canvasToX(pointer.x),
  14606. top: this._canvasToY(pointer.y),
  14607. right: this._canvasToX(pointer.x),
  14608. bottom: this._canvasToY(pointer.y)
  14609. };
  14610. var id;
  14611. var lastPopupNode = this.popupNode;
  14612. if (this.popupNode == undefined) {
  14613. // search the nodes for overlap, select the top one in case of multiple nodes
  14614. var nodes = this.nodes;
  14615. for (id in nodes) {
  14616. if (nodes.hasOwnProperty(id)) {
  14617. var node = nodes[id];
  14618. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  14619. this.popupNode = node;
  14620. break;
  14621. }
  14622. }
  14623. }
  14624. }
  14625. if (this.popupNode === undefined) {
  14626. // search the edges for overlap
  14627. var edges = this.edges;
  14628. for (id in edges) {
  14629. if (edges.hasOwnProperty(id)) {
  14630. var edge = edges[id];
  14631. if (edge.connected && (edge.getTitle() !== undefined) &&
  14632. edge.isOverlappingWith(obj)) {
  14633. this.popupNode = edge;
  14634. break;
  14635. }
  14636. }
  14637. }
  14638. }
  14639. if (this.popupNode) {
  14640. // show popup message window
  14641. if (this.popupNode != lastPopupNode) {
  14642. var me = this;
  14643. if (!me.popup) {
  14644. me.popup = new Popup(me.frame, me.constants.tooltip);
  14645. }
  14646. // adjust a small offset such that the mouse cursor is located in the
  14647. // bottom left location of the popup, and you can easily move over the
  14648. // popup area
  14649. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14650. me.popup.setText(me.popupNode.getTitle());
  14651. me.popup.show();
  14652. }
  14653. }
  14654. else {
  14655. if (this.popup) {
  14656. this.popup.hide();
  14657. }
  14658. }
  14659. };
  14660. /**
  14661. * Check if the popup must be hided, which is the case when the mouse is no
  14662. * longer hovering on the object
  14663. * @param {{x:Number, y:Number}} pointer
  14664. * @private
  14665. */
  14666. Graph.prototype._checkHidePopup = function (pointer) {
  14667. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  14668. this.popupNode = undefined;
  14669. if (this.popup) {
  14670. this.popup.hide();
  14671. }
  14672. }
  14673. };
  14674. /**
  14675. * Set a new size for the graph
  14676. * @param {string} width Width in pixels or percentage (for example '800px'
  14677. * or '50%')
  14678. * @param {string} height Height in pixels or percentage (for example '400px'
  14679. * or '30%')
  14680. */
  14681. Graph.prototype.setSize = function(width, height) {
  14682. this.frame.style.width = width;
  14683. this.frame.style.height = height;
  14684. this.frame.canvas.style.width = '100%';
  14685. this.frame.canvas.style.height = '100%';
  14686. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14687. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14688. if (this.manipulationDiv !== undefined) {
  14689. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  14690. }
  14691. if (this.navigationDivs !== undefined) {
  14692. if (this.navigationDivs['wrapper'] !== undefined) {
  14693. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14694. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14695. }
  14696. }
  14697. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  14698. };
  14699. /**
  14700. * Set a data set with nodes for the graph
  14701. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14702. * @private
  14703. */
  14704. Graph.prototype._setNodes = function(nodes) {
  14705. var oldNodesData = this.nodesData;
  14706. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14707. this.nodesData = nodes;
  14708. }
  14709. else if (nodes instanceof Array) {
  14710. this.nodesData = new DataSet();
  14711. this.nodesData.add(nodes);
  14712. }
  14713. else if (!nodes) {
  14714. this.nodesData = new DataSet();
  14715. }
  14716. else {
  14717. throw new TypeError('Array or DataSet expected');
  14718. }
  14719. if (oldNodesData) {
  14720. // unsubscribe from old dataset
  14721. util.forEach(this.nodesListeners, function (callback, event) {
  14722. oldNodesData.off(event, callback);
  14723. });
  14724. }
  14725. // remove drawn nodes
  14726. this.nodes = {};
  14727. if (this.nodesData) {
  14728. // subscribe to new dataset
  14729. var me = this;
  14730. util.forEach(this.nodesListeners, function (callback, event) {
  14731. me.nodesData.on(event, callback);
  14732. });
  14733. // draw all new nodes
  14734. var ids = this.nodesData.getIds();
  14735. this._addNodes(ids);
  14736. }
  14737. this._updateSelection();
  14738. };
  14739. /**
  14740. * Add nodes
  14741. * @param {Number[] | String[]} ids
  14742. * @private
  14743. */
  14744. Graph.prototype._addNodes = function(ids) {
  14745. var id;
  14746. for (var i = 0, len = ids.length; i < len; i++) {
  14747. id = ids[i];
  14748. var data = this.nodesData.get(id);
  14749. var node = new Node(data, this.images, this.groups, this.constants);
  14750. this.nodes[id] = node; // note: this may replace an existing node
  14751. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  14752. var radius = 10 * 0.1*ids.length;
  14753. var angle = 2 * Math.PI * Math.random();
  14754. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  14755. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  14756. }
  14757. this.moving = true;
  14758. }
  14759. this._updateNodeIndexList();
  14760. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14761. this._resetLevels();
  14762. this._setupHierarchicalLayout();
  14763. }
  14764. this._updateCalculationNodes();
  14765. this._reconnectEdges();
  14766. this._updateValueRange(this.nodes);
  14767. this.updateLabels();
  14768. };
  14769. /**
  14770. * Update existing nodes, or create them when not yet existing
  14771. * @param {Number[] | String[]} ids
  14772. * @private
  14773. */
  14774. Graph.prototype._updateNodes = function(ids) {
  14775. var nodes = this.nodes,
  14776. nodesData = this.nodesData;
  14777. for (var i = 0, len = ids.length; i < len; i++) {
  14778. var id = ids[i];
  14779. var node = nodes[id];
  14780. var data = nodesData.get(id);
  14781. if (node) {
  14782. // update node
  14783. node.setProperties(data, this.constants);
  14784. }
  14785. else {
  14786. // create node
  14787. node = new Node(properties, this.images, this.groups, this.constants);
  14788. nodes[id] = node;
  14789. }
  14790. }
  14791. this.moving = true;
  14792. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14793. this._resetLevels();
  14794. this._setupHierarchicalLayout();
  14795. }
  14796. this._updateNodeIndexList();
  14797. this._reconnectEdges();
  14798. this._updateValueRange(nodes);
  14799. };
  14800. /**
  14801. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  14802. * @param {Number[] | String[]} ids
  14803. * @private
  14804. */
  14805. Graph.prototype._removeNodes = function(ids) {
  14806. var nodes = this.nodes;
  14807. for (var i = 0, len = ids.length; i < len; i++) {
  14808. var id = ids[i];
  14809. delete nodes[id];
  14810. }
  14811. this._updateNodeIndexList();
  14812. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14813. this._resetLevels();
  14814. this._setupHierarchicalLayout();
  14815. }
  14816. this._updateCalculationNodes();
  14817. this._reconnectEdges();
  14818. this._updateSelection();
  14819. this._updateValueRange(nodes);
  14820. };
  14821. /**
  14822. * Load edges by reading the data table
  14823. * @param {Array | DataSet | DataView} edges The data containing the edges.
  14824. * @private
  14825. * @private
  14826. */
  14827. Graph.prototype._setEdges = function(edges) {
  14828. var oldEdgesData = this.edgesData;
  14829. if (edges instanceof DataSet || edges instanceof DataView) {
  14830. this.edgesData = edges;
  14831. }
  14832. else if (edges instanceof Array) {
  14833. this.edgesData = new DataSet();
  14834. this.edgesData.add(edges);
  14835. }
  14836. else if (!edges) {
  14837. this.edgesData = new DataSet();
  14838. }
  14839. else {
  14840. throw new TypeError('Array or DataSet expected');
  14841. }
  14842. if (oldEdgesData) {
  14843. // unsubscribe from old dataset
  14844. util.forEach(this.edgesListeners, function (callback, event) {
  14845. oldEdgesData.off(event, callback);
  14846. });
  14847. }
  14848. // remove drawn edges
  14849. this.edges = {};
  14850. if (this.edgesData) {
  14851. // subscribe to new dataset
  14852. var me = this;
  14853. util.forEach(this.edgesListeners, function (callback, event) {
  14854. me.edgesData.on(event, callback);
  14855. });
  14856. // draw all new nodes
  14857. var ids = this.edgesData.getIds();
  14858. this._addEdges(ids);
  14859. }
  14860. this._reconnectEdges();
  14861. };
  14862. /**
  14863. * Add edges
  14864. * @param {Number[] | String[]} ids
  14865. * @private
  14866. */
  14867. Graph.prototype._addEdges = function (ids) {
  14868. var edges = this.edges,
  14869. edgesData = this.edgesData;
  14870. for (var i = 0, len = ids.length; i < len; i++) {
  14871. var id = ids[i];
  14872. var oldEdge = edges[id];
  14873. if (oldEdge) {
  14874. oldEdge.disconnect();
  14875. }
  14876. var data = edgesData.get(id, {"showInternalIds" : true});
  14877. edges[id] = new Edge(data, this, this.constants);
  14878. }
  14879. this.moving = true;
  14880. this._updateValueRange(edges);
  14881. this._createBezierNodes();
  14882. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14883. this._resetLevels();
  14884. this._setupHierarchicalLayout();
  14885. }
  14886. this._updateCalculationNodes();
  14887. };
  14888. /**
  14889. * Update existing edges, or create them when not yet existing
  14890. * @param {Number[] | String[]} ids
  14891. * @private
  14892. */
  14893. Graph.prototype._updateEdges = function (ids) {
  14894. var edges = this.edges,
  14895. edgesData = this.edgesData;
  14896. for (var i = 0, len = ids.length; i < len; i++) {
  14897. var id = ids[i];
  14898. var data = edgesData.get(id);
  14899. var edge = edges[id];
  14900. if (edge) {
  14901. // update edge
  14902. edge.disconnect();
  14903. edge.setProperties(data, this.constants);
  14904. edge.connect();
  14905. }
  14906. else {
  14907. // create edge
  14908. edge = new Edge(data, this, this.constants);
  14909. this.edges[id] = edge;
  14910. }
  14911. }
  14912. this._createBezierNodes();
  14913. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14914. this._resetLevels();
  14915. this._setupHierarchicalLayout();
  14916. }
  14917. this.moving = true;
  14918. this._updateValueRange(edges);
  14919. };
  14920. /**
  14921. * Remove existing edges. Non existing ids will be ignored
  14922. * @param {Number[] | String[]} ids
  14923. * @private
  14924. */
  14925. Graph.prototype._removeEdges = function (ids) {
  14926. var edges = this.edges;
  14927. for (var i = 0, len = ids.length; i < len; i++) {
  14928. var id = ids[i];
  14929. var edge = edges[id];
  14930. if (edge) {
  14931. if (edge.via != null) {
  14932. delete this.sectors['support']['nodes'][edge.via.id];
  14933. }
  14934. edge.disconnect();
  14935. delete edges[id];
  14936. }
  14937. }
  14938. this.moving = true;
  14939. this._updateValueRange(edges);
  14940. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14941. this._resetLevels();
  14942. this._setupHierarchicalLayout();
  14943. }
  14944. this._updateCalculationNodes();
  14945. };
  14946. /**
  14947. * Reconnect all edges
  14948. * @private
  14949. */
  14950. Graph.prototype._reconnectEdges = function() {
  14951. var id,
  14952. nodes = this.nodes,
  14953. edges = this.edges;
  14954. for (id in nodes) {
  14955. if (nodes.hasOwnProperty(id)) {
  14956. nodes[id].edges = [];
  14957. }
  14958. }
  14959. for (id in edges) {
  14960. if (edges.hasOwnProperty(id)) {
  14961. var edge = edges[id];
  14962. edge.from = null;
  14963. edge.to = null;
  14964. edge.connect();
  14965. }
  14966. }
  14967. };
  14968. /**
  14969. * Update the values of all object in the given array according to the current
  14970. * value range of the objects in the array.
  14971. * @param {Object} obj An object containing a set of Edges or Nodes
  14972. * The objects must have a method getValue() and
  14973. * setValueRange(min, max).
  14974. * @private
  14975. */
  14976. Graph.prototype._updateValueRange = function(obj) {
  14977. var id;
  14978. // determine the range of the objects
  14979. var valueMin = undefined;
  14980. var valueMax = undefined;
  14981. for (id in obj) {
  14982. if (obj.hasOwnProperty(id)) {
  14983. var value = obj[id].getValue();
  14984. if (value !== undefined) {
  14985. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  14986. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  14987. }
  14988. }
  14989. }
  14990. // adjust the range of all objects
  14991. if (valueMin !== undefined && valueMax !== undefined) {
  14992. for (id in obj) {
  14993. if (obj.hasOwnProperty(id)) {
  14994. obj[id].setValueRange(valueMin, valueMax);
  14995. }
  14996. }
  14997. }
  14998. };
  14999. /**
  15000. * Redraw the graph with the current data
  15001. * chart will be resized too.
  15002. */
  15003. Graph.prototype.redraw = function() {
  15004. this.setSize(this.width, this.height);
  15005. this._redraw();
  15006. };
  15007. /**
  15008. * Redraw the graph with the current data
  15009. * @private
  15010. */
  15011. Graph.prototype._redraw = function() {
  15012. var ctx = this.frame.canvas.getContext('2d');
  15013. // clear the canvas
  15014. var w = this.frame.canvas.width;
  15015. var h = this.frame.canvas.height;
  15016. ctx.clearRect(0, 0, w, h);
  15017. // set scaling and translation
  15018. ctx.save();
  15019. ctx.translate(this.translation.x, this.translation.y);
  15020. ctx.scale(this.scale, this.scale);
  15021. this.canvasTopLeft = {
  15022. "x": this._canvasToX(0),
  15023. "y": this._canvasToY(0)
  15024. };
  15025. this.canvasBottomRight = {
  15026. "x": this._canvasToX(this.frame.canvas.clientWidth),
  15027. "y": this._canvasToY(this.frame.canvas.clientHeight)
  15028. };
  15029. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15030. this._doInAllSectors("_drawEdges",ctx);
  15031. this._doInAllSectors("_drawNodes",ctx,false);
  15032. // this._doInSupportSector("_drawNodes",ctx,true);
  15033. // this._drawTree(ctx,"#F00F0F");
  15034. // restore original scaling and translation
  15035. ctx.restore();
  15036. };
  15037. /**
  15038. * Set the translation of the graph
  15039. * @param {Number} offsetX Horizontal offset
  15040. * @param {Number} offsetY Vertical offset
  15041. * @private
  15042. */
  15043. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15044. if (this.translation === undefined) {
  15045. this.translation = {
  15046. x: 0,
  15047. y: 0
  15048. };
  15049. }
  15050. if (offsetX !== undefined) {
  15051. this.translation.x = offsetX;
  15052. }
  15053. if (offsetY !== undefined) {
  15054. this.translation.y = offsetY;
  15055. }
  15056. };
  15057. /**
  15058. * Get the translation of the graph
  15059. * @return {Object} translation An object with parameters x and y, both a number
  15060. * @private
  15061. */
  15062. Graph.prototype._getTranslation = function() {
  15063. return {
  15064. x: this.translation.x,
  15065. y: this.translation.y
  15066. };
  15067. };
  15068. /**
  15069. * Scale the graph
  15070. * @param {Number} scale Scaling factor 1.0 is unscaled
  15071. * @private
  15072. */
  15073. Graph.prototype._setScale = function(scale) {
  15074. this.scale = scale;
  15075. };
  15076. /**
  15077. * Get the current scale of the graph
  15078. * @return {Number} scale Scaling factor 1.0 is unscaled
  15079. * @private
  15080. */
  15081. Graph.prototype._getScale = function() {
  15082. return this.scale;
  15083. };
  15084. /**
  15085. * Convert a horizontal point on the HTML canvas to the x-value of the model
  15086. * @param {number} x
  15087. * @returns {number}
  15088. * @private
  15089. */
  15090. Graph.prototype._canvasToX = function(x) {
  15091. return (x - this.translation.x) / this.scale;
  15092. };
  15093. /**
  15094. * Convert an x-value in the model to a horizontal point on the HTML canvas
  15095. * @param {number} x
  15096. * @returns {number}
  15097. * @private
  15098. */
  15099. Graph.prototype._xToCanvas = function(x) {
  15100. return x * this.scale + this.translation.x;
  15101. };
  15102. /**
  15103. * Convert a vertical point on the HTML canvas to the y-value of the model
  15104. * @param {number} y
  15105. * @returns {number}
  15106. * @private
  15107. */
  15108. Graph.prototype._canvasToY = function(y) {
  15109. return (y - this.translation.y) / this.scale;
  15110. };
  15111. /**
  15112. * Convert an y-value in the model to a vertical point on the HTML canvas
  15113. * @param {number} y
  15114. * @returns {number}
  15115. * @private
  15116. */
  15117. Graph.prototype._yToCanvas = function(y) {
  15118. return y * this.scale + this.translation.y ;
  15119. };
  15120. /**
  15121. * Redraw all nodes
  15122. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15123. * @param {CanvasRenderingContext2D} ctx
  15124. * @param {Boolean} [alwaysShow]
  15125. * @private
  15126. */
  15127. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15128. if (alwaysShow === undefined) {
  15129. alwaysShow = false;
  15130. }
  15131. // first draw the unselected nodes
  15132. var nodes = this.nodes;
  15133. var selected = [];
  15134. for (var id in nodes) {
  15135. if (nodes.hasOwnProperty(id)) {
  15136. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15137. if (nodes[id].isSelected()) {
  15138. selected.push(id);
  15139. }
  15140. else {
  15141. if (nodes[id].inArea() || alwaysShow) {
  15142. nodes[id].draw(ctx);
  15143. }
  15144. }
  15145. }
  15146. }
  15147. // draw the selected nodes on top
  15148. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15149. if (nodes[selected[s]].inArea() || alwaysShow) {
  15150. nodes[selected[s]].draw(ctx);
  15151. }
  15152. }
  15153. };
  15154. /**
  15155. * Redraw all edges
  15156. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15157. * @param {CanvasRenderingContext2D} ctx
  15158. * @private
  15159. */
  15160. Graph.prototype._drawEdges = function(ctx) {
  15161. var edges = this.edges;
  15162. for (var id in edges) {
  15163. if (edges.hasOwnProperty(id)) {
  15164. var edge = edges[id];
  15165. edge.setScale(this.scale);
  15166. if (edge.connected) {
  15167. edges[id].draw(ctx);
  15168. }
  15169. }
  15170. }
  15171. };
  15172. /**
  15173. * Find a stable position for all nodes
  15174. * @private
  15175. */
  15176. Graph.prototype._stabilize = function() {
  15177. if (this.constants.freezeForStabilization == true) {
  15178. this._freezeDefinedNodes();
  15179. }
  15180. // find stable position
  15181. var count = 0;
  15182. while (this.moving && count < this.constants.stabilizationIterations) {
  15183. this._physicsTick();
  15184. count++;
  15185. }
  15186. this.zoomExtent(false,true);
  15187. if (this.constants.freezeForStabilization == true) {
  15188. this._restoreFrozenNodes();
  15189. }
  15190. this.emit("stabilized",{iterations:count});
  15191. };
  15192. /**
  15193. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  15194. * because only the supportnodes for the smoothCurves have to settle.
  15195. *
  15196. * @private
  15197. */
  15198. Graph.prototype._freezeDefinedNodes = function() {
  15199. var nodes = this.nodes;
  15200. for (var id in nodes) {
  15201. if (nodes.hasOwnProperty(id)) {
  15202. if (nodes[id].x != null && nodes[id].y != null) {
  15203. nodes[id].fixedData.x = nodes[id].xFixed;
  15204. nodes[id].fixedData.y = nodes[id].yFixed;
  15205. nodes[id].xFixed = true;
  15206. nodes[id].yFixed = true;
  15207. }
  15208. }
  15209. }
  15210. };
  15211. /**
  15212. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  15213. *
  15214. * @private
  15215. */
  15216. Graph.prototype._restoreFrozenNodes = function() {
  15217. var nodes = this.nodes;
  15218. for (var id in nodes) {
  15219. if (nodes.hasOwnProperty(id)) {
  15220. if (nodes[id].fixedData.x != null) {
  15221. nodes[id].xFixed = nodes[id].fixedData.x;
  15222. nodes[id].yFixed = nodes[id].fixedData.y;
  15223. }
  15224. }
  15225. }
  15226. };
  15227. /**
  15228. * Check if any of the nodes is still moving
  15229. * @param {number} vmin the minimum velocity considered as 'moving'
  15230. * @return {boolean} true if moving, false if non of the nodes is moving
  15231. * @private
  15232. */
  15233. Graph.prototype._isMoving = function(vmin) {
  15234. var nodes = this.nodes;
  15235. for (var id in nodes) {
  15236. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15237. return true;
  15238. }
  15239. }
  15240. return false;
  15241. };
  15242. /**
  15243. * /**
  15244. * Perform one discrete step for all nodes
  15245. *
  15246. * @private
  15247. */
  15248. Graph.prototype._discreteStepNodes = function() {
  15249. var interval = this.physicsDiscreteStepsize;
  15250. var nodes = this.nodes;
  15251. var nodeId;
  15252. var nodesPresent = false;
  15253. if (this.constants.maxVelocity > 0) {
  15254. for (nodeId in nodes) {
  15255. if (nodes.hasOwnProperty(nodeId)) {
  15256. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15257. nodesPresent = true;
  15258. }
  15259. }
  15260. }
  15261. else {
  15262. for (nodeId in nodes) {
  15263. if (nodes.hasOwnProperty(nodeId)) {
  15264. nodes[nodeId].discreteStep(interval);
  15265. nodesPresent = true;
  15266. }
  15267. }
  15268. }
  15269. if (nodesPresent == true) {
  15270. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15271. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15272. this.moving = true;
  15273. }
  15274. else {
  15275. this.moving = this._isMoving(vminCorrected);
  15276. }
  15277. }
  15278. };
  15279. /**
  15280. * A single simulation step (or "tick") in the physics simulation
  15281. *
  15282. * @private
  15283. */
  15284. Graph.prototype._physicsTick = function() {
  15285. if (!this.freezeSimulation) {
  15286. if (this.moving) {
  15287. this._doInAllActiveSectors("_initializeForceCalculation");
  15288. this._doInAllActiveSectors("_discreteStepNodes");
  15289. if (this.constants.smoothCurves) {
  15290. this._doInSupportSector("_discreteStepNodes");
  15291. }
  15292. this._findCenter(this._getRange())
  15293. }
  15294. }
  15295. };
  15296. /**
  15297. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15298. * It reschedules itself at the beginning of the function
  15299. *
  15300. * @private
  15301. */
  15302. Graph.prototype._animationStep = function() {
  15303. // reset the timer so a new scheduled animation step can be set
  15304. this.timer = undefined;
  15305. // handle the keyboad movement
  15306. this._handleNavigation();
  15307. // this schedules a new animation step
  15308. this.start();
  15309. // start the physics simulation
  15310. var calculationTime = Date.now();
  15311. var maxSteps = 1;
  15312. this._physicsTick();
  15313. var timeRequired = Date.now() - calculationTime;
  15314. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15315. this._physicsTick();
  15316. timeRequired = Date.now() - calculationTime;
  15317. maxSteps++;
  15318. }
  15319. // start the rendering process
  15320. var renderTime = Date.now();
  15321. this._redraw();
  15322. this.renderTime = Date.now() - renderTime;
  15323. };
  15324. if (typeof window !== 'undefined') {
  15325. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15326. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15327. }
  15328. /**
  15329. * Schedule a animation step with the refreshrate interval.
  15330. */
  15331. Graph.prototype.start = function() {
  15332. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15333. if (!this.timer) {
  15334. var ua = navigator.userAgent.toLowerCase();
  15335. var requiresTimeout = false;
  15336. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15337. requiresTimeout = true;
  15338. }
  15339. else if (ua.indexOf('safari') != -1) { // safari
  15340. if (ua.indexOf('chrome') <= -1) {
  15341. requiresTimeout = true;
  15342. }
  15343. }
  15344. if (requiresTimeout == true) {
  15345. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15346. }
  15347. else{
  15348. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15349. }
  15350. }
  15351. }
  15352. else {
  15353. this._redraw();
  15354. }
  15355. };
  15356. /**
  15357. * Move the graph according to the keyboard presses.
  15358. *
  15359. * @private
  15360. */
  15361. Graph.prototype._handleNavigation = function() {
  15362. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15363. var translation = this._getTranslation();
  15364. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15365. }
  15366. if (this.zoomIncrement != 0) {
  15367. var center = {
  15368. x: this.frame.canvas.clientWidth / 2,
  15369. y: this.frame.canvas.clientHeight / 2
  15370. };
  15371. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15372. }
  15373. };
  15374. /**
  15375. * Freeze the _animationStep
  15376. */
  15377. Graph.prototype.toggleFreeze = function() {
  15378. if (this.freezeSimulation == false) {
  15379. this.freezeSimulation = true;
  15380. }
  15381. else {
  15382. this.freezeSimulation = false;
  15383. this.start();
  15384. }
  15385. };
  15386. /**
  15387. * This function cleans the support nodes if they are not needed and adds them when they are.
  15388. *
  15389. * @param {boolean} [disableStart]
  15390. * @private
  15391. */
  15392. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15393. if (disableStart === undefined) {
  15394. disableStart = true;
  15395. }
  15396. if (this.constants.smoothCurves == true) {
  15397. this._createBezierNodes();
  15398. }
  15399. else {
  15400. // delete the support nodes
  15401. this.sectors['support']['nodes'] = {};
  15402. for (var edgeId in this.edges) {
  15403. if (this.edges.hasOwnProperty(edgeId)) {
  15404. this.edges[edgeId].smooth = false;
  15405. this.edges[edgeId].via = null;
  15406. }
  15407. }
  15408. }
  15409. this._updateCalculationNodes();
  15410. if (!disableStart) {
  15411. this.moving = true;
  15412. this.start();
  15413. }
  15414. };
  15415. /**
  15416. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  15417. * are used for the force calculation.
  15418. *
  15419. * @private
  15420. */
  15421. Graph.prototype._createBezierNodes = function() {
  15422. if (this.constants.smoothCurves == true) {
  15423. for (var edgeId in this.edges) {
  15424. if (this.edges.hasOwnProperty(edgeId)) {
  15425. var edge = this.edges[edgeId];
  15426. if (edge.via == null) {
  15427. edge.smooth = true;
  15428. var nodeId = "edgeId:".concat(edge.id);
  15429. this.sectors['support']['nodes'][nodeId] = new Node(
  15430. {id:nodeId,
  15431. mass:1,
  15432. shape:'circle',
  15433. image:"",
  15434. internalMultiplier:1
  15435. },{},{},this.constants);
  15436. edge.via = this.sectors['support']['nodes'][nodeId];
  15437. edge.via.parentEdgeId = edge.id;
  15438. edge.positionBezierNode();
  15439. }
  15440. }
  15441. }
  15442. }
  15443. };
  15444. /**
  15445. * load the functions that load the mixins into the prototype.
  15446. *
  15447. * @private
  15448. */
  15449. Graph.prototype._initializeMixinLoaders = function () {
  15450. for (var mixinFunction in graphMixinLoaders) {
  15451. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15452. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15453. }
  15454. }
  15455. };
  15456. /**
  15457. * Load the XY positions of the nodes into the dataset.
  15458. */
  15459. Graph.prototype.storePosition = function() {
  15460. var dataArray = [];
  15461. for (var nodeId in this.nodes) {
  15462. if (this.nodes.hasOwnProperty(nodeId)) {
  15463. var node = this.nodes[nodeId];
  15464. var allowedToMoveX = !this.nodes.xFixed;
  15465. var allowedToMoveY = !this.nodes.yFixed;
  15466. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15467. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15468. }
  15469. }
  15470. }
  15471. this.nodesData.update(dataArray);
  15472. };
  15473. /**
  15474. * vis.js module exports
  15475. */
  15476. var vis = {
  15477. util: util,
  15478. DataSet: DataSet,
  15479. DataView: DataView,
  15480. Range: Range,
  15481. Stack: Stack,
  15482. TimeStep: TimeStep,
  15483. components: {
  15484. items: {
  15485. Item: Item,
  15486. ItemBox: ItemBox,
  15487. ItemPoint: ItemPoint,
  15488. ItemRange: ItemRange
  15489. },
  15490. Component: Component,
  15491. Panel: Panel,
  15492. RootPanel: RootPanel,
  15493. ItemSet: ItemSet,
  15494. TimeAxis: TimeAxis
  15495. },
  15496. graph: {
  15497. Node: Node,
  15498. Edge: Edge,
  15499. Popup: Popup,
  15500. Groups: Groups,
  15501. Images: Images
  15502. },
  15503. Timeline: Timeline,
  15504. Graph: Graph
  15505. };
  15506. /**
  15507. * CommonJS module exports
  15508. */
  15509. if (typeof exports !== 'undefined') {
  15510. exports = vis;
  15511. }
  15512. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15513. module.exports = vis;
  15514. }
  15515. /**
  15516. * AMD module exports
  15517. */
  15518. if (typeof(define) === 'function') {
  15519. define(function () {
  15520. return vis;
  15521. });
  15522. }
  15523. /**
  15524. * Window exports
  15525. */
  15526. if (typeof window !== 'undefined') {
  15527. // attach the module to the window, load as a regular javascript file
  15528. window['vis'] = vis;
  15529. }
  15530. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  15531. /**
  15532. * Expose `Emitter`.
  15533. */
  15534. module.exports = Emitter;
  15535. /**
  15536. * Initialize a new `Emitter`.
  15537. *
  15538. * @api public
  15539. */
  15540. function Emitter(obj) {
  15541. if (obj) return mixin(obj);
  15542. };
  15543. /**
  15544. * Mixin the emitter properties.
  15545. *
  15546. * @param {Object} obj
  15547. * @return {Object}
  15548. * @api private
  15549. */
  15550. function mixin(obj) {
  15551. for (var key in Emitter.prototype) {
  15552. obj[key] = Emitter.prototype[key];
  15553. }
  15554. return obj;
  15555. }
  15556. /**
  15557. * Listen on the given `event` with `fn`.
  15558. *
  15559. * @param {String} event
  15560. * @param {Function} fn
  15561. * @return {Emitter}
  15562. * @api public
  15563. */
  15564. Emitter.prototype.on =
  15565. Emitter.prototype.addEventListener = function(event, fn){
  15566. this._callbacks = this._callbacks || {};
  15567. (this._callbacks[event] = this._callbacks[event] || [])
  15568. .push(fn);
  15569. return this;
  15570. };
  15571. /**
  15572. * Adds an `event` listener that will be invoked a single
  15573. * time then automatically removed.
  15574. *
  15575. * @param {String} event
  15576. * @param {Function} fn
  15577. * @return {Emitter}
  15578. * @api public
  15579. */
  15580. Emitter.prototype.once = function(event, fn){
  15581. var self = this;
  15582. this._callbacks = this._callbacks || {};
  15583. function on() {
  15584. self.off(event, on);
  15585. fn.apply(this, arguments);
  15586. }
  15587. on.fn = fn;
  15588. this.on(event, on);
  15589. return this;
  15590. };
  15591. /**
  15592. * Remove the given callback for `event` or all
  15593. * registered callbacks.
  15594. *
  15595. * @param {String} event
  15596. * @param {Function} fn
  15597. * @return {Emitter}
  15598. * @api public
  15599. */
  15600. Emitter.prototype.off =
  15601. Emitter.prototype.removeListener =
  15602. Emitter.prototype.removeAllListeners =
  15603. Emitter.prototype.removeEventListener = function(event, fn){
  15604. this._callbacks = this._callbacks || {};
  15605. // all
  15606. if (0 == arguments.length) {
  15607. this._callbacks = {};
  15608. return this;
  15609. }
  15610. // specific event
  15611. var callbacks = this._callbacks[event];
  15612. if (!callbacks) return this;
  15613. // remove all handlers
  15614. if (1 == arguments.length) {
  15615. delete this._callbacks[event];
  15616. return this;
  15617. }
  15618. // remove specific handler
  15619. var cb;
  15620. for (var i = 0; i < callbacks.length; i++) {
  15621. cb = callbacks[i];
  15622. if (cb === fn || cb.fn === fn) {
  15623. callbacks.splice(i, 1);
  15624. break;
  15625. }
  15626. }
  15627. return this;
  15628. };
  15629. /**
  15630. * Emit `event` with the given args.
  15631. *
  15632. * @param {String} event
  15633. * @param {Mixed} ...
  15634. * @return {Emitter}
  15635. */
  15636. Emitter.prototype.emit = function(event){
  15637. this._callbacks = this._callbacks || {};
  15638. var args = [].slice.call(arguments, 1)
  15639. , callbacks = this._callbacks[event];
  15640. if (callbacks) {
  15641. callbacks = callbacks.slice(0);
  15642. for (var i = 0, len = callbacks.length; i < len; ++i) {
  15643. callbacks[i].apply(this, args);
  15644. }
  15645. }
  15646. return this;
  15647. };
  15648. /**
  15649. * Return array of callbacks for `event`.
  15650. *
  15651. * @param {String} event
  15652. * @return {Array}
  15653. * @api public
  15654. */
  15655. Emitter.prototype.listeners = function(event){
  15656. this._callbacks = this._callbacks || {};
  15657. return this._callbacks[event] || [];
  15658. };
  15659. /**
  15660. * Check if this emitter has `event` handlers.
  15661. *
  15662. * @param {String} event
  15663. * @return {Boolean}
  15664. * @api public
  15665. */
  15666. Emitter.prototype.hasListeners = function(event){
  15667. return !! this.listeners(event).length;
  15668. };
  15669. },{}],3:[function(require,module,exports){
  15670. /*! Hammer.JS - v1.0.5 - 2013-04-07
  15671. * http://eightmedia.github.com/hammer.js
  15672. *
  15673. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  15674. * Licensed under the MIT license */
  15675. (function(window, undefined) {
  15676. 'use strict';
  15677. /**
  15678. * Hammer
  15679. * use this to create instances
  15680. * @param {HTMLElement} element
  15681. * @param {Object} options
  15682. * @returns {Hammer.Instance}
  15683. * @constructor
  15684. */
  15685. var Hammer = function(element, options) {
  15686. return new Hammer.Instance(element, options || {});
  15687. };
  15688. // default settings
  15689. Hammer.defaults = {
  15690. // add styles and attributes to the element to prevent the browser from doing
  15691. // its native behavior. this doesnt prevent the scrolling, but cancels
  15692. // the contextmenu, tap highlighting etc
  15693. // set to false to disable this
  15694. stop_browser_behavior: {
  15695. // this also triggers onselectstart=false for IE
  15696. userSelect: 'none',
  15697. // this makes the element blocking in IE10 >, you could experiment with the value
  15698. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  15699. touchAction: 'none',
  15700. touchCallout: 'none',
  15701. contentZooming: 'none',
  15702. userDrag: 'none',
  15703. tapHighlightColor: 'rgba(0,0,0,0)'
  15704. }
  15705. // more settings are defined per gesture at gestures.js
  15706. };
  15707. // detect touchevents
  15708. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  15709. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  15710. // dont use mouseevents on mobile devices
  15711. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  15712. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  15713. // eventtypes per touchevent (start, move, end)
  15714. // are filled by Hammer.event.determineEventTypes on setup
  15715. Hammer.EVENT_TYPES = {};
  15716. // direction defines
  15717. Hammer.DIRECTION_DOWN = 'down';
  15718. Hammer.DIRECTION_LEFT = 'left';
  15719. Hammer.DIRECTION_UP = 'up';
  15720. Hammer.DIRECTION_RIGHT = 'right';
  15721. // pointer type
  15722. Hammer.POINTER_MOUSE = 'mouse';
  15723. Hammer.POINTER_TOUCH = 'touch';
  15724. Hammer.POINTER_PEN = 'pen';
  15725. // touch event defines
  15726. Hammer.EVENT_START = 'start';
  15727. Hammer.EVENT_MOVE = 'move';
  15728. Hammer.EVENT_END = 'end';
  15729. // hammer document where the base events are added at
  15730. Hammer.DOCUMENT = document;
  15731. // plugins namespace
  15732. Hammer.plugins = {};
  15733. // if the window events are set...
  15734. Hammer.READY = false;
  15735. /**
  15736. * setup events to detect gestures on the document
  15737. */
  15738. function setup() {
  15739. if(Hammer.READY) {
  15740. return;
  15741. }
  15742. // find what eventtypes we add listeners to
  15743. Hammer.event.determineEventTypes();
  15744. // Register all gestures inside Hammer.gestures
  15745. for(var name in Hammer.gestures) {
  15746. if(Hammer.gestures.hasOwnProperty(name)) {
  15747. Hammer.detection.register(Hammer.gestures[name]);
  15748. }
  15749. }
  15750. // Add touch events on the document
  15751. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  15752. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  15753. // Hammer is ready...!
  15754. Hammer.READY = true;
  15755. }
  15756. /**
  15757. * create new hammer instance
  15758. * all methods should return the instance itself, so it is chainable.
  15759. * @param {HTMLElement} element
  15760. * @param {Object} [options={}]
  15761. * @returns {Hammer.Instance}
  15762. * @constructor
  15763. */
  15764. Hammer.Instance = function(element, options) {
  15765. var self = this;
  15766. // setup HammerJS window events and register all gestures
  15767. // this also sets up the default options
  15768. setup();
  15769. this.element = element;
  15770. // start/stop detection option
  15771. this.enabled = true;
  15772. // merge options
  15773. this.options = Hammer.utils.extend(
  15774. Hammer.utils.extend({}, Hammer.defaults),
  15775. options || {});
  15776. // add some css to the element to prevent the browser from doing its native behavoir
  15777. if(this.options.stop_browser_behavior) {
  15778. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  15779. }
  15780. // start detection on touchstart
  15781. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  15782. if(self.enabled) {
  15783. Hammer.detection.startDetect(self, ev);
  15784. }
  15785. });
  15786. // return instance
  15787. return this;
  15788. };
  15789. Hammer.Instance.prototype = {
  15790. /**
  15791. * bind events to the instance
  15792. * @param {String} gesture
  15793. * @param {Function} handler
  15794. * @returns {Hammer.Instance}
  15795. */
  15796. on: function onEvent(gesture, handler){
  15797. var gestures = gesture.split(' ');
  15798. for(var t=0; t<gestures.length; t++) {
  15799. this.element.addEventListener(gestures[t], handler, false);
  15800. }
  15801. return this;
  15802. },
  15803. /**
  15804. * unbind events to the instance
  15805. * @param {String} gesture
  15806. * @param {Function} handler
  15807. * @returns {Hammer.Instance}
  15808. */
  15809. off: function offEvent(gesture, handler){
  15810. var gestures = gesture.split(' ');
  15811. for(var t=0; t<gestures.length; t++) {
  15812. this.element.removeEventListener(gestures[t], handler, false);
  15813. }
  15814. return this;
  15815. },
  15816. /**
  15817. * trigger gesture event
  15818. * @param {String} gesture
  15819. * @param {Object} eventData
  15820. * @returns {Hammer.Instance}
  15821. */
  15822. trigger: function triggerEvent(gesture, eventData){
  15823. // create DOM event
  15824. var event = Hammer.DOCUMENT.createEvent('Event');
  15825. event.initEvent(gesture, true, true);
  15826. event.gesture = eventData;
  15827. // trigger on the target if it is in the instance element,
  15828. // this is for event delegation tricks
  15829. var element = this.element;
  15830. if(Hammer.utils.hasParent(eventData.target, element)) {
  15831. element = eventData.target;
  15832. }
  15833. element.dispatchEvent(event);
  15834. return this;
  15835. },
  15836. /**
  15837. * enable of disable hammer.js detection
  15838. * @param {Boolean} state
  15839. * @returns {Hammer.Instance}
  15840. */
  15841. enable: function enable(state) {
  15842. this.enabled = state;
  15843. return this;
  15844. }
  15845. };
  15846. /**
  15847. * this holds the last move event,
  15848. * used to fix empty touchend issue
  15849. * see the onTouch event for an explanation
  15850. * @type {Object}
  15851. */
  15852. var last_move_event = null;
  15853. /**
  15854. * when the mouse is hold down, this is true
  15855. * @type {Boolean}
  15856. */
  15857. var enable_detect = false;
  15858. /**
  15859. * when touch events have been fired, this is true
  15860. * @type {Boolean}
  15861. */
  15862. var touch_triggered = false;
  15863. Hammer.event = {
  15864. /**
  15865. * simple addEventListener
  15866. * @param {HTMLElement} element
  15867. * @param {String} type
  15868. * @param {Function} handler
  15869. */
  15870. bindDom: function(element, type, handler) {
  15871. var types = type.split(' ');
  15872. for(var t=0; t<types.length; t++) {
  15873. element.addEventListener(types[t], handler, false);
  15874. }
  15875. },
  15876. /**
  15877. * touch events with mouse fallback
  15878. * @param {HTMLElement} element
  15879. * @param {String} eventType like Hammer.EVENT_MOVE
  15880. * @param {Function} handler
  15881. */
  15882. onTouch: function onTouch(element, eventType, handler) {
  15883. var self = this;
  15884. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  15885. var sourceEventType = ev.type.toLowerCase();
  15886. // onmouseup, but when touchend has been fired we do nothing.
  15887. // this is for touchdevices which also fire a mouseup on touchend
  15888. if(sourceEventType.match(/mouse/) && touch_triggered) {
  15889. return;
  15890. }
  15891. // mousebutton must be down or a touch event
  15892. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  15893. sourceEventType.match(/pointerdown/) || // pointerevents touch
  15894. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  15895. ){
  15896. enable_detect = true;
  15897. }
  15898. // we are in a touch event, set the touch triggered bool to true,
  15899. // this for the conflicts that may occur on ios and android
  15900. if(sourceEventType.match(/touch|pointer/)) {
  15901. touch_triggered = true;
  15902. }
  15903. // count the total touches on the screen
  15904. var count_touches = 0;
  15905. // when touch has been triggered in this detection session
  15906. // and we are now handling a mouse event, we stop that to prevent conflicts
  15907. if(enable_detect) {
  15908. // update pointerevent
  15909. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  15910. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  15911. }
  15912. // touch
  15913. else if(sourceEventType.match(/touch/)) {
  15914. count_touches = ev.touches.length;
  15915. }
  15916. // mouse
  15917. else if(!touch_triggered) {
  15918. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  15919. }
  15920. // if we are in a end event, but when we remove one touch and
  15921. // we still have enough, set eventType to move
  15922. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  15923. eventType = Hammer.EVENT_MOVE;
  15924. }
  15925. // no touches, force the end event
  15926. else if(!count_touches) {
  15927. eventType = Hammer.EVENT_END;
  15928. }
  15929. // because touchend has no touches, and we often want to use these in our gestures,
  15930. // we send the last move event as our eventData in touchend
  15931. if(!count_touches && last_move_event !== null) {
  15932. ev = last_move_event;
  15933. }
  15934. // store the last move event
  15935. else {
  15936. last_move_event = ev;
  15937. }
  15938. // trigger the handler
  15939. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  15940. // remove pointerevent from list
  15941. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  15942. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  15943. }
  15944. }
  15945. //debug(sourceEventType +" "+ eventType);
  15946. // on the end we reset everything
  15947. if(!count_touches) {
  15948. last_move_event = null;
  15949. enable_detect = false;
  15950. touch_triggered = false;
  15951. Hammer.PointerEvent.reset();
  15952. }
  15953. });
  15954. },
  15955. /**
  15956. * we have different events for each device/browser
  15957. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  15958. */
  15959. determineEventTypes: function determineEventTypes() {
  15960. // determine the eventtype we want to set
  15961. var types;
  15962. // pointerEvents magic
  15963. if(Hammer.HAS_POINTEREVENTS) {
  15964. types = Hammer.PointerEvent.getEvents();
  15965. }
  15966. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  15967. else if(Hammer.NO_MOUSEEVENTS) {
  15968. types = [
  15969. 'touchstart',
  15970. 'touchmove',
  15971. 'touchend touchcancel'];
  15972. }
  15973. // for non pointer events browsers and mixed browsers,
  15974. // like chrome on windows8 touch laptop
  15975. else {
  15976. types = [
  15977. 'touchstart mousedown',
  15978. 'touchmove mousemove',
  15979. 'touchend touchcancel mouseup'];
  15980. }
  15981. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  15982. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  15983. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  15984. },
  15985. /**
  15986. * create touchlist depending on the event
  15987. * @param {Object} ev
  15988. * @param {String} eventType used by the fakemultitouch plugin
  15989. */
  15990. getTouchList: function getTouchList(ev/*, eventType*/) {
  15991. // get the fake pointerEvent touchlist
  15992. if(Hammer.HAS_POINTEREVENTS) {
  15993. return Hammer.PointerEvent.getTouchList();
  15994. }
  15995. // get the touchlist
  15996. else if(ev.touches) {
  15997. return ev.touches;
  15998. }
  15999. // make fake touchlist from mouse position
  16000. else {
  16001. return [{
  16002. identifier: 1,
  16003. pageX: ev.pageX,
  16004. pageY: ev.pageY,
  16005. target: ev.target
  16006. }];
  16007. }
  16008. },
  16009. /**
  16010. * collect event data for Hammer js
  16011. * @param {HTMLElement} element
  16012. * @param {String} eventType like Hammer.EVENT_MOVE
  16013. * @param {Object} eventData
  16014. */
  16015. collectEventData: function collectEventData(element, eventType, ev) {
  16016. var touches = this.getTouchList(ev, eventType);
  16017. // find out pointerType
  16018. var pointerType = Hammer.POINTER_TOUCH;
  16019. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  16020. pointerType = Hammer.POINTER_MOUSE;
  16021. }
  16022. return {
  16023. center : Hammer.utils.getCenter(touches),
  16024. timeStamp : new Date().getTime(),
  16025. target : ev.target,
  16026. touches : touches,
  16027. eventType : eventType,
  16028. pointerType : pointerType,
  16029. srcEvent : ev,
  16030. /**
  16031. * prevent the browser default actions
  16032. * mostly used to disable scrolling of the browser
  16033. */
  16034. preventDefault: function() {
  16035. if(this.srcEvent.preventManipulation) {
  16036. this.srcEvent.preventManipulation();
  16037. }
  16038. if(this.srcEvent.preventDefault) {
  16039. this.srcEvent.preventDefault();
  16040. }
  16041. },
  16042. /**
  16043. * stop bubbling the event up to its parents
  16044. */
  16045. stopPropagation: function() {
  16046. this.srcEvent.stopPropagation();
  16047. },
  16048. /**
  16049. * immediately stop gesture detection
  16050. * might be useful after a swipe was detected
  16051. * @return {*}
  16052. */
  16053. stopDetect: function() {
  16054. return Hammer.detection.stopDetect();
  16055. }
  16056. };
  16057. }
  16058. };
  16059. Hammer.PointerEvent = {
  16060. /**
  16061. * holds all pointers
  16062. * @type {Object}
  16063. */
  16064. pointers: {},
  16065. /**
  16066. * get a list of pointers
  16067. * @returns {Array} touchlist
  16068. */
  16069. getTouchList: function() {
  16070. var self = this;
  16071. var touchlist = [];
  16072. // we can use forEach since pointerEvents only is in IE10
  16073. Object.keys(self.pointers).sort().forEach(function(id) {
  16074. touchlist.push(self.pointers[id]);
  16075. });
  16076. return touchlist;
  16077. },
  16078. /**
  16079. * update the position of a pointer
  16080. * @param {String} type Hammer.EVENT_END
  16081. * @param {Object} pointerEvent
  16082. */
  16083. updatePointer: function(type, pointerEvent) {
  16084. if(type == Hammer.EVENT_END) {
  16085. this.pointers = {};
  16086. }
  16087. else {
  16088. pointerEvent.identifier = pointerEvent.pointerId;
  16089. this.pointers[pointerEvent.pointerId] = pointerEvent;
  16090. }
  16091. return Object.keys(this.pointers).length;
  16092. },
  16093. /**
  16094. * check if ev matches pointertype
  16095. * @param {String} pointerType Hammer.POINTER_MOUSE
  16096. * @param {PointerEvent} ev
  16097. */
  16098. matchType: function(pointerType, ev) {
  16099. if(!ev.pointerType) {
  16100. return false;
  16101. }
  16102. var types = {};
  16103. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  16104. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  16105. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  16106. return types[pointerType];
  16107. },
  16108. /**
  16109. * get events
  16110. */
  16111. getEvents: function() {
  16112. return [
  16113. 'pointerdown MSPointerDown',
  16114. 'pointermove MSPointerMove',
  16115. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  16116. ];
  16117. },
  16118. /**
  16119. * reset the list
  16120. */
  16121. reset: function() {
  16122. this.pointers = {};
  16123. }
  16124. };
  16125. Hammer.utils = {
  16126. /**
  16127. * extend method,
  16128. * also used for cloning when dest is an empty object
  16129. * @param {Object} dest
  16130. * @param {Object} src
  16131. * @parm {Boolean} merge do a merge
  16132. * @returns {Object} dest
  16133. */
  16134. extend: function extend(dest, src, merge) {
  16135. for (var key in src) {
  16136. if(dest[key] !== undefined && merge) {
  16137. continue;
  16138. }
  16139. dest[key] = src[key];
  16140. }
  16141. return dest;
  16142. },
  16143. /**
  16144. * find if a node is in the given parent
  16145. * used for event delegation tricks
  16146. * @param {HTMLElement} node
  16147. * @param {HTMLElement} parent
  16148. * @returns {boolean} has_parent
  16149. */
  16150. hasParent: function(node, parent) {
  16151. while(node){
  16152. if(node == parent) {
  16153. return true;
  16154. }
  16155. node = node.parentNode;
  16156. }
  16157. return false;
  16158. },
  16159. /**
  16160. * get the center of all the touches
  16161. * @param {Array} touches
  16162. * @returns {Object} center
  16163. */
  16164. getCenter: function getCenter(touches) {
  16165. var valuesX = [], valuesY = [];
  16166. for(var t= 0,len=touches.length; t<len; t++) {
  16167. valuesX.push(touches[t].pageX);
  16168. valuesY.push(touches[t].pageY);
  16169. }
  16170. return {
  16171. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  16172. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  16173. };
  16174. },
  16175. /**
  16176. * calculate the velocity between two points
  16177. * @param {Number} delta_time
  16178. * @param {Number} delta_x
  16179. * @param {Number} delta_y
  16180. * @returns {Object} velocity
  16181. */
  16182. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  16183. return {
  16184. x: Math.abs(delta_x / delta_time) || 0,
  16185. y: Math.abs(delta_y / delta_time) || 0
  16186. };
  16187. },
  16188. /**
  16189. * calculate the angle between two coordinates
  16190. * @param {Touch} touch1
  16191. * @param {Touch} touch2
  16192. * @returns {Number} angle
  16193. */
  16194. getAngle: function getAngle(touch1, touch2) {
  16195. var y = touch2.pageY - touch1.pageY,
  16196. x = touch2.pageX - touch1.pageX;
  16197. return Math.atan2(y, x) * 180 / Math.PI;
  16198. },
  16199. /**
  16200. * angle to direction define
  16201. * @param {Touch} touch1
  16202. * @param {Touch} touch2
  16203. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  16204. */
  16205. getDirection: function getDirection(touch1, touch2) {
  16206. var x = Math.abs(touch1.pageX - touch2.pageX),
  16207. y = Math.abs(touch1.pageY - touch2.pageY);
  16208. if(x >= y) {
  16209. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16210. }
  16211. else {
  16212. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16213. }
  16214. },
  16215. /**
  16216. * calculate the distance between two touches
  16217. * @param {Touch} touch1
  16218. * @param {Touch} touch2
  16219. * @returns {Number} distance
  16220. */
  16221. getDistance: function getDistance(touch1, touch2) {
  16222. var x = touch2.pageX - touch1.pageX,
  16223. y = touch2.pageY - touch1.pageY;
  16224. return Math.sqrt((x*x) + (y*y));
  16225. },
  16226. /**
  16227. * calculate the scale factor between two touchLists (fingers)
  16228. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  16229. * @param {Array} start
  16230. * @param {Array} end
  16231. * @returns {Number} scale
  16232. */
  16233. getScale: function getScale(start, end) {
  16234. // need two fingers...
  16235. if(start.length >= 2 && end.length >= 2) {
  16236. return this.getDistance(end[0], end[1]) /
  16237. this.getDistance(start[0], start[1]);
  16238. }
  16239. return 1;
  16240. },
  16241. /**
  16242. * calculate the rotation degrees between two touchLists (fingers)
  16243. * @param {Array} start
  16244. * @param {Array} end
  16245. * @returns {Number} rotation
  16246. */
  16247. getRotation: function getRotation(start, end) {
  16248. // need two fingers
  16249. if(start.length >= 2 && end.length >= 2) {
  16250. return this.getAngle(end[1], end[0]) -
  16251. this.getAngle(start[1], start[0]);
  16252. }
  16253. return 0;
  16254. },
  16255. /**
  16256. * boolean if the direction is vertical
  16257. * @param {String} direction
  16258. * @returns {Boolean} is_vertical
  16259. */
  16260. isVertical: function isVertical(direction) {
  16261. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  16262. },
  16263. /**
  16264. * stop browser default behavior with css props
  16265. * @param {HtmlElement} element
  16266. * @param {Object} css_props
  16267. */
  16268. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  16269. var prop,
  16270. vendors = ['webkit','khtml','moz','ms','o',''];
  16271. if(!css_props || !element.style) {
  16272. return;
  16273. }
  16274. // with css properties for modern browsers
  16275. for(var i = 0; i < vendors.length; i++) {
  16276. for(var p in css_props) {
  16277. if(css_props.hasOwnProperty(p)) {
  16278. prop = p;
  16279. // vender prefix at the property
  16280. if(vendors[i]) {
  16281. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  16282. }
  16283. // set the style
  16284. element.style[prop] = css_props[p];
  16285. }
  16286. }
  16287. }
  16288. // also the disable onselectstart
  16289. if(css_props.userSelect == 'none') {
  16290. element.onselectstart = function() {
  16291. return false;
  16292. };
  16293. }
  16294. }
  16295. };
  16296. Hammer.detection = {
  16297. // contains all registred Hammer.gestures in the correct order
  16298. gestures: [],
  16299. // data of the current Hammer.gesture detection session
  16300. current: null,
  16301. // the previous Hammer.gesture session data
  16302. // is a full clone of the previous gesture.current object
  16303. previous: null,
  16304. // when this becomes true, no gestures are fired
  16305. stopped: false,
  16306. /**
  16307. * start Hammer.gesture detection
  16308. * @param {Hammer.Instance} inst
  16309. * @param {Object} eventData
  16310. */
  16311. startDetect: function startDetect(inst, eventData) {
  16312. // already busy with a Hammer.gesture detection on an element
  16313. if(this.current) {
  16314. return;
  16315. }
  16316. this.stopped = false;
  16317. this.current = {
  16318. inst : inst, // reference to HammerInstance we're working for
  16319. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  16320. lastEvent : false, // last eventData
  16321. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  16322. };
  16323. this.detect(eventData);
  16324. },
  16325. /**
  16326. * Hammer.gesture detection
  16327. * @param {Object} eventData
  16328. * @param {Object} eventData
  16329. */
  16330. detect: function detect(eventData) {
  16331. if(!this.current || this.stopped) {
  16332. return;
  16333. }
  16334. // extend event data with calculations about scale, distance etc
  16335. eventData = this.extendEventData(eventData);
  16336. // instance options
  16337. var inst_options = this.current.inst.options;
  16338. // call Hammer.gesture handlers
  16339. for(var g=0,len=this.gestures.length; g<len; g++) {
  16340. var gesture = this.gestures[g];
  16341. // only when the instance options have enabled this gesture
  16342. if(!this.stopped && inst_options[gesture.name] !== false) {
  16343. // if a handler returns false, we stop with the detection
  16344. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  16345. this.stopDetect();
  16346. break;
  16347. }
  16348. }
  16349. }
  16350. // store as previous event event
  16351. if(this.current) {
  16352. this.current.lastEvent = eventData;
  16353. }
  16354. // endevent, but not the last touch, so dont stop
  16355. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  16356. this.stopDetect();
  16357. }
  16358. return eventData;
  16359. },
  16360. /**
  16361. * clear the Hammer.gesture vars
  16362. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  16363. * to stop other Hammer.gestures from being fired
  16364. */
  16365. stopDetect: function stopDetect() {
  16366. // clone current data to the store as the previous gesture
  16367. // used for the double tap gesture, since this is an other gesture detect session
  16368. this.previous = Hammer.utils.extend({}, this.current);
  16369. // reset the current
  16370. this.current = null;
  16371. // stopped!
  16372. this.stopped = true;
  16373. },
  16374. /**
  16375. * extend eventData for Hammer.gestures
  16376. * @param {Object} ev
  16377. * @returns {Object} ev
  16378. */
  16379. extendEventData: function extendEventData(ev) {
  16380. var startEv = this.current.startEvent;
  16381. // if the touches change, set the new touches over the startEvent touches
  16382. // this because touchevents don't have all the touches on touchstart, or the
  16383. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  16384. // but, sometimes it happens that both fingers are touching at the EXACT same time
  16385. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  16386. // extend 1 level deep to get the touchlist with the touch objects
  16387. startEv.touches = [];
  16388. for(var i=0,len=ev.touches.length; i<len; i++) {
  16389. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  16390. }
  16391. }
  16392. var delta_time = ev.timeStamp - startEv.timeStamp,
  16393. delta_x = ev.center.pageX - startEv.center.pageX,
  16394. delta_y = ev.center.pageY - startEv.center.pageY,
  16395. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  16396. Hammer.utils.extend(ev, {
  16397. deltaTime : delta_time,
  16398. deltaX : delta_x,
  16399. deltaY : delta_y,
  16400. velocityX : velocity.x,
  16401. velocityY : velocity.y,
  16402. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  16403. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  16404. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  16405. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  16406. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  16407. startEvent : startEv
  16408. });
  16409. return ev;
  16410. },
  16411. /**
  16412. * register new gesture
  16413. * @param {Object} gesture object, see gestures.js for documentation
  16414. * @returns {Array} gestures
  16415. */
  16416. register: function register(gesture) {
  16417. // add an enable gesture options if there is no given
  16418. var options = gesture.defaults || {};
  16419. if(options[gesture.name] === undefined) {
  16420. options[gesture.name] = true;
  16421. }
  16422. // extend Hammer default options with the Hammer.gesture options
  16423. Hammer.utils.extend(Hammer.defaults, options, true);
  16424. // set its index
  16425. gesture.index = gesture.index || 1000;
  16426. // add Hammer.gesture to the list
  16427. this.gestures.push(gesture);
  16428. // sort the list by index
  16429. this.gestures.sort(function(a, b) {
  16430. if (a.index < b.index) {
  16431. return -1;
  16432. }
  16433. if (a.index > b.index) {
  16434. return 1;
  16435. }
  16436. return 0;
  16437. });
  16438. return this.gestures;
  16439. }
  16440. };
  16441. Hammer.gestures = Hammer.gestures || {};
  16442. /**
  16443. * Custom gestures
  16444. * ==============================
  16445. *
  16446. * Gesture object
  16447. * --------------------
  16448. * The object structure of a gesture:
  16449. *
  16450. * { name: 'mygesture',
  16451. * index: 1337,
  16452. * defaults: {
  16453. * mygesture_option: true
  16454. * }
  16455. * handler: function(type, ev, inst) {
  16456. * // trigger gesture event
  16457. * inst.trigger(this.name, ev);
  16458. * }
  16459. * }
  16460. * @param {String} name
  16461. * this should be the name of the gesture, lowercase
  16462. * it is also being used to disable/enable the gesture per instance config.
  16463. *
  16464. * @param {Number} [index=1000]
  16465. * the index of the gesture, where it is going to be in the stack of gestures detection
  16466. * like when you build an gesture that depends on the drag gesture, it is a good
  16467. * idea to place it after the index of the drag gesture.
  16468. *
  16469. * @param {Object} [defaults={}]
  16470. * the default settings of the gesture. these are added to the instance settings,
  16471. * and can be overruled per instance. you can also add the name of the gesture,
  16472. * but this is also added by default (and set to true).
  16473. *
  16474. * @param {Function} handler
  16475. * this handles the gesture detection of your custom gesture and receives the
  16476. * following arguments:
  16477. *
  16478. * @param {Object} eventData
  16479. * event data containing the following properties:
  16480. * timeStamp {Number} time the event occurred
  16481. * target {HTMLElement} target element
  16482. * touches {Array} touches (fingers, pointers, mouse) on the screen
  16483. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  16484. * center {Object} center position of the touches. contains pageX and pageY
  16485. * deltaTime {Number} the total time of the touches in the screen
  16486. * deltaX {Number} the delta on x axis we haved moved
  16487. * deltaY {Number} the delta on y axis we haved moved
  16488. * velocityX {Number} the velocity on the x
  16489. * velocityY {Number} the velocity on y
  16490. * angle {Number} the angle we are moving
  16491. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  16492. * distance {Number} the distance we haved moved
  16493. * scale {Number} scaling of the touches, needs 2 touches
  16494. * rotation {Number} rotation of the touches, needs 2 touches *
  16495. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  16496. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  16497. * startEvent {Object} contains the same properties as above,
  16498. * but from the first touch. this is used to calculate
  16499. * distances, deltaTime, scaling etc
  16500. *
  16501. * @param {Hammer.Instance} inst
  16502. * the instance we are doing the detection for. you can get the options from
  16503. * the inst.options object and trigger the gesture event by calling inst.trigger
  16504. *
  16505. *
  16506. * Handle gestures
  16507. * --------------------
  16508. * inside the handler you can get/set Hammer.detection.current. This is the current
  16509. * detection session. It has the following properties
  16510. * @param {String} name
  16511. * contains the name of the gesture we have detected. it has not a real function,
  16512. * only to check in other gestures if something is detected.
  16513. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  16514. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  16515. *
  16516. * @readonly
  16517. * @param {Hammer.Instance} inst
  16518. * the instance we do the detection for
  16519. *
  16520. * @readonly
  16521. * @param {Object} startEvent
  16522. * contains the properties of the first gesture detection in this session.
  16523. * Used for calculations about timing, distance, etc.
  16524. *
  16525. * @readonly
  16526. * @param {Object} lastEvent
  16527. * contains all the properties of the last gesture detect in this session.
  16528. *
  16529. * after the gesture detection session has been completed (user has released the screen)
  16530. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  16531. * this is usefull for gestures like doubletap, where you need to know if the
  16532. * previous gesture was a tap
  16533. *
  16534. * options that have been set by the instance can be received by calling inst.options
  16535. *
  16536. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  16537. * The first param is the name of your gesture, the second the event argument
  16538. *
  16539. *
  16540. * Register gestures
  16541. * --------------------
  16542. * When an gesture is added to the Hammer.gestures object, it is auto registered
  16543. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  16544. * manually and pass your gesture object as a param
  16545. *
  16546. */
  16547. /**
  16548. * Hold
  16549. * Touch stays at the same place for x time
  16550. * @events hold
  16551. */
  16552. Hammer.gestures.Hold = {
  16553. name: 'hold',
  16554. index: 10,
  16555. defaults: {
  16556. hold_timeout : 500,
  16557. hold_threshold : 1
  16558. },
  16559. timer: null,
  16560. handler: function holdGesture(ev, inst) {
  16561. switch(ev.eventType) {
  16562. case Hammer.EVENT_START:
  16563. // clear any running timers
  16564. clearTimeout(this.timer);
  16565. // set the gesture so we can check in the timeout if it still is
  16566. Hammer.detection.current.name = this.name;
  16567. // set timer and if after the timeout it still is hold,
  16568. // we trigger the hold event
  16569. this.timer = setTimeout(function() {
  16570. if(Hammer.detection.current.name == 'hold') {
  16571. inst.trigger('hold', ev);
  16572. }
  16573. }, inst.options.hold_timeout);
  16574. break;
  16575. // when you move or end we clear the timer
  16576. case Hammer.EVENT_MOVE:
  16577. if(ev.distance > inst.options.hold_threshold) {
  16578. clearTimeout(this.timer);
  16579. }
  16580. break;
  16581. case Hammer.EVENT_END:
  16582. clearTimeout(this.timer);
  16583. break;
  16584. }
  16585. }
  16586. };
  16587. /**
  16588. * Tap/DoubleTap
  16589. * Quick touch at a place or double at the same place
  16590. * @events tap, doubletap
  16591. */
  16592. Hammer.gestures.Tap = {
  16593. name: 'tap',
  16594. index: 100,
  16595. defaults: {
  16596. tap_max_touchtime : 250,
  16597. tap_max_distance : 10,
  16598. tap_always : true,
  16599. doubletap_distance : 20,
  16600. doubletap_interval : 300
  16601. },
  16602. handler: function tapGesture(ev, inst) {
  16603. if(ev.eventType == Hammer.EVENT_END) {
  16604. // previous gesture, for the double tap since these are two different gesture detections
  16605. var prev = Hammer.detection.previous,
  16606. did_doubletap = false;
  16607. // when the touchtime is higher then the max touch time
  16608. // or when the moving distance is too much
  16609. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  16610. ev.distance > inst.options.tap_max_distance) {
  16611. return;
  16612. }
  16613. // check if double tap
  16614. if(prev && prev.name == 'tap' &&
  16615. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  16616. ev.distance < inst.options.doubletap_distance) {
  16617. inst.trigger('doubletap', ev);
  16618. did_doubletap = true;
  16619. }
  16620. // do a single tap
  16621. if(!did_doubletap || inst.options.tap_always) {
  16622. Hammer.detection.current.name = 'tap';
  16623. inst.trigger(Hammer.detection.current.name, ev);
  16624. }
  16625. }
  16626. }
  16627. };
  16628. /**
  16629. * Swipe
  16630. * triggers swipe events when the end velocity is above the threshold
  16631. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  16632. */
  16633. Hammer.gestures.Swipe = {
  16634. name: 'swipe',
  16635. index: 40,
  16636. defaults: {
  16637. // set 0 for unlimited, but this can conflict with transform
  16638. swipe_max_touches : 1,
  16639. swipe_velocity : 0.7
  16640. },
  16641. handler: function swipeGesture(ev, inst) {
  16642. if(ev.eventType == Hammer.EVENT_END) {
  16643. // max touches
  16644. if(inst.options.swipe_max_touches > 0 &&
  16645. ev.touches.length > inst.options.swipe_max_touches) {
  16646. return;
  16647. }
  16648. // when the distance we moved is too small we skip this gesture
  16649. // or we can be already in dragging
  16650. if(ev.velocityX > inst.options.swipe_velocity ||
  16651. ev.velocityY > inst.options.swipe_velocity) {
  16652. // trigger swipe events
  16653. inst.trigger(this.name, ev);
  16654. inst.trigger(this.name + ev.direction, ev);
  16655. }
  16656. }
  16657. }
  16658. };
  16659. /**
  16660. * Drag
  16661. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  16662. * moving left and right is a good practice. When all the drag events are blocking
  16663. * you disable scrolling on that area.
  16664. * @events drag, drapleft, dragright, dragup, dragdown
  16665. */
  16666. Hammer.gestures.Drag = {
  16667. name: 'drag',
  16668. index: 50,
  16669. defaults: {
  16670. drag_min_distance : 10,
  16671. // set 0 for unlimited, but this can conflict with transform
  16672. drag_max_touches : 1,
  16673. // prevent default browser behavior when dragging occurs
  16674. // be careful with it, it makes the element a blocking element
  16675. // when you are using the drag gesture, it is a good practice to set this true
  16676. drag_block_horizontal : false,
  16677. drag_block_vertical : false,
  16678. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  16679. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  16680. drag_lock_to_axis : false,
  16681. // drag lock only kicks in when distance > drag_lock_min_distance
  16682. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  16683. drag_lock_min_distance : 25
  16684. },
  16685. triggered: false,
  16686. handler: function dragGesture(ev, inst) {
  16687. // current gesture isnt drag, but dragged is true
  16688. // this means an other gesture is busy. now call dragend
  16689. if(Hammer.detection.current.name != this.name && this.triggered) {
  16690. inst.trigger(this.name +'end', ev);
  16691. this.triggered = false;
  16692. return;
  16693. }
  16694. // max touches
  16695. if(inst.options.drag_max_touches > 0 &&
  16696. ev.touches.length > inst.options.drag_max_touches) {
  16697. return;
  16698. }
  16699. switch(ev.eventType) {
  16700. case Hammer.EVENT_START:
  16701. this.triggered = false;
  16702. break;
  16703. case Hammer.EVENT_MOVE:
  16704. // when the distance we moved is too small we skip this gesture
  16705. // or we can be already in dragging
  16706. if(ev.distance < inst.options.drag_min_distance &&
  16707. Hammer.detection.current.name != this.name) {
  16708. return;
  16709. }
  16710. // we are dragging!
  16711. Hammer.detection.current.name = this.name;
  16712. // lock drag to axis?
  16713. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  16714. ev.drag_locked_to_axis = true;
  16715. }
  16716. var last_direction = Hammer.detection.current.lastEvent.direction;
  16717. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  16718. // keep direction on the axis that the drag gesture started on
  16719. if(Hammer.utils.isVertical(last_direction)) {
  16720. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16721. }
  16722. else {
  16723. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16724. }
  16725. }
  16726. // first time, trigger dragstart event
  16727. if(!this.triggered) {
  16728. inst.trigger(this.name +'start', ev);
  16729. this.triggered = true;
  16730. }
  16731. // trigger normal event
  16732. inst.trigger(this.name, ev);
  16733. // direction event, like dragdown
  16734. inst.trigger(this.name + ev.direction, ev);
  16735. // block the browser events
  16736. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  16737. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  16738. ev.preventDefault();
  16739. }
  16740. break;
  16741. case Hammer.EVENT_END:
  16742. // trigger dragend
  16743. if(this.triggered) {
  16744. inst.trigger(this.name +'end', ev);
  16745. }
  16746. this.triggered = false;
  16747. break;
  16748. }
  16749. }
  16750. };
  16751. /**
  16752. * Transform
  16753. * User want to scale or rotate with 2 fingers
  16754. * @events transform, pinch, pinchin, pinchout, rotate
  16755. */
  16756. Hammer.gestures.Transform = {
  16757. name: 'transform',
  16758. index: 45,
  16759. defaults: {
  16760. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  16761. transform_min_scale : 0.01,
  16762. // rotation in degrees
  16763. transform_min_rotation : 1,
  16764. // prevent default browser behavior when two touches are on the screen
  16765. // but it makes the element a blocking element
  16766. // when you are using the transform gesture, it is a good practice to set this true
  16767. transform_always_block : false
  16768. },
  16769. triggered: false,
  16770. handler: function transformGesture(ev, inst) {
  16771. // current gesture isnt drag, but dragged is true
  16772. // this means an other gesture is busy. now call dragend
  16773. if(Hammer.detection.current.name != this.name && this.triggered) {
  16774. inst.trigger(this.name +'end', ev);
  16775. this.triggered = false;
  16776. return;
  16777. }
  16778. // atleast multitouch
  16779. if(ev.touches.length < 2) {
  16780. return;
  16781. }
  16782. // prevent default when two fingers are on the screen
  16783. if(inst.options.transform_always_block) {
  16784. ev.preventDefault();
  16785. }
  16786. switch(ev.eventType) {
  16787. case Hammer.EVENT_START:
  16788. this.triggered = false;
  16789. break;
  16790. case Hammer.EVENT_MOVE:
  16791. var scale_threshold = Math.abs(1-ev.scale);
  16792. var rotation_threshold = Math.abs(ev.rotation);
  16793. // when the distance we moved is too small we skip this gesture
  16794. // or we can be already in dragging
  16795. if(scale_threshold < inst.options.transform_min_scale &&
  16796. rotation_threshold < inst.options.transform_min_rotation) {
  16797. return;
  16798. }
  16799. // we are transforming!
  16800. Hammer.detection.current.name = this.name;
  16801. // first time, trigger dragstart event
  16802. if(!this.triggered) {
  16803. inst.trigger(this.name +'start', ev);
  16804. this.triggered = true;
  16805. }
  16806. inst.trigger(this.name, ev); // basic transform event
  16807. // trigger rotate event
  16808. if(rotation_threshold > inst.options.transform_min_rotation) {
  16809. inst.trigger('rotate', ev);
  16810. }
  16811. // trigger pinch event
  16812. if(scale_threshold > inst.options.transform_min_scale) {
  16813. inst.trigger('pinch', ev);
  16814. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  16815. }
  16816. break;
  16817. case Hammer.EVENT_END:
  16818. // trigger dragend
  16819. if(this.triggered) {
  16820. inst.trigger(this.name +'end', ev);
  16821. }
  16822. this.triggered = false;
  16823. break;
  16824. }
  16825. }
  16826. };
  16827. /**
  16828. * Touch
  16829. * Called as first, tells the user has touched the screen
  16830. * @events touch
  16831. */
  16832. Hammer.gestures.Touch = {
  16833. name: 'touch',
  16834. index: -Infinity,
  16835. defaults: {
  16836. // call preventDefault at touchstart, and makes the element blocking by
  16837. // disabling the scrolling of the page, but it improves gestures like
  16838. // transforming and dragging.
  16839. // be careful with using this, it can be very annoying for users to be stuck
  16840. // on the page
  16841. prevent_default: false,
  16842. // disable mouse events, so only touch (or pen!) input triggers events
  16843. prevent_mouseevents: false
  16844. },
  16845. handler: function touchGesture(ev, inst) {
  16846. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  16847. ev.stopDetect();
  16848. return;
  16849. }
  16850. if(inst.options.prevent_default) {
  16851. ev.preventDefault();
  16852. }
  16853. if(ev.eventType == Hammer.EVENT_START) {
  16854. inst.trigger(this.name, ev);
  16855. }
  16856. }
  16857. };
  16858. /**
  16859. * Release
  16860. * Called as last, tells the user has released the screen
  16861. * @events release
  16862. */
  16863. Hammer.gestures.Release = {
  16864. name: 'release',
  16865. index: Infinity,
  16866. handler: function releaseGesture(ev, inst) {
  16867. if(ev.eventType == Hammer.EVENT_END) {
  16868. inst.trigger(this.name, ev);
  16869. }
  16870. }
  16871. };
  16872. // node export
  16873. if(typeof module === 'object' && typeof module.exports === 'object'){
  16874. module.exports = Hammer;
  16875. }
  16876. // just window export
  16877. else {
  16878. window.Hammer = Hammer;
  16879. // requireJS module definition
  16880. if(typeof window.define === 'function' && window.define.amd) {
  16881. window.define('hammer', [], function() {
  16882. return Hammer;
  16883. });
  16884. }
  16885. }
  16886. })(this);
  16887. },{}],4:[function(require,module,exports){
  16888. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  16889. //! version : 2.6.0
  16890. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  16891. //! license : MIT
  16892. //! momentjs.com
  16893. (function (undefined) {
  16894. /************************************
  16895. Constants
  16896. ************************************/
  16897. var moment,
  16898. VERSION = "2.6.0",
  16899. // the global-scope this is NOT the global object in Node.js
  16900. globalScope = typeof global !== 'undefined' ? global : this,
  16901. oldGlobalMoment,
  16902. round = Math.round,
  16903. i,
  16904. YEAR = 0,
  16905. MONTH = 1,
  16906. DATE = 2,
  16907. HOUR = 3,
  16908. MINUTE = 4,
  16909. SECOND = 5,
  16910. MILLISECOND = 6,
  16911. // internal storage for language config files
  16912. languages = {},
  16913. // moment internal properties
  16914. momentProperties = {
  16915. _isAMomentObject: null,
  16916. _i : null,
  16917. _f : null,
  16918. _l : null,
  16919. _strict : null,
  16920. _isUTC : null,
  16921. _offset : null, // optional. Combine with _isUTC
  16922. _pf : null,
  16923. _lang : null // optional
  16924. },
  16925. // check for nodeJS
  16926. hasModule = (typeof module !== 'undefined' && module.exports),
  16927. // ASP.NET json date format regex
  16928. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  16929. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  16930. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  16931. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  16932. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  16933. // format tokens
  16934. 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,
  16935. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  16936. // parsing token regexes
  16937. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  16938. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  16939. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  16940. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  16941. parseTokenDigits = /\d+/, // nonzero number of digits
  16942. 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.
  16943. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  16944. parseTokenT = /T/i, // T (ISO separator)
  16945. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  16946. parseTokenOrdinal = /\d{1,2}/,
  16947. //strict parsing regexes
  16948. parseTokenOneDigit = /\d/, // 0 - 9
  16949. parseTokenTwoDigits = /\d\d/, // 00 - 99
  16950. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  16951. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  16952. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  16953. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  16954. // iso 8601 regex
  16955. // 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)
  16956. 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)?)?$/,
  16957. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  16958. isoDates = [
  16959. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  16960. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  16961. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  16962. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  16963. ['YYYY-DDD', /\d{4}-\d{3}/]
  16964. ],
  16965. // iso time formats and regexes
  16966. isoTimes = [
  16967. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  16968. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  16969. ['HH:mm', /(T| )\d\d:\d\d/],
  16970. ['HH', /(T| )\d\d/]
  16971. ],
  16972. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  16973. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  16974. // getter and setter names
  16975. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  16976. unitMillisecondFactors = {
  16977. 'Milliseconds' : 1,
  16978. 'Seconds' : 1e3,
  16979. 'Minutes' : 6e4,
  16980. 'Hours' : 36e5,
  16981. 'Days' : 864e5,
  16982. 'Months' : 2592e6,
  16983. 'Years' : 31536e6
  16984. },
  16985. unitAliases = {
  16986. ms : 'millisecond',
  16987. s : 'second',
  16988. m : 'minute',
  16989. h : 'hour',
  16990. d : 'day',
  16991. D : 'date',
  16992. w : 'week',
  16993. W : 'isoWeek',
  16994. M : 'month',
  16995. Q : 'quarter',
  16996. y : 'year',
  16997. DDD : 'dayOfYear',
  16998. e : 'weekday',
  16999. E : 'isoWeekday',
  17000. gg: 'weekYear',
  17001. GG: 'isoWeekYear'
  17002. },
  17003. camelFunctions = {
  17004. dayofyear : 'dayOfYear',
  17005. isoweekday : 'isoWeekday',
  17006. isoweek : 'isoWeek',
  17007. weekyear : 'weekYear',
  17008. isoweekyear : 'isoWeekYear'
  17009. },
  17010. // format function strings
  17011. formatFunctions = {},
  17012. // tokens to ordinalize and pad
  17013. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  17014. paddedTokens = 'M D H h m s w W'.split(' '),
  17015. formatTokenFunctions = {
  17016. M : function () {
  17017. return this.month() + 1;
  17018. },
  17019. MMM : function (format) {
  17020. return this.lang().monthsShort(this, format);
  17021. },
  17022. MMMM : function (format) {
  17023. return this.lang().months(this, format);
  17024. },
  17025. D : function () {
  17026. return this.date();
  17027. },
  17028. DDD : function () {
  17029. return this.dayOfYear();
  17030. },
  17031. d : function () {
  17032. return this.day();
  17033. },
  17034. dd : function (format) {
  17035. return this.lang().weekdaysMin(this, format);
  17036. },
  17037. ddd : function (format) {
  17038. return this.lang().weekdaysShort(this, format);
  17039. },
  17040. dddd : function (format) {
  17041. return this.lang().weekdays(this, format);
  17042. },
  17043. w : function () {
  17044. return this.week();
  17045. },
  17046. W : function () {
  17047. return this.isoWeek();
  17048. },
  17049. YY : function () {
  17050. return leftZeroFill(this.year() % 100, 2);
  17051. },
  17052. YYYY : function () {
  17053. return leftZeroFill(this.year(), 4);
  17054. },
  17055. YYYYY : function () {
  17056. return leftZeroFill(this.year(), 5);
  17057. },
  17058. YYYYYY : function () {
  17059. var y = this.year(), sign = y >= 0 ? '+' : '-';
  17060. return sign + leftZeroFill(Math.abs(y), 6);
  17061. },
  17062. gg : function () {
  17063. return leftZeroFill(this.weekYear() % 100, 2);
  17064. },
  17065. gggg : function () {
  17066. return leftZeroFill(this.weekYear(), 4);
  17067. },
  17068. ggggg : function () {
  17069. return leftZeroFill(this.weekYear(), 5);
  17070. },
  17071. GG : function () {
  17072. return leftZeroFill(this.isoWeekYear() % 100, 2);
  17073. },
  17074. GGGG : function () {
  17075. return leftZeroFill(this.isoWeekYear(), 4);
  17076. },
  17077. GGGGG : function () {
  17078. return leftZeroFill(this.isoWeekYear(), 5);
  17079. },
  17080. e : function () {
  17081. return this.weekday();
  17082. },
  17083. E : function () {
  17084. return this.isoWeekday();
  17085. },
  17086. a : function () {
  17087. return this.lang().meridiem(this.hours(), this.minutes(), true);
  17088. },
  17089. A : function () {
  17090. return this.lang().meridiem(this.hours(), this.minutes(), false);
  17091. },
  17092. H : function () {
  17093. return this.hours();
  17094. },
  17095. h : function () {
  17096. return this.hours() % 12 || 12;
  17097. },
  17098. m : function () {
  17099. return this.minutes();
  17100. },
  17101. s : function () {
  17102. return this.seconds();
  17103. },
  17104. S : function () {
  17105. return toInt(this.milliseconds() / 100);
  17106. },
  17107. SS : function () {
  17108. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  17109. },
  17110. SSS : function () {
  17111. return leftZeroFill(this.milliseconds(), 3);
  17112. },
  17113. SSSS : function () {
  17114. return leftZeroFill(this.milliseconds(), 3);
  17115. },
  17116. Z : function () {
  17117. var a = -this.zone(),
  17118. b = "+";
  17119. if (a < 0) {
  17120. a = -a;
  17121. b = "-";
  17122. }
  17123. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  17124. },
  17125. ZZ : function () {
  17126. var a = -this.zone(),
  17127. b = "+";
  17128. if (a < 0) {
  17129. a = -a;
  17130. b = "-";
  17131. }
  17132. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  17133. },
  17134. z : function () {
  17135. return this.zoneAbbr();
  17136. },
  17137. zz : function () {
  17138. return this.zoneName();
  17139. },
  17140. X : function () {
  17141. return this.unix();
  17142. },
  17143. Q : function () {
  17144. return this.quarter();
  17145. }
  17146. },
  17147. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  17148. function defaultParsingFlags() {
  17149. // We need to deep clone this object, and es5 standard is not very
  17150. // helpful.
  17151. return {
  17152. empty : false,
  17153. unusedTokens : [],
  17154. unusedInput : [],
  17155. overflow : -2,
  17156. charsLeftOver : 0,
  17157. nullInput : false,
  17158. invalidMonth : null,
  17159. invalidFormat : false,
  17160. userInvalidated : false,
  17161. iso: false
  17162. };
  17163. }
  17164. function deprecate(msg, fn) {
  17165. var firstTime = true;
  17166. function printMsg() {
  17167. if (moment.suppressDeprecationWarnings === false &&
  17168. typeof console !== 'undefined' && console.warn) {
  17169. console.warn("Deprecation warning: " + msg);
  17170. }
  17171. }
  17172. return extend(function () {
  17173. if (firstTime) {
  17174. printMsg();
  17175. firstTime = false;
  17176. }
  17177. return fn.apply(this, arguments);
  17178. }, fn);
  17179. }
  17180. function padToken(func, count) {
  17181. return function (a) {
  17182. return leftZeroFill(func.call(this, a), count);
  17183. };
  17184. }
  17185. function ordinalizeToken(func, period) {
  17186. return function (a) {
  17187. return this.lang().ordinal(func.call(this, a), period);
  17188. };
  17189. }
  17190. while (ordinalizeTokens.length) {
  17191. i = ordinalizeTokens.pop();
  17192. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  17193. }
  17194. while (paddedTokens.length) {
  17195. i = paddedTokens.pop();
  17196. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  17197. }
  17198. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  17199. /************************************
  17200. Constructors
  17201. ************************************/
  17202. function Language() {
  17203. }
  17204. // Moment prototype object
  17205. function Moment(config) {
  17206. checkOverflow(config);
  17207. extend(this, config);
  17208. }
  17209. // Duration Constructor
  17210. function Duration(duration) {
  17211. var normalizedInput = normalizeObjectUnits(duration),
  17212. years = normalizedInput.year || 0,
  17213. quarters = normalizedInput.quarter || 0,
  17214. months = normalizedInput.month || 0,
  17215. weeks = normalizedInput.week || 0,
  17216. days = normalizedInput.day || 0,
  17217. hours = normalizedInput.hour || 0,
  17218. minutes = normalizedInput.minute || 0,
  17219. seconds = normalizedInput.second || 0,
  17220. milliseconds = normalizedInput.millisecond || 0;
  17221. // representation for dateAddRemove
  17222. this._milliseconds = +milliseconds +
  17223. seconds * 1e3 + // 1000
  17224. minutes * 6e4 + // 1000 * 60
  17225. hours * 36e5; // 1000 * 60 * 60
  17226. // Because of dateAddRemove treats 24 hours as different from a
  17227. // day when working around DST, we need to store them separately
  17228. this._days = +days +
  17229. weeks * 7;
  17230. // It is impossible translate months into days without knowing
  17231. // which months you are are talking about, so we have to store
  17232. // it separately.
  17233. this._months = +months +
  17234. quarters * 3 +
  17235. years * 12;
  17236. this._data = {};
  17237. this._bubble();
  17238. }
  17239. /************************************
  17240. Helpers
  17241. ************************************/
  17242. function extend(a, b) {
  17243. for (var i in b) {
  17244. if (b.hasOwnProperty(i)) {
  17245. a[i] = b[i];
  17246. }
  17247. }
  17248. if (b.hasOwnProperty("toString")) {
  17249. a.toString = b.toString;
  17250. }
  17251. if (b.hasOwnProperty("valueOf")) {
  17252. a.valueOf = b.valueOf;
  17253. }
  17254. return a;
  17255. }
  17256. function cloneMoment(m) {
  17257. var result = {}, i;
  17258. for (i in m) {
  17259. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  17260. result[i] = m[i];
  17261. }
  17262. }
  17263. return result;
  17264. }
  17265. function absRound(number) {
  17266. if (number < 0) {
  17267. return Math.ceil(number);
  17268. } else {
  17269. return Math.floor(number);
  17270. }
  17271. }
  17272. // left zero fill a number
  17273. // see http://jsperf.com/left-zero-filling for performance comparison
  17274. function leftZeroFill(number, targetLength, forceSign) {
  17275. var output = '' + Math.abs(number),
  17276. sign = number >= 0;
  17277. while (output.length < targetLength) {
  17278. output = '0' + output;
  17279. }
  17280. return (sign ? (forceSign ? '+' : '') : '-') + output;
  17281. }
  17282. // helper function for _.addTime and _.subtractTime
  17283. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  17284. var milliseconds = duration._milliseconds,
  17285. days = duration._days,
  17286. months = duration._months;
  17287. updateOffset = updateOffset == null ? true : updateOffset;
  17288. if (milliseconds) {
  17289. mom._d.setTime(+mom._d + milliseconds * isAdding);
  17290. }
  17291. if (days) {
  17292. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  17293. }
  17294. if (months) {
  17295. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  17296. }
  17297. if (updateOffset) {
  17298. moment.updateOffset(mom, days || months);
  17299. }
  17300. }
  17301. // check if is an array
  17302. function isArray(input) {
  17303. return Object.prototype.toString.call(input) === '[object Array]';
  17304. }
  17305. function isDate(input) {
  17306. return Object.prototype.toString.call(input) === '[object Date]' ||
  17307. input instanceof Date;
  17308. }
  17309. // compare two arrays, return the number of differences
  17310. function compareArrays(array1, array2, dontConvert) {
  17311. var len = Math.min(array1.length, array2.length),
  17312. lengthDiff = Math.abs(array1.length - array2.length),
  17313. diffs = 0,
  17314. i;
  17315. for (i = 0; i < len; i++) {
  17316. if ((dontConvert && array1[i] !== array2[i]) ||
  17317. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  17318. diffs++;
  17319. }
  17320. }
  17321. return diffs + lengthDiff;
  17322. }
  17323. function normalizeUnits(units) {
  17324. if (units) {
  17325. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  17326. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  17327. }
  17328. return units;
  17329. }
  17330. function normalizeObjectUnits(inputObject) {
  17331. var normalizedInput = {},
  17332. normalizedProp,
  17333. prop;
  17334. for (prop in inputObject) {
  17335. if (inputObject.hasOwnProperty(prop)) {
  17336. normalizedProp = normalizeUnits(prop);
  17337. if (normalizedProp) {
  17338. normalizedInput[normalizedProp] = inputObject[prop];
  17339. }
  17340. }
  17341. }
  17342. return normalizedInput;
  17343. }
  17344. function makeList(field) {
  17345. var count, setter;
  17346. if (field.indexOf('week') === 0) {
  17347. count = 7;
  17348. setter = 'day';
  17349. }
  17350. else if (field.indexOf('month') === 0) {
  17351. count = 12;
  17352. setter = 'month';
  17353. }
  17354. else {
  17355. return;
  17356. }
  17357. moment[field] = function (format, index) {
  17358. var i, getter,
  17359. method = moment.fn._lang[field],
  17360. results = [];
  17361. if (typeof format === 'number') {
  17362. index = format;
  17363. format = undefined;
  17364. }
  17365. getter = function (i) {
  17366. var m = moment().utc().set(setter, i);
  17367. return method.call(moment.fn._lang, m, format || '');
  17368. };
  17369. if (index != null) {
  17370. return getter(index);
  17371. }
  17372. else {
  17373. for (i = 0; i < count; i++) {
  17374. results.push(getter(i));
  17375. }
  17376. return results;
  17377. }
  17378. };
  17379. }
  17380. function toInt(argumentForCoercion) {
  17381. var coercedNumber = +argumentForCoercion,
  17382. value = 0;
  17383. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  17384. if (coercedNumber >= 0) {
  17385. value = Math.floor(coercedNumber);
  17386. } else {
  17387. value = Math.ceil(coercedNumber);
  17388. }
  17389. }
  17390. return value;
  17391. }
  17392. function daysInMonth(year, month) {
  17393. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  17394. }
  17395. function weeksInYear(year, dow, doy) {
  17396. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  17397. }
  17398. function daysInYear(year) {
  17399. return isLeapYear(year) ? 366 : 365;
  17400. }
  17401. function isLeapYear(year) {
  17402. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  17403. }
  17404. function checkOverflow(m) {
  17405. var overflow;
  17406. if (m._a && m._pf.overflow === -2) {
  17407. overflow =
  17408. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  17409. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  17410. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  17411. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  17412. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  17413. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  17414. -1;
  17415. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  17416. overflow = DATE;
  17417. }
  17418. m._pf.overflow = overflow;
  17419. }
  17420. }
  17421. function isValid(m) {
  17422. if (m._isValid == null) {
  17423. m._isValid = !isNaN(m._d.getTime()) &&
  17424. m._pf.overflow < 0 &&
  17425. !m._pf.empty &&
  17426. !m._pf.invalidMonth &&
  17427. !m._pf.nullInput &&
  17428. !m._pf.invalidFormat &&
  17429. !m._pf.userInvalidated;
  17430. if (m._strict) {
  17431. m._isValid = m._isValid &&
  17432. m._pf.charsLeftOver === 0 &&
  17433. m._pf.unusedTokens.length === 0;
  17434. }
  17435. }
  17436. return m._isValid;
  17437. }
  17438. function normalizeLanguage(key) {
  17439. return key ? key.toLowerCase().replace('_', '-') : key;
  17440. }
  17441. // Return a moment from input, that is local/utc/zone equivalent to model.
  17442. function makeAs(input, model) {
  17443. return model._isUTC ? moment(input).zone(model._offset || 0) :
  17444. moment(input).local();
  17445. }
  17446. /************************************
  17447. Languages
  17448. ************************************/
  17449. extend(Language.prototype, {
  17450. set : function (config) {
  17451. var prop, i;
  17452. for (i in config) {
  17453. prop = config[i];
  17454. if (typeof prop === 'function') {
  17455. this[i] = prop;
  17456. } else {
  17457. this['_' + i] = prop;
  17458. }
  17459. }
  17460. },
  17461. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  17462. months : function (m) {
  17463. return this._months[m.month()];
  17464. },
  17465. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  17466. monthsShort : function (m) {
  17467. return this._monthsShort[m.month()];
  17468. },
  17469. monthsParse : function (monthName) {
  17470. var i, mom, regex;
  17471. if (!this._monthsParse) {
  17472. this._monthsParse = [];
  17473. }
  17474. for (i = 0; i < 12; i++) {
  17475. // make the regex if we don't have it already
  17476. if (!this._monthsParse[i]) {
  17477. mom = moment.utc([2000, i]);
  17478. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  17479. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17480. }
  17481. // test the regex
  17482. if (this._monthsParse[i].test(monthName)) {
  17483. return i;
  17484. }
  17485. }
  17486. },
  17487. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  17488. weekdays : function (m) {
  17489. return this._weekdays[m.day()];
  17490. },
  17491. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  17492. weekdaysShort : function (m) {
  17493. return this._weekdaysShort[m.day()];
  17494. },
  17495. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  17496. weekdaysMin : function (m) {
  17497. return this._weekdaysMin[m.day()];
  17498. },
  17499. weekdaysParse : function (weekdayName) {
  17500. var i, mom, regex;
  17501. if (!this._weekdaysParse) {
  17502. this._weekdaysParse = [];
  17503. }
  17504. for (i = 0; i < 7; i++) {
  17505. // make the regex if we don't have it already
  17506. if (!this._weekdaysParse[i]) {
  17507. mom = moment([2000, 1]).day(i);
  17508. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  17509. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17510. }
  17511. // test the regex
  17512. if (this._weekdaysParse[i].test(weekdayName)) {
  17513. return i;
  17514. }
  17515. }
  17516. },
  17517. _longDateFormat : {
  17518. LT : "h:mm A",
  17519. L : "MM/DD/YYYY",
  17520. LL : "MMMM D YYYY",
  17521. LLL : "MMMM D YYYY LT",
  17522. LLLL : "dddd, MMMM D YYYY LT"
  17523. },
  17524. longDateFormat : function (key) {
  17525. var output = this._longDateFormat[key];
  17526. if (!output && this._longDateFormat[key.toUpperCase()]) {
  17527. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  17528. return val.slice(1);
  17529. });
  17530. this._longDateFormat[key] = output;
  17531. }
  17532. return output;
  17533. },
  17534. isPM : function (input) {
  17535. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  17536. // Using charAt should be more compatible.
  17537. return ((input + '').toLowerCase().charAt(0) === 'p');
  17538. },
  17539. _meridiemParse : /[ap]\.?m?\.?/i,
  17540. meridiem : function (hours, minutes, isLower) {
  17541. if (hours > 11) {
  17542. return isLower ? 'pm' : 'PM';
  17543. } else {
  17544. return isLower ? 'am' : 'AM';
  17545. }
  17546. },
  17547. _calendar : {
  17548. sameDay : '[Today at] LT',
  17549. nextDay : '[Tomorrow at] LT',
  17550. nextWeek : 'dddd [at] LT',
  17551. lastDay : '[Yesterday at] LT',
  17552. lastWeek : '[Last] dddd [at] LT',
  17553. sameElse : 'L'
  17554. },
  17555. calendar : function (key, mom) {
  17556. var output = this._calendar[key];
  17557. return typeof output === 'function' ? output.apply(mom) : output;
  17558. },
  17559. _relativeTime : {
  17560. future : "in %s",
  17561. past : "%s ago",
  17562. s : "a few seconds",
  17563. m : "a minute",
  17564. mm : "%d minutes",
  17565. h : "an hour",
  17566. hh : "%d hours",
  17567. d : "a day",
  17568. dd : "%d days",
  17569. M : "a month",
  17570. MM : "%d months",
  17571. y : "a year",
  17572. yy : "%d years"
  17573. },
  17574. relativeTime : function (number, withoutSuffix, string, isFuture) {
  17575. var output = this._relativeTime[string];
  17576. return (typeof output === 'function') ?
  17577. output(number, withoutSuffix, string, isFuture) :
  17578. output.replace(/%d/i, number);
  17579. },
  17580. pastFuture : function (diff, output) {
  17581. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  17582. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  17583. },
  17584. ordinal : function (number) {
  17585. return this._ordinal.replace("%d", number);
  17586. },
  17587. _ordinal : "%d",
  17588. preparse : function (string) {
  17589. return string;
  17590. },
  17591. postformat : function (string) {
  17592. return string;
  17593. },
  17594. week : function (mom) {
  17595. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  17596. },
  17597. _week : {
  17598. dow : 0, // Sunday is the first day of the week.
  17599. doy : 6 // The week that contains Jan 1st is the first week of the year.
  17600. },
  17601. _invalidDate: 'Invalid date',
  17602. invalidDate: function () {
  17603. return this._invalidDate;
  17604. }
  17605. });
  17606. // Loads a language definition into the `languages` cache. The function
  17607. // takes a key and optionally values. If not in the browser and no values
  17608. // are provided, it will load the language file module. As a convenience,
  17609. // this function also returns the language values.
  17610. function loadLang(key, values) {
  17611. values.abbr = key;
  17612. if (!languages[key]) {
  17613. languages[key] = new Language();
  17614. }
  17615. languages[key].set(values);
  17616. return languages[key];
  17617. }
  17618. // Remove a language from the `languages` cache. Mostly useful in tests.
  17619. function unloadLang(key) {
  17620. delete languages[key];
  17621. }
  17622. // Determines which language definition to use and returns it.
  17623. //
  17624. // With no parameters, it will return the global language. If you
  17625. // pass in a language key, such as 'en', it will return the
  17626. // definition for 'en', so long as 'en' has already been loaded using
  17627. // moment.lang.
  17628. function getLangDefinition(key) {
  17629. var i = 0, j, lang, next, split,
  17630. get = function (k) {
  17631. if (!languages[k] && hasModule) {
  17632. try {
  17633. require('./lang/' + k);
  17634. } catch (e) { }
  17635. }
  17636. return languages[k];
  17637. };
  17638. if (!key) {
  17639. return moment.fn._lang;
  17640. }
  17641. if (!isArray(key)) {
  17642. //short-circuit everything else
  17643. lang = get(key);
  17644. if (lang) {
  17645. return lang;
  17646. }
  17647. key = [key];
  17648. }
  17649. //pick the language from the array
  17650. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  17651. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  17652. while (i < key.length) {
  17653. split = normalizeLanguage(key[i]).split('-');
  17654. j = split.length;
  17655. next = normalizeLanguage(key[i + 1]);
  17656. next = next ? next.split('-') : null;
  17657. while (j > 0) {
  17658. lang = get(split.slice(0, j).join('-'));
  17659. if (lang) {
  17660. return lang;
  17661. }
  17662. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  17663. //the next array item is better than a shallower substring of this one
  17664. break;
  17665. }
  17666. j--;
  17667. }
  17668. i++;
  17669. }
  17670. return moment.fn._lang;
  17671. }
  17672. /************************************
  17673. Formatting
  17674. ************************************/
  17675. function removeFormattingTokens(input) {
  17676. if (input.match(/\[[\s\S]/)) {
  17677. return input.replace(/^\[|\]$/g, "");
  17678. }
  17679. return input.replace(/\\/g, "");
  17680. }
  17681. function makeFormatFunction(format) {
  17682. var array = format.match(formattingTokens), i, length;
  17683. for (i = 0, length = array.length; i < length; i++) {
  17684. if (formatTokenFunctions[array[i]]) {
  17685. array[i] = formatTokenFunctions[array[i]];
  17686. } else {
  17687. array[i] = removeFormattingTokens(array[i]);
  17688. }
  17689. }
  17690. return function (mom) {
  17691. var output = "";
  17692. for (i = 0; i < length; i++) {
  17693. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  17694. }
  17695. return output;
  17696. };
  17697. }
  17698. // format date using native date object
  17699. function formatMoment(m, format) {
  17700. if (!m.isValid()) {
  17701. return m.lang().invalidDate();
  17702. }
  17703. format = expandFormat(format, m.lang());
  17704. if (!formatFunctions[format]) {
  17705. formatFunctions[format] = makeFormatFunction(format);
  17706. }
  17707. return formatFunctions[format](m);
  17708. }
  17709. function expandFormat(format, lang) {
  17710. var i = 5;
  17711. function replaceLongDateFormatTokens(input) {
  17712. return lang.longDateFormat(input) || input;
  17713. }
  17714. localFormattingTokens.lastIndex = 0;
  17715. while (i >= 0 && localFormattingTokens.test(format)) {
  17716. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  17717. localFormattingTokens.lastIndex = 0;
  17718. i -= 1;
  17719. }
  17720. return format;
  17721. }
  17722. /************************************
  17723. Parsing
  17724. ************************************/
  17725. // get the regex to find the next token
  17726. function getParseRegexForToken(token, config) {
  17727. var a, strict = config._strict;
  17728. switch (token) {
  17729. case 'Q':
  17730. return parseTokenOneDigit;
  17731. case 'DDDD':
  17732. return parseTokenThreeDigits;
  17733. case 'YYYY':
  17734. case 'GGGG':
  17735. case 'gggg':
  17736. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  17737. case 'Y':
  17738. case 'G':
  17739. case 'g':
  17740. return parseTokenSignedNumber;
  17741. case 'YYYYYY':
  17742. case 'YYYYY':
  17743. case 'GGGGG':
  17744. case 'ggggg':
  17745. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  17746. case 'S':
  17747. if (strict) { return parseTokenOneDigit; }
  17748. /* falls through */
  17749. case 'SS':
  17750. if (strict) { return parseTokenTwoDigits; }
  17751. /* falls through */
  17752. case 'SSS':
  17753. if (strict) { return parseTokenThreeDigits; }
  17754. /* falls through */
  17755. case 'DDD':
  17756. return parseTokenOneToThreeDigits;
  17757. case 'MMM':
  17758. case 'MMMM':
  17759. case 'dd':
  17760. case 'ddd':
  17761. case 'dddd':
  17762. return parseTokenWord;
  17763. case 'a':
  17764. case 'A':
  17765. return getLangDefinition(config._l)._meridiemParse;
  17766. case 'X':
  17767. return parseTokenTimestampMs;
  17768. case 'Z':
  17769. case 'ZZ':
  17770. return parseTokenTimezone;
  17771. case 'T':
  17772. return parseTokenT;
  17773. case 'SSSS':
  17774. return parseTokenDigits;
  17775. case 'MM':
  17776. case 'DD':
  17777. case 'YY':
  17778. case 'GG':
  17779. case 'gg':
  17780. case 'HH':
  17781. case 'hh':
  17782. case 'mm':
  17783. case 'ss':
  17784. case 'ww':
  17785. case 'WW':
  17786. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  17787. case 'M':
  17788. case 'D':
  17789. case 'd':
  17790. case 'H':
  17791. case 'h':
  17792. case 'm':
  17793. case 's':
  17794. case 'w':
  17795. case 'W':
  17796. case 'e':
  17797. case 'E':
  17798. return parseTokenOneOrTwoDigits;
  17799. case 'Do':
  17800. return parseTokenOrdinal;
  17801. default :
  17802. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  17803. return a;
  17804. }
  17805. }
  17806. function timezoneMinutesFromString(string) {
  17807. string = string || "";
  17808. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  17809. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  17810. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  17811. minutes = +(parts[1] * 60) + toInt(parts[2]);
  17812. return parts[0] === '+' ? -minutes : minutes;
  17813. }
  17814. // function to convert string input to date
  17815. function addTimeToArrayFromToken(token, input, config) {
  17816. var a, datePartArray = config._a;
  17817. switch (token) {
  17818. // QUARTER
  17819. case 'Q':
  17820. if (input != null) {
  17821. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  17822. }
  17823. break;
  17824. // MONTH
  17825. case 'M' : // fall through to MM
  17826. case 'MM' :
  17827. if (input != null) {
  17828. datePartArray[MONTH] = toInt(input) - 1;
  17829. }
  17830. break;
  17831. case 'MMM' : // fall through to MMMM
  17832. case 'MMMM' :
  17833. a = getLangDefinition(config._l).monthsParse(input);
  17834. // if we didn't find a month name, mark the date as invalid.
  17835. if (a != null) {
  17836. datePartArray[MONTH] = a;
  17837. } else {
  17838. config._pf.invalidMonth = input;
  17839. }
  17840. break;
  17841. // DAY OF MONTH
  17842. case 'D' : // fall through to DD
  17843. case 'DD' :
  17844. if (input != null) {
  17845. datePartArray[DATE] = toInt(input);
  17846. }
  17847. break;
  17848. case 'Do' :
  17849. if (input != null) {
  17850. datePartArray[DATE] = toInt(parseInt(input, 10));
  17851. }
  17852. break;
  17853. // DAY OF YEAR
  17854. case 'DDD' : // fall through to DDDD
  17855. case 'DDDD' :
  17856. if (input != null) {
  17857. config._dayOfYear = toInt(input);
  17858. }
  17859. break;
  17860. // YEAR
  17861. case 'YY' :
  17862. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  17863. break;
  17864. case 'YYYY' :
  17865. case 'YYYYY' :
  17866. case 'YYYYYY' :
  17867. datePartArray[YEAR] = toInt(input);
  17868. break;
  17869. // AM / PM
  17870. case 'a' : // fall through to A
  17871. case 'A' :
  17872. config._isPm = getLangDefinition(config._l).isPM(input);
  17873. break;
  17874. // 24 HOUR
  17875. case 'H' : // fall through to hh
  17876. case 'HH' : // fall through to hh
  17877. case 'h' : // fall through to hh
  17878. case 'hh' :
  17879. datePartArray[HOUR] = toInt(input);
  17880. break;
  17881. // MINUTE
  17882. case 'm' : // fall through to mm
  17883. case 'mm' :
  17884. datePartArray[MINUTE] = toInt(input);
  17885. break;
  17886. // SECOND
  17887. case 's' : // fall through to ss
  17888. case 'ss' :
  17889. datePartArray[SECOND] = toInt(input);
  17890. break;
  17891. // MILLISECOND
  17892. case 'S' :
  17893. case 'SS' :
  17894. case 'SSS' :
  17895. case 'SSSS' :
  17896. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  17897. break;
  17898. // UNIX TIMESTAMP WITH MS
  17899. case 'X':
  17900. config._d = new Date(parseFloat(input) * 1000);
  17901. break;
  17902. // TIMEZONE
  17903. case 'Z' : // fall through to ZZ
  17904. case 'ZZ' :
  17905. config._useUTC = true;
  17906. config._tzm = timezoneMinutesFromString(input);
  17907. break;
  17908. case 'w':
  17909. case 'ww':
  17910. case 'W':
  17911. case 'WW':
  17912. case 'd':
  17913. case 'dd':
  17914. case 'ddd':
  17915. case 'dddd':
  17916. case 'e':
  17917. case 'E':
  17918. token = token.substr(0, 1);
  17919. /* falls through */
  17920. case 'gg':
  17921. case 'gggg':
  17922. case 'GG':
  17923. case 'GGGG':
  17924. case 'GGGGG':
  17925. token = token.substr(0, 2);
  17926. if (input) {
  17927. config._w = config._w || {};
  17928. config._w[token] = input;
  17929. }
  17930. break;
  17931. }
  17932. }
  17933. // convert an array to a date.
  17934. // the array should mirror the parameters below
  17935. // note: all values past the year are optional and will default to the lowest possible value.
  17936. // [year, month, day , hour, minute, second, millisecond]
  17937. function dateFromConfig(config) {
  17938. var i, date, input = [], currentDate,
  17939. yearToUse, fixYear, w, temp, lang, weekday, week;
  17940. if (config._d) {
  17941. return;
  17942. }
  17943. currentDate = currentDateArray(config);
  17944. //compute day of the year from weeks and weekdays
  17945. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  17946. fixYear = function (val) {
  17947. var intVal = parseInt(val, 10);
  17948. return val ?
  17949. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  17950. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  17951. };
  17952. w = config._w;
  17953. if (w.GG != null || w.W != null || w.E != null) {
  17954. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  17955. }
  17956. else {
  17957. lang = getLangDefinition(config._l);
  17958. weekday = w.d != null ? parseWeekday(w.d, lang) :
  17959. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  17960. week = parseInt(w.w, 10) || 1;
  17961. //if we're parsing 'd', then the low day numbers may be next week
  17962. if (w.d != null && weekday < lang._week.dow) {
  17963. week++;
  17964. }
  17965. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  17966. }
  17967. config._a[YEAR] = temp.year;
  17968. config._dayOfYear = temp.dayOfYear;
  17969. }
  17970. //if the day of the year is set, figure out what it is
  17971. if (config._dayOfYear) {
  17972. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  17973. if (config._dayOfYear > daysInYear(yearToUse)) {
  17974. config._pf._overflowDayOfYear = true;
  17975. }
  17976. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  17977. config._a[MONTH] = date.getUTCMonth();
  17978. config._a[DATE] = date.getUTCDate();
  17979. }
  17980. // Default to current date.
  17981. // * if no year, month, day of month are given, default to today
  17982. // * if day of month is given, default month and year
  17983. // * if month is given, default only year
  17984. // * if year is given, don't default anything
  17985. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  17986. config._a[i] = input[i] = currentDate[i];
  17987. }
  17988. // Zero out whatever was not defaulted, including time
  17989. for (; i < 7; i++) {
  17990. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  17991. }
  17992. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  17993. input[HOUR] += toInt((config._tzm || 0) / 60);
  17994. input[MINUTE] += toInt((config._tzm || 0) % 60);
  17995. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  17996. }
  17997. function dateFromObject(config) {
  17998. var normalizedInput;
  17999. if (config._d) {
  18000. return;
  18001. }
  18002. normalizedInput = normalizeObjectUnits(config._i);
  18003. config._a = [
  18004. normalizedInput.year,
  18005. normalizedInput.month,
  18006. normalizedInput.day,
  18007. normalizedInput.hour,
  18008. normalizedInput.minute,
  18009. normalizedInput.second,
  18010. normalizedInput.millisecond
  18011. ];
  18012. dateFromConfig(config);
  18013. }
  18014. function currentDateArray(config) {
  18015. var now = new Date();
  18016. if (config._useUTC) {
  18017. return [
  18018. now.getUTCFullYear(),
  18019. now.getUTCMonth(),
  18020. now.getUTCDate()
  18021. ];
  18022. } else {
  18023. return [now.getFullYear(), now.getMonth(), now.getDate()];
  18024. }
  18025. }
  18026. // date from string and format string
  18027. function makeDateFromStringAndFormat(config) {
  18028. config._a = [];
  18029. config._pf.empty = true;
  18030. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  18031. var lang = getLangDefinition(config._l),
  18032. string = '' + config._i,
  18033. i, parsedInput, tokens, token, skipped,
  18034. stringLength = string.length,
  18035. totalParsedInputLength = 0;
  18036. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  18037. for (i = 0; i < tokens.length; i++) {
  18038. token = tokens[i];
  18039. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  18040. if (parsedInput) {
  18041. skipped = string.substr(0, string.indexOf(parsedInput));
  18042. if (skipped.length > 0) {
  18043. config._pf.unusedInput.push(skipped);
  18044. }
  18045. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  18046. totalParsedInputLength += parsedInput.length;
  18047. }
  18048. // don't parse if it's not a known token
  18049. if (formatTokenFunctions[token]) {
  18050. if (parsedInput) {
  18051. config._pf.empty = false;
  18052. }
  18053. else {
  18054. config._pf.unusedTokens.push(token);
  18055. }
  18056. addTimeToArrayFromToken(token, parsedInput, config);
  18057. }
  18058. else if (config._strict && !parsedInput) {
  18059. config._pf.unusedTokens.push(token);
  18060. }
  18061. }
  18062. // add remaining unparsed input length to the string
  18063. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  18064. if (string.length > 0) {
  18065. config._pf.unusedInput.push(string);
  18066. }
  18067. // handle am pm
  18068. if (config._isPm && config._a[HOUR] < 12) {
  18069. config._a[HOUR] += 12;
  18070. }
  18071. // if is 12 am, change hours to 0
  18072. if (config._isPm === false && config._a[HOUR] === 12) {
  18073. config._a[HOUR] = 0;
  18074. }
  18075. dateFromConfig(config);
  18076. checkOverflow(config);
  18077. }
  18078. function unescapeFormat(s) {
  18079. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  18080. return p1 || p2 || p3 || p4;
  18081. });
  18082. }
  18083. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  18084. function regexpEscape(s) {
  18085. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  18086. }
  18087. // date from string and array of format strings
  18088. function makeDateFromStringAndArray(config) {
  18089. var tempConfig,
  18090. bestMoment,
  18091. scoreToBeat,
  18092. i,
  18093. currentScore;
  18094. if (config._f.length === 0) {
  18095. config._pf.invalidFormat = true;
  18096. config._d = new Date(NaN);
  18097. return;
  18098. }
  18099. for (i = 0; i < config._f.length; i++) {
  18100. currentScore = 0;
  18101. tempConfig = extend({}, config);
  18102. tempConfig._pf = defaultParsingFlags();
  18103. tempConfig._f = config._f[i];
  18104. makeDateFromStringAndFormat(tempConfig);
  18105. if (!isValid(tempConfig)) {
  18106. continue;
  18107. }
  18108. // if there is any input that was not parsed add a penalty for that format
  18109. currentScore += tempConfig._pf.charsLeftOver;
  18110. //or tokens
  18111. currentScore += tempConfig._pf.unusedTokens.length * 10;
  18112. tempConfig._pf.score = currentScore;
  18113. if (scoreToBeat == null || currentScore < scoreToBeat) {
  18114. scoreToBeat = currentScore;
  18115. bestMoment = tempConfig;
  18116. }
  18117. }
  18118. extend(config, bestMoment || tempConfig);
  18119. }
  18120. // date from iso format
  18121. function makeDateFromString(config) {
  18122. var i, l,
  18123. string = config._i,
  18124. match = isoRegex.exec(string);
  18125. if (match) {
  18126. config._pf.iso = true;
  18127. for (i = 0, l = isoDates.length; i < l; i++) {
  18128. if (isoDates[i][1].exec(string)) {
  18129. // match[5] should be "T" or undefined
  18130. config._f = isoDates[i][0] + (match[6] || " ");
  18131. break;
  18132. }
  18133. }
  18134. for (i = 0, l = isoTimes.length; i < l; i++) {
  18135. if (isoTimes[i][1].exec(string)) {
  18136. config._f += isoTimes[i][0];
  18137. break;
  18138. }
  18139. }
  18140. if (string.match(parseTokenTimezone)) {
  18141. config._f += "Z";
  18142. }
  18143. makeDateFromStringAndFormat(config);
  18144. }
  18145. else {
  18146. moment.createFromInputFallback(config);
  18147. }
  18148. }
  18149. function makeDateFromInput(config) {
  18150. var input = config._i,
  18151. matched = aspNetJsonRegex.exec(input);
  18152. if (input === undefined) {
  18153. config._d = new Date();
  18154. } else if (matched) {
  18155. config._d = new Date(+matched[1]);
  18156. } else if (typeof input === 'string') {
  18157. makeDateFromString(config);
  18158. } else if (isArray(input)) {
  18159. config._a = input.slice(0);
  18160. dateFromConfig(config);
  18161. } else if (isDate(input)) {
  18162. config._d = new Date(+input);
  18163. } else if (typeof(input) === 'object') {
  18164. dateFromObject(config);
  18165. } else if (typeof(input) === 'number') {
  18166. // from milliseconds
  18167. config._d = new Date(input);
  18168. } else {
  18169. moment.createFromInputFallback(config);
  18170. }
  18171. }
  18172. function makeDate(y, m, d, h, M, s, ms) {
  18173. //can't just apply() to create a date:
  18174. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  18175. var date = new Date(y, m, d, h, M, s, ms);
  18176. //the date constructor doesn't accept years < 1970
  18177. if (y < 1970) {
  18178. date.setFullYear(y);
  18179. }
  18180. return date;
  18181. }
  18182. function makeUTCDate(y) {
  18183. var date = new Date(Date.UTC.apply(null, arguments));
  18184. if (y < 1970) {
  18185. date.setUTCFullYear(y);
  18186. }
  18187. return date;
  18188. }
  18189. function parseWeekday(input, language) {
  18190. if (typeof input === 'string') {
  18191. if (!isNaN(input)) {
  18192. input = parseInt(input, 10);
  18193. }
  18194. else {
  18195. input = language.weekdaysParse(input);
  18196. if (typeof input !== 'number') {
  18197. return null;
  18198. }
  18199. }
  18200. }
  18201. return input;
  18202. }
  18203. /************************************
  18204. Relative Time
  18205. ************************************/
  18206. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  18207. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  18208. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  18209. }
  18210. function relativeTime(milliseconds, withoutSuffix, lang) {
  18211. var seconds = round(Math.abs(milliseconds) / 1000),
  18212. minutes = round(seconds / 60),
  18213. hours = round(minutes / 60),
  18214. days = round(hours / 24),
  18215. years = round(days / 365),
  18216. args = seconds < 45 && ['s', seconds] ||
  18217. minutes === 1 && ['m'] ||
  18218. minutes < 45 && ['mm', minutes] ||
  18219. hours === 1 && ['h'] ||
  18220. hours < 22 && ['hh', hours] ||
  18221. days === 1 && ['d'] ||
  18222. days <= 25 && ['dd', days] ||
  18223. days <= 45 && ['M'] ||
  18224. days < 345 && ['MM', round(days / 30)] ||
  18225. years === 1 && ['y'] || ['yy', years];
  18226. args[2] = withoutSuffix;
  18227. args[3] = milliseconds > 0;
  18228. args[4] = lang;
  18229. return substituteTimeAgo.apply({}, args);
  18230. }
  18231. /************************************
  18232. Week of Year
  18233. ************************************/
  18234. // firstDayOfWeek 0 = sun, 6 = sat
  18235. // the day of the week that starts the week
  18236. // (usually sunday or monday)
  18237. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  18238. // the first week is the week that contains the first
  18239. // of this day of the week
  18240. // (eg. ISO weeks use thursday (4))
  18241. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  18242. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  18243. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  18244. adjustedMoment;
  18245. if (daysToDayOfWeek > end) {
  18246. daysToDayOfWeek -= 7;
  18247. }
  18248. if (daysToDayOfWeek < end - 7) {
  18249. daysToDayOfWeek += 7;
  18250. }
  18251. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  18252. return {
  18253. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  18254. year: adjustedMoment.year()
  18255. };
  18256. }
  18257. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  18258. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  18259. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  18260. weekday = weekday != null ? weekday : firstDayOfWeek;
  18261. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  18262. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  18263. return {
  18264. year: dayOfYear > 0 ? year : year - 1,
  18265. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  18266. };
  18267. }
  18268. /************************************
  18269. Top Level Functions
  18270. ************************************/
  18271. function makeMoment(config) {
  18272. var input = config._i,
  18273. format = config._f;
  18274. if (input === null || (format === undefined && input === '')) {
  18275. return moment.invalid({nullInput: true});
  18276. }
  18277. if (typeof input === 'string') {
  18278. config._i = input = getLangDefinition().preparse(input);
  18279. }
  18280. if (moment.isMoment(input)) {
  18281. config = cloneMoment(input);
  18282. config._d = new Date(+input._d);
  18283. } else if (format) {
  18284. if (isArray(format)) {
  18285. makeDateFromStringAndArray(config);
  18286. } else {
  18287. makeDateFromStringAndFormat(config);
  18288. }
  18289. } else {
  18290. makeDateFromInput(config);
  18291. }
  18292. return new Moment(config);
  18293. }
  18294. moment = function (input, format, lang, strict) {
  18295. var c;
  18296. if (typeof(lang) === "boolean") {
  18297. strict = lang;
  18298. lang = undefined;
  18299. }
  18300. // object construction must be done this way.
  18301. // https://github.com/moment/moment/issues/1423
  18302. c = {};
  18303. c._isAMomentObject = true;
  18304. c._i = input;
  18305. c._f = format;
  18306. c._l = lang;
  18307. c._strict = strict;
  18308. c._isUTC = false;
  18309. c._pf = defaultParsingFlags();
  18310. return makeMoment(c);
  18311. };
  18312. moment.suppressDeprecationWarnings = false;
  18313. moment.createFromInputFallback = deprecate(
  18314. "moment construction falls back to js Date. This is " +
  18315. "discouraged and will be removed in upcoming major " +
  18316. "release. Please refer to " +
  18317. "https://github.com/moment/moment/issues/1407 for more info.",
  18318. function (config) {
  18319. config._d = new Date(config._i);
  18320. });
  18321. // creating with utc
  18322. moment.utc = function (input, format, lang, strict) {
  18323. var c;
  18324. if (typeof(lang) === "boolean") {
  18325. strict = lang;
  18326. lang = undefined;
  18327. }
  18328. // object construction must be done this way.
  18329. // https://github.com/moment/moment/issues/1423
  18330. c = {};
  18331. c._isAMomentObject = true;
  18332. c._useUTC = true;
  18333. c._isUTC = true;
  18334. c._l = lang;
  18335. c._i = input;
  18336. c._f = format;
  18337. c._strict = strict;
  18338. c._pf = defaultParsingFlags();
  18339. return makeMoment(c).utc();
  18340. };
  18341. // creating with unix timestamp (in seconds)
  18342. moment.unix = function (input) {
  18343. return moment(input * 1000);
  18344. };
  18345. // duration
  18346. moment.duration = function (input, key) {
  18347. var duration = input,
  18348. // matching against regexp is expensive, do it on demand
  18349. match = null,
  18350. sign,
  18351. ret,
  18352. parseIso;
  18353. if (moment.isDuration(input)) {
  18354. duration = {
  18355. ms: input._milliseconds,
  18356. d: input._days,
  18357. M: input._months
  18358. };
  18359. } else if (typeof input === 'number') {
  18360. duration = {};
  18361. if (key) {
  18362. duration[key] = input;
  18363. } else {
  18364. duration.milliseconds = input;
  18365. }
  18366. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  18367. sign = (match[1] === "-") ? -1 : 1;
  18368. duration = {
  18369. y: 0,
  18370. d: toInt(match[DATE]) * sign,
  18371. h: toInt(match[HOUR]) * sign,
  18372. m: toInt(match[MINUTE]) * sign,
  18373. s: toInt(match[SECOND]) * sign,
  18374. ms: toInt(match[MILLISECOND]) * sign
  18375. };
  18376. } else if (!!(match = isoDurationRegex.exec(input))) {
  18377. sign = (match[1] === "-") ? -1 : 1;
  18378. parseIso = function (inp) {
  18379. // We'd normally use ~~inp for this, but unfortunately it also
  18380. // converts floats to ints.
  18381. // inp may be undefined, so careful calling replace on it.
  18382. var res = inp && parseFloat(inp.replace(',', '.'));
  18383. // apply sign while we're at it
  18384. return (isNaN(res) ? 0 : res) * sign;
  18385. };
  18386. duration = {
  18387. y: parseIso(match[2]),
  18388. M: parseIso(match[3]),
  18389. d: parseIso(match[4]),
  18390. h: parseIso(match[5]),
  18391. m: parseIso(match[6]),
  18392. s: parseIso(match[7]),
  18393. w: parseIso(match[8])
  18394. };
  18395. }
  18396. ret = new Duration(duration);
  18397. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  18398. ret._lang = input._lang;
  18399. }
  18400. return ret;
  18401. };
  18402. // version number
  18403. moment.version = VERSION;
  18404. // default format
  18405. moment.defaultFormat = isoFormat;
  18406. // Plugins that add properties should also add the key here (null value),
  18407. // so we can properly clone ourselves.
  18408. moment.momentProperties = momentProperties;
  18409. // This function will be called whenever a moment is mutated.
  18410. // It is intended to keep the offset in sync with the timezone.
  18411. moment.updateOffset = function () {};
  18412. // This function will load languages and then set the global language. If
  18413. // no arguments are passed in, it will simply return the current global
  18414. // language key.
  18415. moment.lang = function (key, values) {
  18416. var r;
  18417. if (!key) {
  18418. return moment.fn._lang._abbr;
  18419. }
  18420. if (values) {
  18421. loadLang(normalizeLanguage(key), values);
  18422. } else if (values === null) {
  18423. unloadLang(key);
  18424. key = 'en';
  18425. } else if (!languages[key]) {
  18426. getLangDefinition(key);
  18427. }
  18428. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  18429. return r._abbr;
  18430. };
  18431. // returns language data
  18432. moment.langData = function (key) {
  18433. if (key && key._lang && key._lang._abbr) {
  18434. key = key._lang._abbr;
  18435. }
  18436. return getLangDefinition(key);
  18437. };
  18438. // compare moment object
  18439. moment.isMoment = function (obj) {
  18440. return obj instanceof Moment ||
  18441. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  18442. };
  18443. // for typechecking Duration objects
  18444. moment.isDuration = function (obj) {
  18445. return obj instanceof Duration;
  18446. };
  18447. for (i = lists.length - 1; i >= 0; --i) {
  18448. makeList(lists[i]);
  18449. }
  18450. moment.normalizeUnits = function (units) {
  18451. return normalizeUnits(units);
  18452. };
  18453. moment.invalid = function (flags) {
  18454. var m = moment.utc(NaN);
  18455. if (flags != null) {
  18456. extend(m._pf, flags);
  18457. }
  18458. else {
  18459. m._pf.userInvalidated = true;
  18460. }
  18461. return m;
  18462. };
  18463. moment.parseZone = function () {
  18464. return moment.apply(null, arguments).parseZone();
  18465. };
  18466. moment.parseTwoDigitYear = function (input) {
  18467. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  18468. };
  18469. /************************************
  18470. Moment Prototype
  18471. ************************************/
  18472. extend(moment.fn = Moment.prototype, {
  18473. clone : function () {
  18474. return moment(this);
  18475. },
  18476. valueOf : function () {
  18477. return +this._d + ((this._offset || 0) * 60000);
  18478. },
  18479. unix : function () {
  18480. return Math.floor(+this / 1000);
  18481. },
  18482. toString : function () {
  18483. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  18484. },
  18485. toDate : function () {
  18486. return this._offset ? new Date(+this) : this._d;
  18487. },
  18488. toISOString : function () {
  18489. var m = moment(this).utc();
  18490. if (0 < m.year() && m.year() <= 9999) {
  18491. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18492. } else {
  18493. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18494. }
  18495. },
  18496. toArray : function () {
  18497. var m = this;
  18498. return [
  18499. m.year(),
  18500. m.month(),
  18501. m.date(),
  18502. m.hours(),
  18503. m.minutes(),
  18504. m.seconds(),
  18505. m.milliseconds()
  18506. ];
  18507. },
  18508. isValid : function () {
  18509. return isValid(this);
  18510. },
  18511. isDSTShifted : function () {
  18512. if (this._a) {
  18513. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  18514. }
  18515. return false;
  18516. },
  18517. parsingFlags : function () {
  18518. return extend({}, this._pf);
  18519. },
  18520. invalidAt: function () {
  18521. return this._pf.overflow;
  18522. },
  18523. utc : function () {
  18524. return this.zone(0);
  18525. },
  18526. local : function () {
  18527. this.zone(0);
  18528. this._isUTC = false;
  18529. return this;
  18530. },
  18531. format : function (inputString) {
  18532. var output = formatMoment(this, inputString || moment.defaultFormat);
  18533. return this.lang().postformat(output);
  18534. },
  18535. add : function (input, val) {
  18536. var dur;
  18537. // switch args to support add('s', 1) and add(1, 's')
  18538. if (typeof input === 'string') {
  18539. dur = moment.duration(+val, input);
  18540. } else {
  18541. dur = moment.duration(input, val);
  18542. }
  18543. addOrSubtractDurationFromMoment(this, dur, 1);
  18544. return this;
  18545. },
  18546. subtract : function (input, val) {
  18547. var dur;
  18548. // switch args to support subtract('s', 1) and subtract(1, 's')
  18549. if (typeof input === 'string') {
  18550. dur = moment.duration(+val, input);
  18551. } else {
  18552. dur = moment.duration(input, val);
  18553. }
  18554. addOrSubtractDurationFromMoment(this, dur, -1);
  18555. return this;
  18556. },
  18557. diff : function (input, units, asFloat) {
  18558. var that = makeAs(input, this),
  18559. zoneDiff = (this.zone() - that.zone()) * 6e4,
  18560. diff, output;
  18561. units = normalizeUnits(units);
  18562. if (units === 'year' || units === 'month') {
  18563. // average number of days in the months in the given dates
  18564. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  18565. // difference in months
  18566. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  18567. // adjust by taking difference in days, average number of days
  18568. // and dst in the given months.
  18569. output += ((this - moment(this).startOf('month')) -
  18570. (that - moment(that).startOf('month'))) / diff;
  18571. // same as above but with zones, to negate all dst
  18572. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  18573. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  18574. if (units === 'year') {
  18575. output = output / 12;
  18576. }
  18577. } else {
  18578. diff = (this - that);
  18579. output = units === 'second' ? diff / 1e3 : // 1000
  18580. units === 'minute' ? diff / 6e4 : // 1000 * 60
  18581. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  18582. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  18583. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  18584. diff;
  18585. }
  18586. return asFloat ? output : absRound(output);
  18587. },
  18588. from : function (time, withoutSuffix) {
  18589. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  18590. },
  18591. fromNow : function (withoutSuffix) {
  18592. return this.from(moment(), withoutSuffix);
  18593. },
  18594. calendar : function () {
  18595. // We want to compare the start of today, vs this.
  18596. // Getting start-of-today depends on whether we're zone'd or not.
  18597. var sod = makeAs(moment(), this).startOf('day'),
  18598. diff = this.diff(sod, 'days', true),
  18599. format = diff < -6 ? 'sameElse' :
  18600. diff < -1 ? 'lastWeek' :
  18601. diff < 0 ? 'lastDay' :
  18602. diff < 1 ? 'sameDay' :
  18603. diff < 2 ? 'nextDay' :
  18604. diff < 7 ? 'nextWeek' : 'sameElse';
  18605. return this.format(this.lang().calendar(format, this));
  18606. },
  18607. isLeapYear : function () {
  18608. return isLeapYear(this.year());
  18609. },
  18610. isDST : function () {
  18611. return (this.zone() < this.clone().month(0).zone() ||
  18612. this.zone() < this.clone().month(5).zone());
  18613. },
  18614. day : function (input) {
  18615. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  18616. if (input != null) {
  18617. input = parseWeekday(input, this.lang());
  18618. return this.add({ d : input - day });
  18619. } else {
  18620. return day;
  18621. }
  18622. },
  18623. month : makeAccessor('Month', true),
  18624. startOf: function (units) {
  18625. units = normalizeUnits(units);
  18626. // the following switch intentionally omits break keywords
  18627. // to utilize falling through the cases.
  18628. switch (units) {
  18629. case 'year':
  18630. this.month(0);
  18631. /* falls through */
  18632. case 'quarter':
  18633. case 'month':
  18634. this.date(1);
  18635. /* falls through */
  18636. case 'week':
  18637. case 'isoWeek':
  18638. case 'day':
  18639. this.hours(0);
  18640. /* falls through */
  18641. case 'hour':
  18642. this.minutes(0);
  18643. /* falls through */
  18644. case 'minute':
  18645. this.seconds(0);
  18646. /* falls through */
  18647. case 'second':
  18648. this.milliseconds(0);
  18649. /* falls through */
  18650. }
  18651. // weeks are a special case
  18652. if (units === 'week') {
  18653. this.weekday(0);
  18654. } else if (units === 'isoWeek') {
  18655. this.isoWeekday(1);
  18656. }
  18657. // quarters are also special
  18658. if (units === 'quarter') {
  18659. this.month(Math.floor(this.month() / 3) * 3);
  18660. }
  18661. return this;
  18662. },
  18663. endOf: function (units) {
  18664. units = normalizeUnits(units);
  18665. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  18666. },
  18667. isAfter: function (input, units) {
  18668. units = typeof units !== 'undefined' ? units : 'millisecond';
  18669. return +this.clone().startOf(units) > +moment(input).startOf(units);
  18670. },
  18671. isBefore: function (input, units) {
  18672. units = typeof units !== 'undefined' ? units : 'millisecond';
  18673. return +this.clone().startOf(units) < +moment(input).startOf(units);
  18674. },
  18675. isSame: function (input, units) {
  18676. units = units || 'ms';
  18677. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  18678. },
  18679. min: function (other) {
  18680. other = moment.apply(null, arguments);
  18681. return other < this ? this : other;
  18682. },
  18683. max: function (other) {
  18684. other = moment.apply(null, arguments);
  18685. return other > this ? this : other;
  18686. },
  18687. // keepTime = true means only change the timezone, without affecting
  18688. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  18689. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  18690. // adjust the time as needed, to be valid.
  18691. //
  18692. // Keeping the time actually adds/subtracts (one hour)
  18693. // from the actual represented time. That is why we call updateOffset
  18694. // a second time. In case it wants us to change the offset again
  18695. // _changeInProgress == true case, then we have to adjust, because
  18696. // there is no such time in the given timezone.
  18697. zone : function (input, keepTime) {
  18698. var offset = this._offset || 0;
  18699. if (input != null) {
  18700. if (typeof input === "string") {
  18701. input = timezoneMinutesFromString(input);
  18702. }
  18703. if (Math.abs(input) < 16) {
  18704. input = input * 60;
  18705. }
  18706. this._offset = input;
  18707. this._isUTC = true;
  18708. if (offset !== input) {
  18709. if (!keepTime || this._changeInProgress) {
  18710. addOrSubtractDurationFromMoment(this,
  18711. moment.duration(offset - input, 'm'), 1, false);
  18712. } else if (!this._changeInProgress) {
  18713. this._changeInProgress = true;
  18714. moment.updateOffset(this, true);
  18715. this._changeInProgress = null;
  18716. }
  18717. }
  18718. } else {
  18719. return this._isUTC ? offset : this._d.getTimezoneOffset();
  18720. }
  18721. return this;
  18722. },
  18723. zoneAbbr : function () {
  18724. return this._isUTC ? "UTC" : "";
  18725. },
  18726. zoneName : function () {
  18727. return this._isUTC ? "Coordinated Universal Time" : "";
  18728. },
  18729. parseZone : function () {
  18730. if (this._tzm) {
  18731. this.zone(this._tzm);
  18732. } else if (typeof this._i === 'string') {
  18733. this.zone(this._i);
  18734. }
  18735. return this;
  18736. },
  18737. hasAlignedHourOffset : function (input) {
  18738. if (!input) {
  18739. input = 0;
  18740. }
  18741. else {
  18742. input = moment(input).zone();
  18743. }
  18744. return (this.zone() - input) % 60 === 0;
  18745. },
  18746. daysInMonth : function () {
  18747. return daysInMonth(this.year(), this.month());
  18748. },
  18749. dayOfYear : function (input) {
  18750. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  18751. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  18752. },
  18753. quarter : function (input) {
  18754. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  18755. },
  18756. weekYear : function (input) {
  18757. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  18758. return input == null ? year : this.add("y", (input - year));
  18759. },
  18760. isoWeekYear : function (input) {
  18761. var year = weekOfYear(this, 1, 4).year;
  18762. return input == null ? year : this.add("y", (input - year));
  18763. },
  18764. week : function (input) {
  18765. var week = this.lang().week(this);
  18766. return input == null ? week : this.add("d", (input - week) * 7);
  18767. },
  18768. isoWeek : function (input) {
  18769. var week = weekOfYear(this, 1, 4).week;
  18770. return input == null ? week : this.add("d", (input - week) * 7);
  18771. },
  18772. weekday : function (input) {
  18773. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  18774. return input == null ? weekday : this.add("d", input - weekday);
  18775. },
  18776. isoWeekday : function (input) {
  18777. // behaves the same as moment#day except
  18778. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  18779. // as a setter, sunday should belong to the previous week.
  18780. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  18781. },
  18782. isoWeeksInYear : function () {
  18783. return weeksInYear(this.year(), 1, 4);
  18784. },
  18785. weeksInYear : function () {
  18786. var weekInfo = this._lang._week;
  18787. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  18788. },
  18789. get : function (units) {
  18790. units = normalizeUnits(units);
  18791. return this[units]();
  18792. },
  18793. set : function (units, value) {
  18794. units = normalizeUnits(units);
  18795. if (typeof this[units] === 'function') {
  18796. this[units](value);
  18797. }
  18798. return this;
  18799. },
  18800. // If passed a language key, it will set the language for this
  18801. // instance. Otherwise, it will return the language configuration
  18802. // variables for this instance.
  18803. lang : function (key) {
  18804. if (key === undefined) {
  18805. return this._lang;
  18806. } else {
  18807. this._lang = getLangDefinition(key);
  18808. return this;
  18809. }
  18810. }
  18811. });
  18812. function rawMonthSetter(mom, value) {
  18813. var dayOfMonth;
  18814. // TODO: Move this out of here!
  18815. if (typeof value === 'string') {
  18816. value = mom.lang().monthsParse(value);
  18817. // TODO: Another silent failure?
  18818. if (typeof value !== 'number') {
  18819. return mom;
  18820. }
  18821. }
  18822. dayOfMonth = Math.min(mom.date(),
  18823. daysInMonth(mom.year(), value));
  18824. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  18825. return mom;
  18826. }
  18827. function rawGetter(mom, unit) {
  18828. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  18829. }
  18830. function rawSetter(mom, unit, value) {
  18831. if (unit === 'Month') {
  18832. return rawMonthSetter(mom, value);
  18833. } else {
  18834. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  18835. }
  18836. }
  18837. function makeAccessor(unit, keepTime) {
  18838. return function (value) {
  18839. if (value != null) {
  18840. rawSetter(this, unit, value);
  18841. moment.updateOffset(this, keepTime);
  18842. return this;
  18843. } else {
  18844. return rawGetter(this, unit);
  18845. }
  18846. };
  18847. }
  18848. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  18849. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  18850. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  18851. // Setting the hour should keep the time, because the user explicitly
  18852. // specified which hour he wants. So trying to maintain the same hour (in
  18853. // a new timezone) makes sense. Adding/subtracting hours does not follow
  18854. // this rule.
  18855. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  18856. // moment.fn.month is defined separately
  18857. moment.fn.date = makeAccessor('Date', true);
  18858. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  18859. moment.fn.year = makeAccessor('FullYear', true);
  18860. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  18861. // add plural methods
  18862. moment.fn.days = moment.fn.day;
  18863. moment.fn.months = moment.fn.month;
  18864. moment.fn.weeks = moment.fn.week;
  18865. moment.fn.isoWeeks = moment.fn.isoWeek;
  18866. moment.fn.quarters = moment.fn.quarter;
  18867. // add aliased format methods
  18868. moment.fn.toJSON = moment.fn.toISOString;
  18869. /************************************
  18870. Duration Prototype
  18871. ************************************/
  18872. extend(moment.duration.fn = Duration.prototype, {
  18873. _bubble : function () {
  18874. var milliseconds = this._milliseconds,
  18875. days = this._days,
  18876. months = this._months,
  18877. data = this._data,
  18878. seconds, minutes, hours, years;
  18879. // The following code bubbles up values, see the tests for
  18880. // examples of what that means.
  18881. data.milliseconds = milliseconds % 1000;
  18882. seconds = absRound(milliseconds / 1000);
  18883. data.seconds = seconds % 60;
  18884. minutes = absRound(seconds / 60);
  18885. data.minutes = minutes % 60;
  18886. hours = absRound(minutes / 60);
  18887. data.hours = hours % 24;
  18888. days += absRound(hours / 24);
  18889. data.days = days % 30;
  18890. months += absRound(days / 30);
  18891. data.months = months % 12;
  18892. years = absRound(months / 12);
  18893. data.years = years;
  18894. },
  18895. weeks : function () {
  18896. return absRound(this.days() / 7);
  18897. },
  18898. valueOf : function () {
  18899. return this._milliseconds +
  18900. this._days * 864e5 +
  18901. (this._months % 12) * 2592e6 +
  18902. toInt(this._months / 12) * 31536e6;
  18903. },
  18904. humanize : function (withSuffix) {
  18905. var difference = +this,
  18906. output = relativeTime(difference, !withSuffix, this.lang());
  18907. if (withSuffix) {
  18908. output = this.lang().pastFuture(difference, output);
  18909. }
  18910. return this.lang().postformat(output);
  18911. },
  18912. add : function (input, val) {
  18913. // supports only 2.0-style add(1, 's') or add(moment)
  18914. var dur = moment.duration(input, val);
  18915. this._milliseconds += dur._milliseconds;
  18916. this._days += dur._days;
  18917. this._months += dur._months;
  18918. this._bubble();
  18919. return this;
  18920. },
  18921. subtract : function (input, val) {
  18922. var dur = moment.duration(input, val);
  18923. this._milliseconds -= dur._milliseconds;
  18924. this._days -= dur._days;
  18925. this._months -= dur._months;
  18926. this._bubble();
  18927. return this;
  18928. },
  18929. get : function (units) {
  18930. units = normalizeUnits(units);
  18931. return this[units.toLowerCase() + 's']();
  18932. },
  18933. as : function (units) {
  18934. units = normalizeUnits(units);
  18935. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  18936. },
  18937. lang : moment.fn.lang,
  18938. toIsoString : function () {
  18939. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  18940. var years = Math.abs(this.years()),
  18941. months = Math.abs(this.months()),
  18942. days = Math.abs(this.days()),
  18943. hours = Math.abs(this.hours()),
  18944. minutes = Math.abs(this.minutes()),
  18945. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  18946. if (!this.asSeconds()) {
  18947. // this is the same as C#'s (Noda) and python (isodate)...
  18948. // but not other JS (goog.date)
  18949. return 'P0D';
  18950. }
  18951. return (this.asSeconds() < 0 ? '-' : '') +
  18952. 'P' +
  18953. (years ? years + 'Y' : '') +
  18954. (months ? months + 'M' : '') +
  18955. (days ? days + 'D' : '') +
  18956. ((hours || minutes || seconds) ? 'T' : '') +
  18957. (hours ? hours + 'H' : '') +
  18958. (minutes ? minutes + 'M' : '') +
  18959. (seconds ? seconds + 'S' : '');
  18960. }
  18961. });
  18962. function makeDurationGetter(name) {
  18963. moment.duration.fn[name] = function () {
  18964. return this._data[name];
  18965. };
  18966. }
  18967. function makeDurationAsGetter(name, factor) {
  18968. moment.duration.fn['as' + name] = function () {
  18969. return +this / factor;
  18970. };
  18971. }
  18972. for (i in unitMillisecondFactors) {
  18973. if (unitMillisecondFactors.hasOwnProperty(i)) {
  18974. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  18975. makeDurationGetter(i.toLowerCase());
  18976. }
  18977. }
  18978. makeDurationAsGetter('Weeks', 6048e5);
  18979. moment.duration.fn.asMonths = function () {
  18980. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  18981. };
  18982. /************************************
  18983. Default Lang
  18984. ************************************/
  18985. // Set default language, other languages will inherit from English.
  18986. moment.lang('en', {
  18987. ordinal : function (number) {
  18988. var b = number % 10,
  18989. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  18990. (b === 1) ? 'st' :
  18991. (b === 2) ? 'nd' :
  18992. (b === 3) ? 'rd' : 'th';
  18993. return number + output;
  18994. }
  18995. });
  18996. /* EMBED_LANGUAGES */
  18997. /************************************
  18998. Exposing Moment
  18999. ************************************/
  19000. function makeGlobal(shouldDeprecate) {
  19001. /*global ender:false */
  19002. if (typeof ender !== 'undefined') {
  19003. return;
  19004. }
  19005. oldGlobalMoment = globalScope.moment;
  19006. if (shouldDeprecate) {
  19007. globalScope.moment = deprecate(
  19008. "Accessing Moment through the global scope is " +
  19009. "deprecated, and will be removed in an upcoming " +
  19010. "release.",
  19011. moment);
  19012. } else {
  19013. globalScope.moment = moment;
  19014. }
  19015. }
  19016. // CommonJS module is defined
  19017. if (hasModule) {
  19018. module.exports = moment;
  19019. } else if (typeof define === "function" && define.amd) {
  19020. define("moment", function (require, exports, module) {
  19021. if (module.config && module.config() && module.config().noGlobal === true) {
  19022. // release the global variable
  19023. globalScope.moment = oldGlobalMoment;
  19024. }
  19025. return moment;
  19026. });
  19027. makeGlobal(true);
  19028. } else {
  19029. makeGlobal();
  19030. }
  19031. }).call(this);
  19032. },{}],5:[function(require,module,exports){
  19033. /**
  19034. * Copyright 2012 Craig Campbell
  19035. *
  19036. * Licensed under the Apache License, Version 2.0 (the "License");
  19037. * you may not use this file except in compliance with the License.
  19038. * You may obtain a copy of the License at
  19039. *
  19040. * http://www.apache.org/licenses/LICENSE-2.0
  19041. *
  19042. * Unless required by applicable law or agreed to in writing, software
  19043. * distributed under the License is distributed on an "AS IS" BASIS,
  19044. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19045. * See the License for the specific language governing permissions and
  19046. * limitations under the License.
  19047. *
  19048. * Mousetrap is a simple keyboard shortcut library for Javascript with
  19049. * no external dependencies
  19050. *
  19051. * @version 1.1.2
  19052. * @url craig.is/killing/mice
  19053. */
  19054. /**
  19055. * mapping of special keycodes to their corresponding keys
  19056. *
  19057. * everything in this dictionary cannot use keypress events
  19058. * so it has to be here to map to the correct keycodes for
  19059. * keyup/keydown events
  19060. *
  19061. * @type {Object}
  19062. */
  19063. var _MAP = {
  19064. 8: 'backspace',
  19065. 9: 'tab',
  19066. 13: 'enter',
  19067. 16: 'shift',
  19068. 17: 'ctrl',
  19069. 18: 'alt',
  19070. 20: 'capslock',
  19071. 27: 'esc',
  19072. 32: 'space',
  19073. 33: 'pageup',
  19074. 34: 'pagedown',
  19075. 35: 'end',
  19076. 36: 'home',
  19077. 37: 'left',
  19078. 38: 'up',
  19079. 39: 'right',
  19080. 40: 'down',
  19081. 45: 'ins',
  19082. 46: 'del',
  19083. 91: 'meta',
  19084. 93: 'meta',
  19085. 224: 'meta'
  19086. },
  19087. /**
  19088. * mapping for special characters so they can support
  19089. *
  19090. * this dictionary is only used incase you want to bind a
  19091. * keyup or keydown event to one of these keys
  19092. *
  19093. * @type {Object}
  19094. */
  19095. _KEYCODE_MAP = {
  19096. 106: '*',
  19097. 107: '+',
  19098. 109: '-',
  19099. 110: '.',
  19100. 111 : '/',
  19101. 186: ';',
  19102. 187: '=',
  19103. 188: ',',
  19104. 189: '-',
  19105. 190: '.',
  19106. 191: '/',
  19107. 192: '`',
  19108. 219: '[',
  19109. 220: '\\',
  19110. 221: ']',
  19111. 222: '\''
  19112. },
  19113. /**
  19114. * this is a mapping of keys that require shift on a US keypad
  19115. * back to the non shift equivelents
  19116. *
  19117. * this is so you can use keyup events with these keys
  19118. *
  19119. * note that this will only work reliably on US keyboards
  19120. *
  19121. * @type {Object}
  19122. */
  19123. _SHIFT_MAP = {
  19124. '~': '`',
  19125. '!': '1',
  19126. '@': '2',
  19127. '#': '3',
  19128. '$': '4',
  19129. '%': '5',
  19130. '^': '6',
  19131. '&': '7',
  19132. '*': '8',
  19133. '(': '9',
  19134. ')': '0',
  19135. '_': '-',
  19136. '+': '=',
  19137. ':': ';',
  19138. '\"': '\'',
  19139. '<': ',',
  19140. '>': '.',
  19141. '?': '/',
  19142. '|': '\\'
  19143. },
  19144. /**
  19145. * this is a list of special strings you can use to map
  19146. * to modifier keys when you specify your keyboard shortcuts
  19147. *
  19148. * @type {Object}
  19149. */
  19150. _SPECIAL_ALIASES = {
  19151. 'option': 'alt',
  19152. 'command': 'meta',
  19153. 'return': 'enter',
  19154. 'escape': 'esc'
  19155. },
  19156. /**
  19157. * variable to store the flipped version of _MAP from above
  19158. * needed to check if we should use keypress or not when no action
  19159. * is specified
  19160. *
  19161. * @type {Object|undefined}
  19162. */
  19163. _REVERSE_MAP,
  19164. /**
  19165. * a list of all the callbacks setup via Mousetrap.bind()
  19166. *
  19167. * @type {Object}
  19168. */
  19169. _callbacks = {},
  19170. /**
  19171. * direct map of string combinations to callbacks used for trigger()
  19172. *
  19173. * @type {Object}
  19174. */
  19175. _direct_map = {},
  19176. /**
  19177. * keeps track of what level each sequence is at since multiple
  19178. * sequences can start out with the same sequence
  19179. *
  19180. * @type {Object}
  19181. */
  19182. _sequence_levels = {},
  19183. /**
  19184. * variable to store the setTimeout call
  19185. *
  19186. * @type {null|number}
  19187. */
  19188. _reset_timer,
  19189. /**
  19190. * temporary state where we will ignore the next keyup
  19191. *
  19192. * @type {boolean|string}
  19193. */
  19194. _ignore_next_keyup = false,
  19195. /**
  19196. * are we currently inside of a sequence?
  19197. * type of action ("keyup" or "keydown" or "keypress") or false
  19198. *
  19199. * @type {boolean|string}
  19200. */
  19201. _inside_sequence = false;
  19202. /**
  19203. * loop through the f keys, f1 to f19 and add them to the map
  19204. * programatically
  19205. */
  19206. for (var i = 1; i < 20; ++i) {
  19207. _MAP[111 + i] = 'f' + i;
  19208. }
  19209. /**
  19210. * loop through to map numbers on the numeric keypad
  19211. */
  19212. for (i = 0; i <= 9; ++i) {
  19213. _MAP[i + 96] = i;
  19214. }
  19215. /**
  19216. * cross browser add event method
  19217. *
  19218. * @param {Element|HTMLDocument} object
  19219. * @param {string} type
  19220. * @param {Function} callback
  19221. * @returns void
  19222. */
  19223. function _addEvent(object, type, callback) {
  19224. if (object.addEventListener) {
  19225. return object.addEventListener(type, callback, false);
  19226. }
  19227. object.attachEvent('on' + type, callback);
  19228. }
  19229. /**
  19230. * takes the event and returns the key character
  19231. *
  19232. * @param {Event} e
  19233. * @return {string}
  19234. */
  19235. function _characterFromEvent(e) {
  19236. // for keypress events we should return the character as is
  19237. if (e.type == 'keypress') {
  19238. return String.fromCharCode(e.which);
  19239. }
  19240. // for non keypress events the special maps are needed
  19241. if (_MAP[e.which]) {
  19242. return _MAP[e.which];
  19243. }
  19244. if (_KEYCODE_MAP[e.which]) {
  19245. return _KEYCODE_MAP[e.which];
  19246. }
  19247. // if it is not in the special map
  19248. return String.fromCharCode(e.which).toLowerCase();
  19249. }
  19250. /**
  19251. * should we stop this event before firing off callbacks
  19252. *
  19253. * @param {Event} e
  19254. * @return {boolean}
  19255. */
  19256. function _stop(e) {
  19257. var element = e.target || e.srcElement,
  19258. tag_name = element.tagName;
  19259. // if the element has the class "mousetrap" then no need to stop
  19260. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  19261. return false;
  19262. }
  19263. // stop for input, select, and textarea
  19264. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  19265. }
  19266. /**
  19267. * checks if two arrays are equal
  19268. *
  19269. * @param {Array} modifiers1
  19270. * @param {Array} modifiers2
  19271. * @returns {boolean}
  19272. */
  19273. function _modifiersMatch(modifiers1, modifiers2) {
  19274. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  19275. }
  19276. /**
  19277. * resets all sequence counters except for the ones passed in
  19278. *
  19279. * @param {Object} do_not_reset
  19280. * @returns void
  19281. */
  19282. function _resetSequences(do_not_reset) {
  19283. do_not_reset = do_not_reset || {};
  19284. var active_sequences = false,
  19285. key;
  19286. for (key in _sequence_levels) {
  19287. if (do_not_reset[key]) {
  19288. active_sequences = true;
  19289. continue;
  19290. }
  19291. _sequence_levels[key] = 0;
  19292. }
  19293. if (!active_sequences) {
  19294. _inside_sequence = false;
  19295. }
  19296. }
  19297. /**
  19298. * finds all callbacks that match based on the keycode, modifiers,
  19299. * and action
  19300. *
  19301. * @param {string} character
  19302. * @param {Array} modifiers
  19303. * @param {string} action
  19304. * @param {boolean=} remove - should we remove any matches
  19305. * @param {string=} combination
  19306. * @returns {Array}
  19307. */
  19308. function _getMatches(character, modifiers, action, remove, combination) {
  19309. var i,
  19310. callback,
  19311. matches = [];
  19312. // if there are no events related to this keycode
  19313. if (!_callbacks[character]) {
  19314. return [];
  19315. }
  19316. // if a modifier key is coming up on its own we should allow it
  19317. if (action == 'keyup' && _isModifier(character)) {
  19318. modifiers = [character];
  19319. }
  19320. // loop through all callbacks for the key that was pressed
  19321. // and see if any of them match
  19322. for (i = 0; i < _callbacks[character].length; ++i) {
  19323. callback = _callbacks[character][i];
  19324. // if this is a sequence but it is not at the right level
  19325. // then move onto the next match
  19326. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  19327. continue;
  19328. }
  19329. // if the action we are looking for doesn't match the action we got
  19330. // then we should keep going
  19331. if (action != callback.action) {
  19332. continue;
  19333. }
  19334. // if this is a keypress event that means that we need to only
  19335. // look at the character, otherwise check the modifiers as
  19336. // well
  19337. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  19338. // remove is used so if you change your mind and call bind a
  19339. // second time with a new function the first one is overwritten
  19340. if (remove && callback.combo == combination) {
  19341. _callbacks[character].splice(i, 1);
  19342. }
  19343. matches.push(callback);
  19344. }
  19345. }
  19346. return matches;
  19347. }
  19348. /**
  19349. * takes a key event and figures out what the modifiers are
  19350. *
  19351. * @param {Event} e
  19352. * @returns {Array}
  19353. */
  19354. function _eventModifiers(e) {
  19355. var modifiers = [];
  19356. if (e.shiftKey) {
  19357. modifiers.push('shift');
  19358. }
  19359. if (e.altKey) {
  19360. modifiers.push('alt');
  19361. }
  19362. if (e.ctrlKey) {
  19363. modifiers.push('ctrl');
  19364. }
  19365. if (e.metaKey) {
  19366. modifiers.push('meta');
  19367. }
  19368. return modifiers;
  19369. }
  19370. /**
  19371. * actually calls the callback function
  19372. *
  19373. * if your callback function returns false this will use the jquery
  19374. * convention - prevent default and stop propogation on the event
  19375. *
  19376. * @param {Function} callback
  19377. * @param {Event} e
  19378. * @returns void
  19379. */
  19380. function _fireCallback(callback, e) {
  19381. if (callback(e) === false) {
  19382. if (e.preventDefault) {
  19383. e.preventDefault();
  19384. }
  19385. if (e.stopPropagation) {
  19386. e.stopPropagation();
  19387. }
  19388. e.returnValue = false;
  19389. e.cancelBubble = true;
  19390. }
  19391. }
  19392. /**
  19393. * handles a character key event
  19394. *
  19395. * @param {string} character
  19396. * @param {Event} e
  19397. * @returns void
  19398. */
  19399. function _handleCharacter(character, e) {
  19400. // if this event should not happen stop here
  19401. if (_stop(e)) {
  19402. return;
  19403. }
  19404. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  19405. i,
  19406. do_not_reset = {},
  19407. processed_sequence_callback = false;
  19408. // loop through matching callbacks for this key event
  19409. for (i = 0; i < callbacks.length; ++i) {
  19410. // fire for all sequence callbacks
  19411. // this is because if for example you have multiple sequences
  19412. // bound such as "g i" and "g t" they both need to fire the
  19413. // callback for matching g cause otherwise you can only ever
  19414. // match the first one
  19415. if (callbacks[i].seq) {
  19416. processed_sequence_callback = true;
  19417. // keep a list of which sequences were matches for later
  19418. do_not_reset[callbacks[i].seq] = 1;
  19419. _fireCallback(callbacks[i].callback, e);
  19420. continue;
  19421. }
  19422. // if there were no sequence matches but we are still here
  19423. // that means this is a regular match so we should fire that
  19424. if (!processed_sequence_callback && !_inside_sequence) {
  19425. _fireCallback(callbacks[i].callback, e);
  19426. }
  19427. }
  19428. // if you are inside of a sequence and the key you are pressing
  19429. // is not a modifier key then we should reset all sequences
  19430. // that were not matched by this key event
  19431. if (e.type == _inside_sequence && !_isModifier(character)) {
  19432. _resetSequences(do_not_reset);
  19433. }
  19434. }
  19435. /**
  19436. * handles a keydown event
  19437. *
  19438. * @param {Event} e
  19439. * @returns void
  19440. */
  19441. function _handleKey(e) {
  19442. // normalize e.which for key events
  19443. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  19444. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  19445. var character = _characterFromEvent(e);
  19446. // no character found then stop
  19447. if (!character) {
  19448. return;
  19449. }
  19450. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  19451. _ignore_next_keyup = false;
  19452. return;
  19453. }
  19454. _handleCharacter(character, e);
  19455. }
  19456. /**
  19457. * determines if the keycode specified is a modifier key or not
  19458. *
  19459. * @param {string} key
  19460. * @returns {boolean}
  19461. */
  19462. function _isModifier(key) {
  19463. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  19464. }
  19465. /**
  19466. * called to set a 1 second timeout on the specified sequence
  19467. *
  19468. * this is so after each key press in the sequence you have 1 second
  19469. * to press the next key before you have to start over
  19470. *
  19471. * @returns void
  19472. */
  19473. function _resetSequenceTimer() {
  19474. clearTimeout(_reset_timer);
  19475. _reset_timer = setTimeout(_resetSequences, 1000);
  19476. }
  19477. /**
  19478. * reverses the map lookup so that we can look for specific keys
  19479. * to see what can and can't use keypress
  19480. *
  19481. * @return {Object}
  19482. */
  19483. function _getReverseMap() {
  19484. if (!_REVERSE_MAP) {
  19485. _REVERSE_MAP = {};
  19486. for (var key in _MAP) {
  19487. // pull out the numeric keypad from here cause keypress should
  19488. // be able to detect the keys from the character
  19489. if (key > 95 && key < 112) {
  19490. continue;
  19491. }
  19492. if (_MAP.hasOwnProperty(key)) {
  19493. _REVERSE_MAP[_MAP[key]] = key;
  19494. }
  19495. }
  19496. }
  19497. return _REVERSE_MAP;
  19498. }
  19499. /**
  19500. * picks the best action based on the key combination
  19501. *
  19502. * @param {string} key - character for key
  19503. * @param {Array} modifiers
  19504. * @param {string=} action passed in
  19505. */
  19506. function _pickBestAction(key, modifiers, action) {
  19507. // if no action was picked in we should try to pick the one
  19508. // that we think would work best for this key
  19509. if (!action) {
  19510. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  19511. }
  19512. // modifier keys don't work as expected with keypress,
  19513. // switch to keydown
  19514. if (action == 'keypress' && modifiers.length) {
  19515. action = 'keydown';
  19516. }
  19517. return action;
  19518. }
  19519. /**
  19520. * binds a key sequence to an event
  19521. *
  19522. * @param {string} combo - combo specified in bind call
  19523. * @param {Array} keys
  19524. * @param {Function} callback
  19525. * @param {string=} action
  19526. * @returns void
  19527. */
  19528. function _bindSequence(combo, keys, callback, action) {
  19529. // start off by adding a sequence level record for this combination
  19530. // and setting the level to 0
  19531. _sequence_levels[combo] = 0;
  19532. // if there is no action pick the best one for the first key
  19533. // in the sequence
  19534. if (!action) {
  19535. action = _pickBestAction(keys[0], []);
  19536. }
  19537. /**
  19538. * callback to increase the sequence level for this sequence and reset
  19539. * all other sequences that were active
  19540. *
  19541. * @param {Event} e
  19542. * @returns void
  19543. */
  19544. var _increaseSequence = function(e) {
  19545. _inside_sequence = action;
  19546. ++_sequence_levels[combo];
  19547. _resetSequenceTimer();
  19548. },
  19549. /**
  19550. * wraps the specified callback inside of another function in order
  19551. * to reset all sequence counters as soon as this sequence is done
  19552. *
  19553. * @param {Event} e
  19554. * @returns void
  19555. */
  19556. _callbackAndReset = function(e) {
  19557. _fireCallback(callback, e);
  19558. // we should ignore the next key up if the action is key down
  19559. // or keypress. this is so if you finish a sequence and
  19560. // release the key the final key will not trigger a keyup
  19561. if (action !== 'keyup') {
  19562. _ignore_next_keyup = _characterFromEvent(e);
  19563. }
  19564. // weird race condition if a sequence ends with the key
  19565. // another sequence begins with
  19566. setTimeout(_resetSequences, 10);
  19567. },
  19568. i;
  19569. // loop through keys one at a time and bind the appropriate callback
  19570. // function. for any key leading up to the final one it should
  19571. // increase the sequence. after the final, it should reset all sequences
  19572. for (i = 0; i < keys.length; ++i) {
  19573. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  19574. }
  19575. }
  19576. /**
  19577. * binds a single keyboard combination
  19578. *
  19579. * @param {string} combination
  19580. * @param {Function} callback
  19581. * @param {string=} action
  19582. * @param {string=} sequence_name - name of sequence if part of sequence
  19583. * @param {number=} level - what part of the sequence the command is
  19584. * @returns void
  19585. */
  19586. function _bindSingle(combination, callback, action, sequence_name, level) {
  19587. // make sure multiple spaces in a row become a single space
  19588. combination = combination.replace(/\s+/g, ' ');
  19589. var sequence = combination.split(' '),
  19590. i,
  19591. key,
  19592. keys,
  19593. modifiers = [];
  19594. // if this pattern is a sequence of keys then run through this method
  19595. // to reprocess each pattern one key at a time
  19596. if (sequence.length > 1) {
  19597. return _bindSequence(combination, sequence, callback, action);
  19598. }
  19599. // take the keys from this pattern and figure out what the actual
  19600. // pattern is all about
  19601. keys = combination === '+' ? ['+'] : combination.split('+');
  19602. for (i = 0; i < keys.length; ++i) {
  19603. key = keys[i];
  19604. // normalize key names
  19605. if (_SPECIAL_ALIASES[key]) {
  19606. key = _SPECIAL_ALIASES[key];
  19607. }
  19608. // if this is not a keypress event then we should
  19609. // be smart about using shift keys
  19610. // this will only work for US keyboards however
  19611. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  19612. key = _SHIFT_MAP[key];
  19613. modifiers.push('shift');
  19614. }
  19615. // if this key is a modifier then add it to the list of modifiers
  19616. if (_isModifier(key)) {
  19617. modifiers.push(key);
  19618. }
  19619. }
  19620. // depending on what the key combination is
  19621. // we will try to pick the best event for it
  19622. action = _pickBestAction(key, modifiers, action);
  19623. // make sure to initialize array if this is the first time
  19624. // a callback is added for this key
  19625. if (!_callbacks[key]) {
  19626. _callbacks[key] = [];
  19627. }
  19628. // remove an existing match if there is one
  19629. _getMatches(key, modifiers, action, !sequence_name, combination);
  19630. // add this call back to the array
  19631. // if it is a sequence put it at the beginning
  19632. // if not put it at the end
  19633. //
  19634. // this is important because the way these are processed expects
  19635. // the sequence ones to come first
  19636. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  19637. callback: callback,
  19638. modifiers: modifiers,
  19639. action: action,
  19640. seq: sequence_name,
  19641. level: level,
  19642. combo: combination
  19643. });
  19644. }
  19645. /**
  19646. * binds multiple combinations to the same callback
  19647. *
  19648. * @param {Array} combinations
  19649. * @param {Function} callback
  19650. * @param {string|undefined} action
  19651. * @returns void
  19652. */
  19653. function _bindMultiple(combinations, callback, action) {
  19654. for (var i = 0; i < combinations.length; ++i) {
  19655. _bindSingle(combinations[i], callback, action);
  19656. }
  19657. }
  19658. // start!
  19659. _addEvent(document, 'keypress', _handleKey);
  19660. _addEvent(document, 'keydown', _handleKey);
  19661. _addEvent(document, 'keyup', _handleKey);
  19662. var mousetrap = {
  19663. /**
  19664. * binds an event to mousetrap
  19665. *
  19666. * can be a single key, a combination of keys separated with +,
  19667. * a comma separated list of keys, an array of keys, or
  19668. * a sequence of keys separated by spaces
  19669. *
  19670. * be sure to list the modifier keys first to make sure that the
  19671. * correct key ends up getting bound (the last key in the pattern)
  19672. *
  19673. * @param {string|Array} keys
  19674. * @param {Function} callback
  19675. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  19676. * @returns void
  19677. */
  19678. bind: function(keys, callback, action) {
  19679. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  19680. _direct_map[keys + ':' + action] = callback;
  19681. return this;
  19682. },
  19683. /**
  19684. * unbinds an event to mousetrap
  19685. *
  19686. * the unbinding sets the callback function of the specified key combo
  19687. * to an empty function and deletes the corresponding key in the
  19688. * _direct_map dict.
  19689. *
  19690. * the keycombo+action has to be exactly the same as
  19691. * it was defined in the bind method
  19692. *
  19693. * TODO: actually remove this from the _callbacks dictionary instead
  19694. * of binding an empty function
  19695. *
  19696. * @param {string|Array} keys
  19697. * @param {string} action
  19698. * @returns void
  19699. */
  19700. unbind: function(keys, action) {
  19701. if (_direct_map[keys + ':' + action]) {
  19702. delete _direct_map[keys + ':' + action];
  19703. this.bind(keys, function() {}, action);
  19704. }
  19705. return this;
  19706. },
  19707. /**
  19708. * triggers an event that has already been bound
  19709. *
  19710. * @param {string} keys
  19711. * @param {string=} action
  19712. * @returns void
  19713. */
  19714. trigger: function(keys, action) {
  19715. _direct_map[keys + ':' + action]();
  19716. return this;
  19717. },
  19718. /**
  19719. * resets the library back to its initial state. this is useful
  19720. * if you want to clear out the current keyboard shortcuts and bind
  19721. * new ones - for example if you switch to another page
  19722. *
  19723. * @returns void
  19724. */
  19725. reset: function() {
  19726. _callbacks = {};
  19727. _direct_map = {};
  19728. return this;
  19729. }
  19730. };
  19731. module.exports = mousetrap;
  19732. },{}]},{},[1])
  19733. (1)
  19734. });