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.

22938 lines
667 KiB

  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.6.0-SNAPSHOT
  8. * @date 2014-03-03
  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. * Convert an object to another type
  354. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  355. * @param {String | undefined} type Name of the type. Available types:
  356. * 'Boolean', 'Number', 'String',
  357. * 'Date', 'Moment', ISODate', 'ASPDate'.
  358. * @return {*} object
  359. * @throws Error
  360. */
  361. util.convert = function convert(object, type) {
  362. var match;
  363. if (object === undefined) {
  364. return undefined;
  365. }
  366. if (object === null) {
  367. return null;
  368. }
  369. if (!type) {
  370. return object;
  371. }
  372. if (!(typeof type === 'string') && !(type instanceof String)) {
  373. throw new Error('Type must be a string');
  374. }
  375. //noinspection FallthroughInSwitchStatementJS
  376. switch (type) {
  377. case 'boolean':
  378. case 'Boolean':
  379. return Boolean(object);
  380. case 'number':
  381. case 'Number':
  382. return Number(object.valueOf());
  383. case 'string':
  384. case 'String':
  385. return String(object);
  386. case 'Date':
  387. if (util.isNumber(object)) {
  388. return new Date(object);
  389. }
  390. if (object instanceof Date) {
  391. return new Date(object.valueOf());
  392. }
  393. else if (moment.isMoment(object)) {
  394. return new Date(object.valueOf());
  395. }
  396. if (util.isString(object)) {
  397. match = ASPDateRegex.exec(object);
  398. if (match) {
  399. // object is an ASP date
  400. return new Date(Number(match[1])); // parse number
  401. }
  402. else {
  403. return moment(object).toDate(); // parse string
  404. }
  405. }
  406. else {
  407. throw new Error(
  408. 'Cannot convert object of type ' + util.getType(object) +
  409. ' to type Date');
  410. }
  411. case 'Moment':
  412. if (util.isNumber(object)) {
  413. return moment(object);
  414. }
  415. if (object instanceof Date) {
  416. return moment(object.valueOf());
  417. }
  418. else if (moment.isMoment(object)) {
  419. return moment(object);
  420. }
  421. if (util.isString(object)) {
  422. match = ASPDateRegex.exec(object);
  423. if (match) {
  424. // object is an ASP date
  425. return moment(Number(match[1])); // parse number
  426. }
  427. else {
  428. return moment(object); // parse string
  429. }
  430. }
  431. else {
  432. throw new Error(
  433. 'Cannot convert object of type ' + util.getType(object) +
  434. ' to type Date');
  435. }
  436. case 'ISODate':
  437. if (util.isNumber(object)) {
  438. return new Date(object);
  439. }
  440. else if (object instanceof Date) {
  441. return object.toISOString();
  442. }
  443. else if (moment.isMoment(object)) {
  444. return object.toDate().toISOString();
  445. }
  446. else if (util.isString(object)) {
  447. match = ASPDateRegex.exec(object);
  448. if (match) {
  449. // object is an ASP date
  450. return new Date(Number(match[1])).toISOString(); // parse number
  451. }
  452. else {
  453. return new Date(object).toISOString(); // parse string
  454. }
  455. }
  456. else {
  457. throw new Error(
  458. 'Cannot convert object of type ' + util.getType(object) +
  459. ' to type ISODate');
  460. }
  461. case 'ASPDate':
  462. if (util.isNumber(object)) {
  463. return '/Date(' + object + ')/';
  464. }
  465. else if (object instanceof Date) {
  466. return '/Date(' + object.valueOf() + ')/';
  467. }
  468. else if (util.isString(object)) {
  469. match = ASPDateRegex.exec(object);
  470. var value;
  471. if (match) {
  472. // object is an ASP date
  473. value = new Date(Number(match[1])).valueOf(); // parse number
  474. }
  475. else {
  476. value = new Date(object).valueOf(); // parse string
  477. }
  478. return '/Date(' + value + ')/';
  479. }
  480. else {
  481. throw new Error(
  482. 'Cannot convert object of type ' + util.getType(object) +
  483. ' to type ASPDate');
  484. }
  485. default:
  486. throw new Error('Cannot convert object of type ' + util.getType(object) +
  487. ' to type "' + type + '"');
  488. }
  489. };
  490. // parse ASP.Net Date pattern,
  491. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  492. // code from http://momentjs.com/
  493. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  494. /**
  495. * Get the type of an object, for example util.getType([]) returns 'Array'
  496. * @param {*} object
  497. * @return {String} type
  498. */
  499. util.getType = function getType(object) {
  500. var type = typeof object;
  501. if (type == 'object') {
  502. if (object == null) {
  503. return 'null';
  504. }
  505. if (object instanceof Boolean) {
  506. return 'Boolean';
  507. }
  508. if (object instanceof Number) {
  509. return 'Number';
  510. }
  511. if (object instanceof String) {
  512. return 'String';
  513. }
  514. if (object instanceof Array) {
  515. return 'Array';
  516. }
  517. if (object instanceof Date) {
  518. return 'Date';
  519. }
  520. return 'Object';
  521. }
  522. else if (type == 'number') {
  523. return 'Number';
  524. }
  525. else if (type == 'boolean') {
  526. return 'Boolean';
  527. }
  528. else if (type == 'string') {
  529. return 'String';
  530. }
  531. return type;
  532. };
  533. /**
  534. * Retrieve the absolute left value of a DOM element
  535. * @param {Element} elem A dom element, for example a div
  536. * @return {number} left The absolute left position of this element
  537. * in the browser page.
  538. */
  539. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  540. var doc = document.documentElement;
  541. var body = document.body;
  542. var left = elem.offsetLeft;
  543. var e = elem.offsetParent;
  544. while (e != null && e != body && e != doc) {
  545. left += e.offsetLeft;
  546. left -= e.scrollLeft;
  547. e = e.offsetParent;
  548. }
  549. return left;
  550. };
  551. /**
  552. * Retrieve the absolute top value of a DOM element
  553. * @param {Element} elem A dom element, for example a div
  554. * @return {number} top The absolute top position of this element
  555. * in the browser page.
  556. */
  557. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  558. var doc = document.documentElement;
  559. var body = document.body;
  560. var top = elem.offsetTop;
  561. var e = elem.offsetParent;
  562. while (e != null && e != body && e != doc) {
  563. top += e.offsetTop;
  564. top -= e.scrollTop;
  565. e = e.offsetParent;
  566. }
  567. return top;
  568. };
  569. /**
  570. * Get the absolute, vertical mouse position from an event.
  571. * @param {Event} event
  572. * @return {Number} pageY
  573. */
  574. util.getPageY = function getPageY (event) {
  575. if ('pageY' in event) {
  576. return event.pageY;
  577. }
  578. else {
  579. var clientY;
  580. if (('targetTouches' in event) && event.targetTouches.length) {
  581. clientY = event.targetTouches[0].clientY;
  582. }
  583. else {
  584. clientY = event.clientY;
  585. }
  586. var doc = document.documentElement;
  587. var body = document.body;
  588. return clientY +
  589. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  590. ( doc && doc.clientTop || body && body.clientTop || 0 );
  591. }
  592. };
  593. /**
  594. * Get the absolute, horizontal mouse position from an event.
  595. * @param {Event} event
  596. * @return {Number} pageX
  597. */
  598. util.getPageX = function getPageX (event) {
  599. if ('pageY' in event) {
  600. return event.pageX;
  601. }
  602. else {
  603. var clientX;
  604. if (('targetTouches' in event) && event.targetTouches.length) {
  605. clientX = event.targetTouches[0].clientX;
  606. }
  607. else {
  608. clientX = event.clientX;
  609. }
  610. var doc = document.documentElement;
  611. var body = document.body;
  612. return clientX +
  613. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  614. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  615. }
  616. };
  617. /**
  618. * add a className to the given elements style
  619. * @param {Element} elem
  620. * @param {String} className
  621. */
  622. util.addClassName = function addClassName(elem, className) {
  623. var classes = elem.className.split(' ');
  624. if (classes.indexOf(className) == -1) {
  625. classes.push(className); // add the class to the array
  626. elem.className = classes.join(' ');
  627. }
  628. };
  629. /**
  630. * add a className to the given elements style
  631. * @param {Element} elem
  632. * @param {String} className
  633. */
  634. util.removeClassName = function removeClassname(elem, className) {
  635. var classes = elem.className.split(' ');
  636. var index = classes.indexOf(className);
  637. if (index != -1) {
  638. classes.splice(index, 1); // remove the class from the array
  639. elem.className = classes.join(' ');
  640. }
  641. };
  642. /**
  643. * For each method for both arrays and objects.
  644. * In case of an array, the built-in Array.forEach() is applied.
  645. * In case of an Object, the method loops over all properties of the object.
  646. * @param {Object | Array} object An Object or Array
  647. * @param {function} callback Callback method, called for each item in
  648. * the object or array with three parameters:
  649. * callback(value, index, object)
  650. */
  651. util.forEach = function forEach (object, callback) {
  652. var i,
  653. len;
  654. if (object instanceof Array) {
  655. // array
  656. for (i = 0, len = object.length; i < len; i++) {
  657. callback(object[i], i, object);
  658. }
  659. }
  660. else {
  661. // object
  662. for (i in object) {
  663. if (object.hasOwnProperty(i)) {
  664. callback(object[i], i, object);
  665. }
  666. }
  667. }
  668. };
  669. /**
  670. * Update a property in an object
  671. * @param {Object} object
  672. * @param {String} key
  673. * @param {*} value
  674. * @return {Boolean} changed
  675. */
  676. util.updateProperty = function updateProp (object, key, value) {
  677. if (object[key] !== value) {
  678. object[key] = value;
  679. return true;
  680. }
  681. else {
  682. return false;
  683. }
  684. };
  685. /**
  686. * Add and event listener. Works for all browsers
  687. * @param {Element} element An html element
  688. * @param {string} action The action, for example "click",
  689. * without the prefix "on"
  690. * @param {function} listener The callback function to be executed
  691. * @param {boolean} [useCapture]
  692. */
  693. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  694. if (element.addEventListener) {
  695. if (useCapture === undefined)
  696. useCapture = false;
  697. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  698. action = "DOMMouseScroll"; // For Firefox
  699. }
  700. element.addEventListener(action, listener, useCapture);
  701. } else {
  702. element.attachEvent("on" + action, listener); // IE browsers
  703. }
  704. };
  705. /**
  706. * Remove an event listener from an element
  707. * @param {Element} element An html dom element
  708. * @param {string} action The name of the event, for example "mousedown"
  709. * @param {function} listener The listener function
  710. * @param {boolean} [useCapture]
  711. */
  712. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  713. if (element.removeEventListener) {
  714. // non-IE browsers
  715. if (useCapture === undefined)
  716. useCapture = false;
  717. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  718. action = "DOMMouseScroll"; // For Firefox
  719. }
  720. element.removeEventListener(action, listener, useCapture);
  721. } else {
  722. // IE browsers
  723. element.detachEvent("on" + action, listener);
  724. }
  725. };
  726. /**
  727. * Get HTML element which is the target of the event
  728. * @param {Event} event
  729. * @return {Element} target element
  730. */
  731. util.getTarget = function getTarget(event) {
  732. // code from http://www.quirksmode.org/js/events_properties.html
  733. if (!event) {
  734. event = window.event;
  735. }
  736. var target;
  737. if (event.target) {
  738. target = event.target;
  739. }
  740. else if (event.srcElement) {
  741. target = event.srcElement;
  742. }
  743. if (target.nodeType != undefined && target.nodeType == 3) {
  744. // defeat Safari bug
  745. target = target.parentNode;
  746. }
  747. return target;
  748. };
  749. /**
  750. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  751. * @param {Element} element
  752. * @param {Event} event
  753. */
  754. util.fakeGesture = function fakeGesture (element, event) {
  755. var eventType = null;
  756. // for hammer.js 1.0.5
  757. var gesture = Hammer.event.collectEventData(this, eventType, event);
  758. // for hammer.js 1.0.6
  759. //var touches = Hammer.event.getTouchList(event, eventType);
  760. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  761. // on IE in standards mode, no touches are recognized by hammer.js,
  762. // resulting in NaN values for center.pageX and center.pageY
  763. if (isNaN(gesture.center.pageX)) {
  764. gesture.center.pageX = event.pageX;
  765. }
  766. if (isNaN(gesture.center.pageY)) {
  767. gesture.center.pageY = event.pageY;
  768. }
  769. return gesture;
  770. };
  771. util.option = {};
  772. /**
  773. * Convert a value into a boolean
  774. * @param {Boolean | function | undefined} value
  775. * @param {Boolean} [defaultValue]
  776. * @returns {Boolean} bool
  777. */
  778. util.option.asBoolean = function (value, defaultValue) {
  779. if (typeof value == 'function') {
  780. value = value();
  781. }
  782. if (value != null) {
  783. return (value != false);
  784. }
  785. return defaultValue || null;
  786. };
  787. /**
  788. * Convert a value into a number
  789. * @param {Boolean | function | undefined} value
  790. * @param {Number} [defaultValue]
  791. * @returns {Number} number
  792. */
  793. util.option.asNumber = function (value, defaultValue) {
  794. if (typeof value == 'function') {
  795. value = value();
  796. }
  797. if (value != null) {
  798. return Number(value) || defaultValue || null;
  799. }
  800. return defaultValue || null;
  801. };
  802. /**
  803. * Convert a value into a string
  804. * @param {String | function | undefined} value
  805. * @param {String} [defaultValue]
  806. * @returns {String} str
  807. */
  808. util.option.asString = function (value, defaultValue) {
  809. if (typeof value == 'function') {
  810. value = value();
  811. }
  812. if (value != null) {
  813. return String(value);
  814. }
  815. return defaultValue || null;
  816. };
  817. /**
  818. * Convert a size or location into a string with pixels or a percentage
  819. * @param {String | Number | function | undefined} value
  820. * @param {String} [defaultValue]
  821. * @returns {String} size
  822. */
  823. util.option.asSize = function (value, defaultValue) {
  824. if (typeof value == 'function') {
  825. value = value();
  826. }
  827. if (util.isString(value)) {
  828. return value;
  829. }
  830. else if (util.isNumber(value)) {
  831. return value + 'px';
  832. }
  833. else {
  834. return defaultValue || null;
  835. }
  836. };
  837. /**
  838. * Convert a value into a DOM element
  839. * @param {HTMLElement | function | undefined} value
  840. * @param {HTMLElement} [defaultValue]
  841. * @returns {HTMLElement | null} dom
  842. */
  843. util.option.asElement = function (value, defaultValue) {
  844. if (typeof value == 'function') {
  845. value = value();
  846. }
  847. return value || defaultValue || null;
  848. };
  849. util.GiveDec = function GiveDec(Hex)
  850. {
  851. if(Hex == "A")
  852. Value = 10;
  853. else
  854. if(Hex == "B")
  855. Value = 11;
  856. else
  857. if(Hex == "C")
  858. Value = 12;
  859. else
  860. if(Hex == "D")
  861. Value = 13;
  862. else
  863. if(Hex == "E")
  864. Value = 14;
  865. else
  866. if(Hex == "F")
  867. Value = 15;
  868. else
  869. Value = eval(Hex)
  870. return Value;
  871. }
  872. util.GiveHex = function GiveHex(Dec)
  873. {
  874. if(Dec == 10)
  875. Value = "A";
  876. else
  877. if(Dec == 11)
  878. Value = "B";
  879. else
  880. if(Dec == 12)
  881. Value = "C";
  882. else
  883. if(Dec == 13)
  884. Value = "D";
  885. else
  886. if(Dec == 14)
  887. Value = "E";
  888. else
  889. if(Dec == 15)
  890. Value = "F";
  891. else
  892. Value = "" + Dec;
  893. return Value;
  894. }
  895. /**
  896. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  897. *
  898. * @param {String} hex
  899. * @returns {{r: *, g: *, b: *}}
  900. */
  901. util.hexToRGB = function hexToRGB(hex) {
  902. hex = hex.replace("#","").toUpperCase();
  903. var a = util.GiveDec(hex.substring(0, 1));
  904. var b = util.GiveDec(hex.substring(1, 2));
  905. var c = util.GiveDec(hex.substring(2, 3));
  906. var d = util.GiveDec(hex.substring(3, 4));
  907. var e = util.GiveDec(hex.substring(4, 5));
  908. var f = util.GiveDec(hex.substring(5, 6));
  909. var r = (a * 16) + b;
  910. var g = (c * 16) + d;
  911. var b = (e * 16) + f;
  912. return {r:r,g:g,b:b};
  913. };
  914. util.RGBToHex = function RGBToHex(red,green,blue) {
  915. var a = util.GiveHex(Math.floor(red / 16));
  916. var b = util.GiveHex(red % 16);
  917. var c = util.GiveHex(Math.floor(green / 16));
  918. var d = util.GiveHex(green % 16);
  919. var e = util.GiveHex(Math.floor(blue / 16));
  920. var f = util.GiveHex(blue % 16);
  921. var hex = a + b + c + d + e + f;
  922. return "#" + hex;
  923. };
  924. /**
  925. * http://www.javascripter.net/faq/rgb2hsv.htm
  926. *
  927. * @param red
  928. * @param green
  929. * @param blue
  930. * @returns {*}
  931. * @constructor
  932. */
  933. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  934. red=red/255; green=green/255; blue=blue/255;
  935. var minRGB = Math.min(red,Math.min(green,blue));
  936. var maxRGB = Math.max(red,Math.max(green,blue));
  937. // Black-gray-white
  938. if (minRGB == maxRGB) {
  939. return {h:0,s:0,v:minRGB};
  940. }
  941. // Colors other than black-gray-white:
  942. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  943. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  944. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  945. var saturation = (maxRGB - minRGB)/maxRGB;
  946. var value = maxRGB;
  947. return {h:hue,s:saturation,v:value};
  948. };
  949. /**
  950. * https://gist.github.com/mjijackson/5311256
  951. * @param hue
  952. * @param saturation
  953. * @param value
  954. * @returns {{r: number, g: number, b: number}}
  955. * @constructor
  956. */
  957. util.HSVToRGB = function HSVToRGB(h, s, v) {
  958. var r, g, b;
  959. var i = Math.floor(h * 6);
  960. var f = h * 6 - i;
  961. var p = v * (1 - s);
  962. var q = v * (1 - f * s);
  963. var t = v * (1 - (1 - f) * s);
  964. switch (i % 6) {
  965. case 0: r = v, g = t, b = p; break;
  966. case 1: r = q, g = v, b = p; break;
  967. case 2: r = p, g = v, b = t; break;
  968. case 3: r = p, g = q, b = v; break;
  969. case 4: r = t, g = p, b = v; break;
  970. case 5: r = v, g = p, b = q; break;
  971. }
  972. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  973. };
  974. util.HSVToHex = function HSVToHex(h,s,v) {
  975. var rgb = util.HSVToRGB(h,s,v);
  976. return util.RGBToHex(rgb.r,rgb.g,rgb.b);
  977. }
  978. util.hexToHSV = function hexToHSV(hex) {
  979. var rgb = util.hexToRGB(hex);
  980. return util.RGBToHSV(rgb.r,rgb.g,rgb.b);
  981. }
  982. util.isValidHex = function isValidHex(hex) {
  983. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  984. return isOk;
  985. }
  986. /**
  987. * DataSet
  988. *
  989. * Usage:
  990. * var dataSet = new DataSet({
  991. * fieldId: '_id',
  992. * convert: {
  993. * // ...
  994. * }
  995. * });
  996. *
  997. * dataSet.add(item);
  998. * dataSet.add(data);
  999. * dataSet.update(item);
  1000. * dataSet.update(data);
  1001. * dataSet.remove(id);
  1002. * dataSet.remove(ids);
  1003. * var data = dataSet.get();
  1004. * var data = dataSet.get(id);
  1005. * var data = dataSet.get(ids);
  1006. * var data = dataSet.get(ids, options, data);
  1007. * dataSet.clear();
  1008. *
  1009. * A data set can:
  1010. * - add/remove/update data
  1011. * - gives triggers upon changes in the data
  1012. * - can import/export data in various data formats
  1013. *
  1014. * @param {Object} [options] Available options:
  1015. * {String} fieldId Field name of the id in the
  1016. * items, 'id' by default.
  1017. * {Object.<String, String} convert
  1018. * A map with field names as key,
  1019. * and the field type as value.
  1020. * @constructor DataSet
  1021. */
  1022. // TODO: add a DataSet constructor DataSet(data, options)
  1023. function DataSet (options) {
  1024. this.id = util.randomUUID();
  1025. this.options = options || {};
  1026. this.data = {}; // map with data indexed by id
  1027. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1028. this.convert = {}; // field types by field name
  1029. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1030. if (this.options.convert) {
  1031. for (var field in this.options.convert) {
  1032. if (this.options.convert.hasOwnProperty(field)) {
  1033. var value = this.options.convert[field];
  1034. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1035. this.convert[field] = 'Date';
  1036. }
  1037. else {
  1038. this.convert[field] = value;
  1039. }
  1040. }
  1041. }
  1042. }
  1043. // event subscribers
  1044. this.subscribers = {};
  1045. this.internalIds = {}; // internally generated id's
  1046. }
  1047. /**
  1048. * Subscribe to an event, add an event listener
  1049. * @param {String} event Event name. Available events: 'put', 'update',
  1050. * 'remove'
  1051. * @param {function} callback Callback method. Called with three parameters:
  1052. * {String} event
  1053. * {Object | null} params
  1054. * {String | Number} senderId
  1055. */
  1056. DataSet.prototype.on = function on (event, callback) {
  1057. var subscribers = this.subscribers[event];
  1058. if (!subscribers) {
  1059. subscribers = [];
  1060. this.subscribers[event] = subscribers;
  1061. }
  1062. subscribers.push({
  1063. callback: callback
  1064. });
  1065. };
  1066. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1067. DataSet.prototype.subscribe = DataSet.prototype.on;
  1068. /**
  1069. * Unsubscribe from an event, remove an event listener
  1070. * @param {String} event
  1071. * @param {function} callback
  1072. */
  1073. DataSet.prototype.off = function off(event, callback) {
  1074. var subscribers = this.subscribers[event];
  1075. if (subscribers) {
  1076. this.subscribers[event] = subscribers.filter(function (listener) {
  1077. return (listener.callback != callback);
  1078. });
  1079. }
  1080. };
  1081. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1082. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1083. /**
  1084. * Trigger an event
  1085. * @param {String} event
  1086. * @param {Object | null} params
  1087. * @param {String} [senderId] Optional id of the sender.
  1088. * @private
  1089. */
  1090. DataSet.prototype._trigger = function (event, params, senderId) {
  1091. if (event == '*') {
  1092. throw new Error('Cannot trigger event *');
  1093. }
  1094. var subscribers = [];
  1095. if (event in this.subscribers) {
  1096. subscribers = subscribers.concat(this.subscribers[event]);
  1097. }
  1098. if ('*' in this.subscribers) {
  1099. subscribers = subscribers.concat(this.subscribers['*']);
  1100. }
  1101. for (var i = 0; i < subscribers.length; i++) {
  1102. var subscriber = subscribers[i];
  1103. if (subscriber.callback) {
  1104. subscriber.callback(event, params, senderId || null);
  1105. }
  1106. }
  1107. };
  1108. /**
  1109. * Add data.
  1110. * Adding an item will fail when there already is an item with the same id.
  1111. * @param {Object | Array | DataTable} data
  1112. * @param {String} [senderId] Optional sender id
  1113. * @return {Array} addedIds Array with the ids of the added items
  1114. */
  1115. DataSet.prototype.add = function (data, senderId) {
  1116. var addedIds = [],
  1117. id,
  1118. me = this;
  1119. if (data instanceof Array) {
  1120. // Array
  1121. for (var i = 0, len = data.length; i < len; i++) {
  1122. id = me._addItem(data[i]);
  1123. addedIds.push(id);
  1124. }
  1125. }
  1126. else if (util.isDataTable(data)) {
  1127. // Google DataTable
  1128. var columns = this._getColumnNames(data);
  1129. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1130. var item = {};
  1131. for (var col = 0, cols = columns.length; col < cols; col++) {
  1132. var field = columns[col];
  1133. item[field] = data.getValue(row, col);
  1134. }
  1135. id = me._addItem(item);
  1136. addedIds.push(id);
  1137. }
  1138. }
  1139. else if (data instanceof Object) {
  1140. // Single item
  1141. id = me._addItem(data);
  1142. addedIds.push(id);
  1143. }
  1144. else {
  1145. throw new Error('Unknown dataType');
  1146. }
  1147. if (addedIds.length) {
  1148. this._trigger('add', {items: addedIds}, senderId);
  1149. }
  1150. return addedIds;
  1151. };
  1152. /**
  1153. * Update existing items. When an item does not exist, it will be created
  1154. * @param {Object | Array | DataTable} data
  1155. * @param {String} [senderId] Optional sender id
  1156. * @return {Array} updatedIds The ids of the added or updated items
  1157. */
  1158. DataSet.prototype.update = function (data, senderId) {
  1159. var addedIds = [],
  1160. updatedIds = [],
  1161. me = this,
  1162. fieldId = me.fieldId;
  1163. var addOrUpdate = function (item) {
  1164. var id = item[fieldId];
  1165. if (me.data[id]) {
  1166. // update item
  1167. id = me._updateItem(item);
  1168. updatedIds.push(id);
  1169. }
  1170. else {
  1171. // add new item
  1172. id = me._addItem(item);
  1173. addedIds.push(id);
  1174. }
  1175. };
  1176. if (data instanceof Array) {
  1177. // Array
  1178. for (var i = 0, len = data.length; i < len; i++) {
  1179. addOrUpdate(data[i]);
  1180. }
  1181. }
  1182. else if (util.isDataTable(data)) {
  1183. // Google DataTable
  1184. var columns = this._getColumnNames(data);
  1185. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1186. var item = {};
  1187. for (var col = 0, cols = columns.length; col < cols; col++) {
  1188. var field = columns[col];
  1189. item[field] = data.getValue(row, col);
  1190. }
  1191. addOrUpdate(item);
  1192. }
  1193. }
  1194. else if (data instanceof Object) {
  1195. // Single item
  1196. addOrUpdate(data);
  1197. }
  1198. else {
  1199. throw new Error('Unknown dataType');
  1200. }
  1201. if (addedIds.length) {
  1202. this._trigger('add', {items: addedIds}, senderId);
  1203. }
  1204. if (updatedIds.length) {
  1205. this._trigger('update', {items: updatedIds}, senderId);
  1206. }
  1207. return addedIds.concat(updatedIds);
  1208. };
  1209. /**
  1210. * Get a data item or multiple items.
  1211. *
  1212. * Usage:
  1213. *
  1214. * get()
  1215. * get(options: Object)
  1216. * get(options: Object, data: Array | DataTable)
  1217. *
  1218. * get(id: Number | String)
  1219. * get(id: Number | String, options: Object)
  1220. * get(id: Number | String, options: Object, data: Array | DataTable)
  1221. *
  1222. * get(ids: Number[] | String[])
  1223. * get(ids: Number[] | String[], options: Object)
  1224. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1225. *
  1226. * Where:
  1227. *
  1228. * {Number | String} id The id of an item
  1229. * {Number[] | String{}} ids An array with ids of items
  1230. * {Object} options An Object with options. Available options:
  1231. * {String} [type] Type of data to be returned. Can
  1232. * be 'DataTable' or 'Array' (default)
  1233. * {Object.<String, String>} [convert]
  1234. * {String[]} [fields] field names to be returned
  1235. * {function} [filter] filter items
  1236. * {String | function} [order] Order the items by
  1237. * a field name or custom sort function.
  1238. * {Array | DataTable} [data] If provided, items will be appended to this
  1239. * array or table. Required in case of Google
  1240. * DataTable.
  1241. *
  1242. * @throws Error
  1243. */
  1244. DataSet.prototype.get = function (args) {
  1245. var me = this;
  1246. var globalShowInternalIds = this.showInternalIds;
  1247. // parse the arguments
  1248. var id, ids, options, data;
  1249. var firstType = util.getType(arguments[0]);
  1250. if (firstType == 'String' || firstType == 'Number') {
  1251. // get(id [, options] [, data])
  1252. id = arguments[0];
  1253. options = arguments[1];
  1254. data = arguments[2];
  1255. }
  1256. else if (firstType == 'Array') {
  1257. // get(ids [, options] [, data])
  1258. ids = arguments[0];
  1259. options = arguments[1];
  1260. data = arguments[2];
  1261. }
  1262. else {
  1263. // get([, options] [, data])
  1264. options = arguments[0];
  1265. data = arguments[1];
  1266. }
  1267. // determine the return type
  1268. var type;
  1269. if (options && options.type) {
  1270. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1271. if (data && (type != util.getType(data))) {
  1272. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1273. 'does not correspond with specified options.type (' + options.type + ')');
  1274. }
  1275. if (type == 'DataTable' && !util.isDataTable(data)) {
  1276. throw new Error('Parameter "data" must be a DataTable ' +
  1277. 'when options.type is "DataTable"');
  1278. }
  1279. }
  1280. else if (data) {
  1281. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1282. }
  1283. else {
  1284. type = 'Array';
  1285. }
  1286. // we allow the setting of this value for a single get request.
  1287. if (options != undefined) {
  1288. if (options.showInternalIds != undefined) {
  1289. this.showInternalIds = options.showInternalIds;
  1290. }
  1291. }
  1292. // build options
  1293. var convert = options && options.convert || this.options.convert;
  1294. var filter = options && options.filter;
  1295. var items = [], item, itemId, i, len;
  1296. // convert items
  1297. if (id != undefined) {
  1298. // return a single item
  1299. item = me._getItem(id, convert);
  1300. if (filter && !filter(item)) {
  1301. item = null;
  1302. }
  1303. }
  1304. else if (ids != undefined) {
  1305. // return a subset of items
  1306. for (i = 0, len = ids.length; i < len; i++) {
  1307. item = me._getItem(ids[i], convert);
  1308. if (!filter || filter(item)) {
  1309. items.push(item);
  1310. }
  1311. }
  1312. }
  1313. else {
  1314. // return all items
  1315. for (itemId in this.data) {
  1316. if (this.data.hasOwnProperty(itemId)) {
  1317. item = me._getItem(itemId, convert);
  1318. if (!filter || filter(item)) {
  1319. items.push(item);
  1320. }
  1321. }
  1322. }
  1323. }
  1324. // restore the global value of showInternalIds
  1325. this.showInternalIds = globalShowInternalIds;
  1326. // order the results
  1327. if (options && options.order && id == undefined) {
  1328. this._sort(items, options.order);
  1329. }
  1330. // filter fields of the items
  1331. if (options && options.fields) {
  1332. var fields = options.fields;
  1333. if (id != undefined) {
  1334. item = this._filterFields(item, fields);
  1335. }
  1336. else {
  1337. for (i = 0, len = items.length; i < len; i++) {
  1338. items[i] = this._filterFields(items[i], fields);
  1339. }
  1340. }
  1341. }
  1342. // return the results
  1343. if (type == 'DataTable') {
  1344. var columns = this._getColumnNames(data);
  1345. if (id != undefined) {
  1346. // append a single item to the data table
  1347. me._appendRow(data, columns, item);
  1348. }
  1349. else {
  1350. // copy the items to the provided data table
  1351. for (i = 0, len = items.length; i < len; i++) {
  1352. me._appendRow(data, columns, items[i]);
  1353. }
  1354. }
  1355. return data;
  1356. }
  1357. else {
  1358. // return an array
  1359. if (id != undefined) {
  1360. // a single item
  1361. return item;
  1362. }
  1363. else {
  1364. // multiple items
  1365. if (data) {
  1366. // copy the items to the provided array
  1367. for (i = 0, len = items.length; i < len; i++) {
  1368. data.push(items[i]);
  1369. }
  1370. return data;
  1371. }
  1372. else {
  1373. // just return our array
  1374. return items;
  1375. }
  1376. }
  1377. }
  1378. };
  1379. /**
  1380. * Get ids of all items or from a filtered set of items.
  1381. * @param {Object} [options] An Object with options. Available options:
  1382. * {function} [filter] filter items
  1383. * {String | function} [order] Order the items by
  1384. * a field name or custom sort function.
  1385. * @return {Array} ids
  1386. */
  1387. DataSet.prototype.getIds = function (options) {
  1388. var data = this.data,
  1389. filter = options && options.filter,
  1390. order = options && options.order,
  1391. convert = options && options.convert || this.options.convert,
  1392. i,
  1393. len,
  1394. id,
  1395. item,
  1396. items,
  1397. ids = [];
  1398. if (filter) {
  1399. // get filtered items
  1400. if (order) {
  1401. // create ordered list
  1402. items = [];
  1403. for (id in data) {
  1404. if (data.hasOwnProperty(id)) {
  1405. item = this._getItem(id, convert);
  1406. if (filter(item)) {
  1407. items.push(item);
  1408. }
  1409. }
  1410. }
  1411. this._sort(items, order);
  1412. for (i = 0, len = items.length; i < len; i++) {
  1413. ids[i] = items[i][this.fieldId];
  1414. }
  1415. }
  1416. else {
  1417. // create unordered list
  1418. for (id in data) {
  1419. if (data.hasOwnProperty(id)) {
  1420. item = this._getItem(id, convert);
  1421. if (filter(item)) {
  1422. ids.push(item[this.fieldId]);
  1423. }
  1424. }
  1425. }
  1426. }
  1427. }
  1428. else {
  1429. // get all items
  1430. if (order) {
  1431. // create an ordered list
  1432. items = [];
  1433. for (id in data) {
  1434. if (data.hasOwnProperty(id)) {
  1435. items.push(data[id]);
  1436. }
  1437. }
  1438. this._sort(items, order);
  1439. for (i = 0, len = items.length; i < len; i++) {
  1440. ids[i] = items[i][this.fieldId];
  1441. }
  1442. }
  1443. else {
  1444. // create unordered list
  1445. for (id in data) {
  1446. if (data.hasOwnProperty(id)) {
  1447. item = data[id];
  1448. ids.push(item[this.fieldId]);
  1449. }
  1450. }
  1451. }
  1452. }
  1453. return ids;
  1454. };
  1455. /**
  1456. * Execute a callback function for every item in the dataset.
  1457. * The order of the items is not determined.
  1458. * @param {function} callback
  1459. * @param {Object} [options] Available options:
  1460. * {Object.<String, String>} [convert]
  1461. * {String[]} [fields] filter fields
  1462. * {function} [filter] filter items
  1463. * {String | function} [order] Order the items by
  1464. * a field name or custom sort function.
  1465. */
  1466. DataSet.prototype.forEach = function (callback, options) {
  1467. var filter = options && options.filter,
  1468. convert = options && options.convert || this.options.convert,
  1469. data = this.data,
  1470. item,
  1471. id;
  1472. if (options && options.order) {
  1473. // execute forEach on ordered list
  1474. var items = this.get(options);
  1475. for (var i = 0, len = items.length; i < len; i++) {
  1476. item = items[i];
  1477. id = item[this.fieldId];
  1478. callback(item, id);
  1479. }
  1480. }
  1481. else {
  1482. // unordered
  1483. for (id in data) {
  1484. if (data.hasOwnProperty(id)) {
  1485. item = this._getItem(id, convert);
  1486. if (!filter || filter(item)) {
  1487. callback(item, id);
  1488. }
  1489. }
  1490. }
  1491. }
  1492. };
  1493. /**
  1494. * Map every item in the dataset.
  1495. * @param {function} callback
  1496. * @param {Object} [options] Available options:
  1497. * {Object.<String, String>} [convert]
  1498. * {String[]} [fields] filter fields
  1499. * {function} [filter] filter items
  1500. * {String | function} [order] Order the items by
  1501. * a field name or custom sort function.
  1502. * @return {Object[]} mappedItems
  1503. */
  1504. DataSet.prototype.map = function (callback, options) {
  1505. var filter = options && options.filter,
  1506. convert = options && options.convert || this.options.convert,
  1507. mappedItems = [],
  1508. data = this.data,
  1509. item;
  1510. // convert and filter items
  1511. for (var id in data) {
  1512. if (data.hasOwnProperty(id)) {
  1513. item = this._getItem(id, convert);
  1514. if (!filter || filter(item)) {
  1515. mappedItems.push(callback(item, id));
  1516. }
  1517. }
  1518. }
  1519. // order items
  1520. if (options && options.order) {
  1521. this._sort(mappedItems, options.order);
  1522. }
  1523. return mappedItems;
  1524. };
  1525. /**
  1526. * Filter the fields of an item
  1527. * @param {Object} item
  1528. * @param {String[]} fields Field names
  1529. * @return {Object} filteredItem
  1530. * @private
  1531. */
  1532. DataSet.prototype._filterFields = function (item, fields) {
  1533. var filteredItem = {};
  1534. for (var field in item) {
  1535. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1536. filteredItem[field] = item[field];
  1537. }
  1538. }
  1539. return filteredItem;
  1540. };
  1541. /**
  1542. * Sort the provided array with items
  1543. * @param {Object[]} items
  1544. * @param {String | function} order A field name or custom sort function.
  1545. * @private
  1546. */
  1547. DataSet.prototype._sort = function (items, order) {
  1548. if (util.isString(order)) {
  1549. // order by provided field name
  1550. var name = order; // field name
  1551. items.sort(function (a, b) {
  1552. var av = a[name];
  1553. var bv = b[name];
  1554. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1555. });
  1556. }
  1557. else if (typeof order === 'function') {
  1558. // order by sort function
  1559. items.sort(order);
  1560. }
  1561. // TODO: extend order by an Object {field:String, direction:String}
  1562. // where direction can be 'asc' or 'desc'
  1563. else {
  1564. throw new TypeError('Order must be a function or a string');
  1565. }
  1566. };
  1567. /**
  1568. * Remove an object by pointer or by id
  1569. * @param {String | Number | Object | Array} id Object or id, or an array with
  1570. * objects or ids to be removed
  1571. * @param {String} [senderId] Optional sender id
  1572. * @return {Array} removedIds
  1573. */
  1574. DataSet.prototype.remove = function (id, senderId) {
  1575. var removedIds = [],
  1576. i, len, removedId;
  1577. if (id instanceof Array) {
  1578. for (i = 0, len = id.length; i < len; i++) {
  1579. removedId = this._remove(id[i]);
  1580. if (removedId != null) {
  1581. removedIds.push(removedId);
  1582. }
  1583. }
  1584. }
  1585. else {
  1586. removedId = this._remove(id);
  1587. if (removedId != null) {
  1588. removedIds.push(removedId);
  1589. }
  1590. }
  1591. if (removedIds.length) {
  1592. this._trigger('remove', {items: removedIds}, senderId);
  1593. }
  1594. return removedIds;
  1595. };
  1596. /**
  1597. * Remove an item by its id
  1598. * @param {Number | String | Object} id id or item
  1599. * @returns {Number | String | null} id
  1600. * @private
  1601. */
  1602. DataSet.prototype._remove = function (id) {
  1603. if (util.isNumber(id) || util.isString(id)) {
  1604. if (this.data[id]) {
  1605. delete this.data[id];
  1606. delete this.internalIds[id];
  1607. return id;
  1608. }
  1609. }
  1610. else if (id instanceof Object) {
  1611. var itemId = id[this.fieldId];
  1612. if (itemId && this.data[itemId]) {
  1613. delete this.data[itemId];
  1614. delete this.internalIds[itemId];
  1615. return itemId;
  1616. }
  1617. }
  1618. return null;
  1619. };
  1620. /**
  1621. * Clear the data
  1622. * @param {String} [senderId] Optional sender id
  1623. * @return {Array} removedIds The ids of all removed items
  1624. */
  1625. DataSet.prototype.clear = function (senderId) {
  1626. var ids = Object.keys(this.data);
  1627. this.data = {};
  1628. this.internalIds = {};
  1629. this._trigger('remove', {items: ids}, senderId);
  1630. return ids;
  1631. };
  1632. /**
  1633. * Find the item with maximum value of a specified field
  1634. * @param {String} field
  1635. * @return {Object | null} item Item containing max value, or null if no items
  1636. */
  1637. DataSet.prototype.max = function (field) {
  1638. var data = this.data,
  1639. max = null,
  1640. maxField = null;
  1641. for (var id in data) {
  1642. if (data.hasOwnProperty(id)) {
  1643. var item = data[id];
  1644. var itemField = item[field];
  1645. if (itemField != null && (!max || itemField > maxField)) {
  1646. max = item;
  1647. maxField = itemField;
  1648. }
  1649. }
  1650. }
  1651. return max;
  1652. };
  1653. /**
  1654. * Find the item with minimum value of a specified field
  1655. * @param {String} field
  1656. * @return {Object | null} item Item containing max value, or null if no items
  1657. */
  1658. DataSet.prototype.min = function (field) {
  1659. var data = this.data,
  1660. min = null,
  1661. minField = null;
  1662. for (var id in data) {
  1663. if (data.hasOwnProperty(id)) {
  1664. var item = data[id];
  1665. var itemField = item[field];
  1666. if (itemField != null && (!min || itemField < minField)) {
  1667. min = item;
  1668. minField = itemField;
  1669. }
  1670. }
  1671. }
  1672. return min;
  1673. };
  1674. /**
  1675. * Find all distinct values of a specified field
  1676. * @param {String} field
  1677. * @return {Array} values Array containing all distinct values. If the data
  1678. * items do not contain the specified field, an array
  1679. * containing a single value undefined is returned.
  1680. * The returned array is unordered.
  1681. */
  1682. DataSet.prototype.distinct = function (field) {
  1683. var data = this.data,
  1684. values = [],
  1685. fieldType = this.options.convert[field],
  1686. count = 0;
  1687. for (var prop in data) {
  1688. if (data.hasOwnProperty(prop)) {
  1689. var item = data[prop];
  1690. var value = util.convert(item[field], fieldType);
  1691. var exists = false;
  1692. for (var i = 0; i < count; i++) {
  1693. if (values[i] == value) {
  1694. exists = true;
  1695. break;
  1696. }
  1697. }
  1698. if (!exists) {
  1699. values[count] = value;
  1700. count++;
  1701. }
  1702. }
  1703. }
  1704. return values;
  1705. };
  1706. /**
  1707. * Add a single item. Will fail when an item with the same id already exists.
  1708. * @param {Object} item
  1709. * @return {String} id
  1710. * @private
  1711. */
  1712. DataSet.prototype._addItem = function (item) {
  1713. var id = item[this.fieldId];
  1714. if (id != undefined) {
  1715. // check whether this id is already taken
  1716. if (this.data[id]) {
  1717. // item already exists
  1718. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1719. }
  1720. }
  1721. else {
  1722. // generate an id
  1723. id = util.randomUUID();
  1724. item[this.fieldId] = id;
  1725. this.internalIds[id] = item;
  1726. }
  1727. var d = {};
  1728. for (var field in item) {
  1729. if (item.hasOwnProperty(field)) {
  1730. var fieldType = this.convert[field]; // type may be undefined
  1731. d[field] = util.convert(item[field], fieldType);
  1732. }
  1733. }
  1734. this.data[id] = d;
  1735. return id;
  1736. };
  1737. /**
  1738. * Get an item. Fields can be converted to a specific type
  1739. * @param {String} id
  1740. * @param {Object.<String, String>} [convert] field types to convert
  1741. * @return {Object | null} item
  1742. * @private
  1743. */
  1744. DataSet.prototype._getItem = function (id, convert) {
  1745. var field, value;
  1746. // get the item from the dataset
  1747. var raw = this.data[id];
  1748. if (!raw) {
  1749. return null;
  1750. }
  1751. // convert the items field types
  1752. var converted = {},
  1753. fieldId = this.fieldId,
  1754. internalIds = this.internalIds;
  1755. if (convert) {
  1756. for (field in raw) {
  1757. if (raw.hasOwnProperty(field)) {
  1758. value = raw[field];
  1759. // output all fields, except internal ids
  1760. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1761. converted[field] = util.convert(value, convert[field]);
  1762. }
  1763. }
  1764. }
  1765. }
  1766. else {
  1767. // no field types specified, no converting needed
  1768. for (field in raw) {
  1769. if (raw.hasOwnProperty(field)) {
  1770. value = raw[field];
  1771. // output all fields, except internal ids
  1772. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1773. converted[field] = value;
  1774. }
  1775. }
  1776. }
  1777. }
  1778. return converted;
  1779. };
  1780. /**
  1781. * Update a single item: merge with existing item.
  1782. * Will fail when the item has no id, or when there does not exist an item
  1783. * with the same id.
  1784. * @param {Object} item
  1785. * @return {String} id
  1786. * @private
  1787. */
  1788. DataSet.prototype._updateItem = function (item) {
  1789. var id = item[this.fieldId];
  1790. if (id == undefined) {
  1791. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1792. }
  1793. var d = this.data[id];
  1794. if (!d) {
  1795. // item doesn't exist
  1796. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1797. }
  1798. // merge with current item
  1799. for (var field in item) {
  1800. if (item.hasOwnProperty(field)) {
  1801. var fieldType = this.convert[field]; // type may be undefined
  1802. d[field] = util.convert(item[field], fieldType);
  1803. }
  1804. }
  1805. return id;
  1806. };
  1807. /**
  1808. * check if an id is an internal or external id
  1809. * @param id
  1810. * @returns {boolean}
  1811. * @private
  1812. */
  1813. DataSet.prototype.isInternalId = function(id) {
  1814. return (id in this.internalIds);
  1815. };
  1816. /**
  1817. * Get an array with the column names of a Google DataTable
  1818. * @param {DataTable} dataTable
  1819. * @return {String[]} columnNames
  1820. * @private
  1821. */
  1822. DataSet.prototype._getColumnNames = function (dataTable) {
  1823. var columns = [];
  1824. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1825. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1826. }
  1827. return columns;
  1828. };
  1829. /**
  1830. * Append an item as a row to the dataTable
  1831. * @param dataTable
  1832. * @param columns
  1833. * @param item
  1834. * @private
  1835. */
  1836. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1837. var row = dataTable.addRow();
  1838. for (var col = 0, cols = columns.length; col < cols; col++) {
  1839. var field = columns[col];
  1840. dataTable.setValue(row, col, item[field]);
  1841. }
  1842. };
  1843. /**
  1844. * DataView
  1845. *
  1846. * a dataview offers a filtered view on a dataset or an other dataview.
  1847. *
  1848. * @param {DataSet | DataView} data
  1849. * @param {Object} [options] Available options: see method get
  1850. *
  1851. * @constructor DataView
  1852. */
  1853. function DataView (data, options) {
  1854. this.id = util.randomUUID();
  1855. this.data = null;
  1856. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1857. this.options = options || {};
  1858. this.fieldId = 'id'; // name of the field containing id
  1859. this.subscribers = {}; // event subscribers
  1860. var me = this;
  1861. this.listener = function () {
  1862. me._onEvent.apply(me, arguments);
  1863. };
  1864. this.setData(data);
  1865. }
  1866. // TODO: implement a function .config() to dynamically update things like configured filter
  1867. // and trigger changes accordingly
  1868. /**
  1869. * Set a data source for the view
  1870. * @param {DataSet | DataView} data
  1871. */
  1872. DataView.prototype.setData = function (data) {
  1873. var ids, dataItems, i, len;
  1874. if (this.data) {
  1875. // unsubscribe from current dataset
  1876. if (this.data.unsubscribe) {
  1877. this.data.unsubscribe('*', this.listener);
  1878. }
  1879. // trigger a remove of all items in memory
  1880. ids = [];
  1881. for (var id in this.ids) {
  1882. if (this.ids.hasOwnProperty(id)) {
  1883. ids.push(id);
  1884. }
  1885. }
  1886. this.ids = {};
  1887. this._trigger('remove', {items: ids});
  1888. }
  1889. this.data = data;
  1890. if (this.data) {
  1891. // update fieldId
  1892. this.fieldId = this.options.fieldId ||
  1893. (this.data && this.data.options && this.data.options.fieldId) ||
  1894. 'id';
  1895. // trigger an add of all added items
  1896. ids = this.data.getIds({filter: this.options && this.options.filter});
  1897. for (i = 0, len = ids.length; i < len; i++) {
  1898. id = ids[i];
  1899. this.ids[id] = true;
  1900. }
  1901. this._trigger('add', {items: ids});
  1902. // subscribe to new dataset
  1903. if (this.data.on) {
  1904. this.data.on('*', this.listener);
  1905. }
  1906. }
  1907. };
  1908. /**
  1909. * Get data from the data view
  1910. *
  1911. * Usage:
  1912. *
  1913. * get()
  1914. * get(options: Object)
  1915. * get(options: Object, data: Array | DataTable)
  1916. *
  1917. * get(id: Number)
  1918. * get(id: Number, options: Object)
  1919. * get(id: Number, options: Object, data: Array | DataTable)
  1920. *
  1921. * get(ids: Number[])
  1922. * get(ids: Number[], options: Object)
  1923. * get(ids: Number[], options: Object, data: Array | DataTable)
  1924. *
  1925. * Where:
  1926. *
  1927. * {Number | String} id The id of an item
  1928. * {Number[] | String{}} ids An array with ids of items
  1929. * {Object} options An Object with options. Available options:
  1930. * {String} [type] Type of data to be returned. Can
  1931. * be 'DataTable' or 'Array' (default)
  1932. * {Object.<String, String>} [convert]
  1933. * {String[]} [fields] field names to be returned
  1934. * {function} [filter] filter items
  1935. * {String | function} [order] Order the items by
  1936. * a field name or custom sort function.
  1937. * {Array | DataTable} [data] If provided, items will be appended to this
  1938. * array or table. Required in case of Google
  1939. * DataTable.
  1940. * @param args
  1941. */
  1942. DataView.prototype.get = function (args) {
  1943. var me = this;
  1944. // parse the arguments
  1945. var ids, options, data;
  1946. var firstType = util.getType(arguments[0]);
  1947. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  1948. // get(id(s) [, options] [, data])
  1949. ids = arguments[0]; // can be a single id or an array with ids
  1950. options = arguments[1];
  1951. data = arguments[2];
  1952. }
  1953. else {
  1954. // get([, options] [, data])
  1955. options = arguments[0];
  1956. data = arguments[1];
  1957. }
  1958. // extend the options with the default options and provided options
  1959. var viewOptions = util.extend({}, this.options, options);
  1960. // create a combined filter method when needed
  1961. if (this.options.filter && options && options.filter) {
  1962. viewOptions.filter = function (item) {
  1963. return me.options.filter(item) && options.filter(item);
  1964. }
  1965. }
  1966. // build up the call to the linked data set
  1967. var getArguments = [];
  1968. if (ids != undefined) {
  1969. getArguments.push(ids);
  1970. }
  1971. getArguments.push(viewOptions);
  1972. getArguments.push(data);
  1973. return this.data && this.data.get.apply(this.data, getArguments);
  1974. };
  1975. /**
  1976. * Get ids of all items or from a filtered set of items.
  1977. * @param {Object} [options] An Object with options. Available options:
  1978. * {function} [filter] filter items
  1979. * {String | function} [order] Order the items by
  1980. * a field name or custom sort function.
  1981. * @return {Array} ids
  1982. */
  1983. DataView.prototype.getIds = function (options) {
  1984. var ids;
  1985. if (this.data) {
  1986. var defaultFilter = this.options.filter;
  1987. var filter;
  1988. if (options && options.filter) {
  1989. if (defaultFilter) {
  1990. filter = function (item) {
  1991. return defaultFilter(item) && options.filter(item);
  1992. }
  1993. }
  1994. else {
  1995. filter = options.filter;
  1996. }
  1997. }
  1998. else {
  1999. filter = defaultFilter;
  2000. }
  2001. ids = this.data.getIds({
  2002. filter: filter,
  2003. order: options && options.order
  2004. });
  2005. }
  2006. else {
  2007. ids = [];
  2008. }
  2009. return ids;
  2010. };
  2011. /**
  2012. * Event listener. Will propagate all events from the connected data set to
  2013. * the subscribers of the DataView, but will filter the items and only trigger
  2014. * when there are changes in the filtered data set.
  2015. * @param {String} event
  2016. * @param {Object | null} params
  2017. * @param {String} senderId
  2018. * @private
  2019. */
  2020. DataView.prototype._onEvent = function (event, params, senderId) {
  2021. var i, len, id, item,
  2022. ids = params && params.items,
  2023. data = this.data,
  2024. added = [],
  2025. updated = [],
  2026. removed = [];
  2027. if (ids && data) {
  2028. switch (event) {
  2029. case 'add':
  2030. // filter the ids of the added items
  2031. for (i = 0, len = ids.length; i < len; i++) {
  2032. id = ids[i];
  2033. item = this.get(id);
  2034. if (item) {
  2035. this.ids[id] = true;
  2036. added.push(id);
  2037. }
  2038. }
  2039. break;
  2040. case 'update':
  2041. // determine the event from the views viewpoint: an updated
  2042. // item can be added, updated, or removed from this view.
  2043. for (i = 0, len = ids.length; i < len; i++) {
  2044. id = ids[i];
  2045. item = this.get(id);
  2046. if (item) {
  2047. if (this.ids[id]) {
  2048. updated.push(id);
  2049. }
  2050. else {
  2051. this.ids[id] = true;
  2052. added.push(id);
  2053. }
  2054. }
  2055. else {
  2056. if (this.ids[id]) {
  2057. delete this.ids[id];
  2058. removed.push(id);
  2059. }
  2060. else {
  2061. // nothing interesting for me :-(
  2062. }
  2063. }
  2064. }
  2065. break;
  2066. case 'remove':
  2067. // filter the ids of the removed items
  2068. for (i = 0, len = ids.length; i < len; i++) {
  2069. id = ids[i];
  2070. if (this.ids[id]) {
  2071. delete this.ids[id];
  2072. removed.push(id);
  2073. }
  2074. }
  2075. break;
  2076. }
  2077. if (added.length) {
  2078. this._trigger('add', {items: added}, senderId);
  2079. }
  2080. if (updated.length) {
  2081. this._trigger('update', {items: updated}, senderId);
  2082. }
  2083. if (removed.length) {
  2084. this._trigger('remove', {items: removed}, senderId);
  2085. }
  2086. }
  2087. };
  2088. // copy subscription functionality from DataSet
  2089. DataView.prototype.on = DataSet.prototype.on;
  2090. DataView.prototype.off = DataSet.prototype.off;
  2091. DataView.prototype._trigger = DataSet.prototype._trigger;
  2092. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2093. DataView.prototype.subscribe = DataView.prototype.on;
  2094. DataView.prototype.unsubscribe = DataView.prototype.off;
  2095. /**
  2096. * Created by Alex on 2/27/14.
  2097. */
  2098. function SvgAxis (range,mainId, constants) {
  2099. this.svgId = mainId;
  2100. this.range = range;
  2101. this.constants = constants;
  2102. this.duration = this.range.end - this.range.start; // in milliseconds
  2103. this.minColumnWidth = 100;
  2104. this._drawElements();
  2105. this._update();
  2106. }
  2107. SvgAxis.prototype._drawElements = function() {
  2108. d3.select(this.svgId)
  2109. .append("rect")
  2110. .attr("id","bars")
  2111. .attr("x",0)
  2112. .attr("y",0)
  2113. .attr("width", this.constants.width)
  2114. .attr("height",this.constants.barHeight)
  2115. .style("stroke", "rgb(6,120,155)");
  2116. this.leftText = d3.select(this.svgId)
  2117. .append("text")
  2118. .attr("x", 5)
  2119. .attr("y", 20)
  2120. .attr("font-size", 14)
  2121. .text(moment(this.range.start));
  2122. this.rightText = d3.select(this.svgId)
  2123. .append("text")
  2124. .attr("y", 20)
  2125. .attr("font-size", 14)
  2126. .text(moment(this.range.end))
  2127. this.rightText.attr("x", this.constants.width - 5 - this.rightText.node().getBBox().width);
  2128. this.dateLabels = {};
  2129. this.markerLines = {};
  2130. }
  2131. SvgAxis.prototype._createMarkerLine = function(index) {
  2132. this.markerLines[index] = {svg:d3.select("svg#main").append("line")
  2133. .attr('y1',0)
  2134. .attr('y2',this.constants.height)
  2135. .style("stroke", "rgb(220,220,220)")
  2136. }
  2137. }
  2138. SvgAxis.prototype._createDateLabel = function(index) {
  2139. this.dateLabels[index] = {svg:d3.select(this.svgId)
  2140. .append("text")
  2141. .attr("font-size",12)
  2142. , active:false};
  2143. }
  2144. SvgAxis.prototype._update = function() {
  2145. this.duration = this.range.end - this.range.start; // in milliseconds
  2146. this.leftText.text(moment(this.range.start).format("DD-MM-YYYY HH:mm:ss"))
  2147. this.rightText.text(moment(this.range.end).format("DD-MM-YYYY"))
  2148. this.rightText.attr("x", this.constants.width - 5 - this.rightText.node().getBBox().width);
  2149. this.msPerPixel = this.duration / this.constants.width;
  2150. this.columnDuration = this.minColumnWidth * this.msPerPixel;
  2151. var milliSecondScale = [1,10,50,100,250,500];
  2152. var secondScale = [1,5,15,30];
  2153. var minuteScale = [1,5,15,30];
  2154. var hourScale = [1,3,6,12];
  2155. var dayScale = [1,2,3,5,10,15];
  2156. var monthScale = [1,2,3,4,5,6];
  2157. var yearScale = [1,2,3,4,5,6,7,8,9,10,15,20,25,50,75,100,150,250,500,1000];
  2158. var multipliers = [1,1000,60000,3600000,24*3600000,30*24*3600000,365*24*3600000];
  2159. var scales = [milliSecondScale,secondScale,minuteScale,hourScale,dayScale,monthScale,yearScale]
  2160. var formats = ["SSS","mm:ss","hh:mm:ss","DD HH:mm","DD-MM","MM-YYYY","YYYY"]
  2161. var indices = this._getAppropriateScale(scales,multipliers);
  2162. var scale = scales[indices[0]][indices[1]] * multipliers[indices[0]];
  2163. var dateCorrection = (this.range.start.valueOf() % scale) +3600000;
  2164. for (var i = 0; i < 30; i++) {
  2165. var date = this.range.start + i*scale - dateCorrection;
  2166. if (((i+1)*scale - dateCorrection)/this.msPerPixel > this.constants.width + 200) {
  2167. if (this.dateLabels.hasOwnProperty(i)) {
  2168. this.dateLabels[i].svg.remove();
  2169. delete this.dateLabels[i]
  2170. }
  2171. if (this.markerLines.hasOwnProperty(i)) {
  2172. this.markerLines[i].svg.remove();
  2173. delete this.markerLines[i]
  2174. }
  2175. }
  2176. else {
  2177. if (!this.dateLabels.hasOwnProperty(i)) {
  2178. this._createDateLabel(i);
  2179. }
  2180. if (!this.markerLines.hasOwnProperty(i)) {
  2181. this._createMarkerLine(i);
  2182. }
  2183. this.dateLabels[i].svg.text(moment(date).format(formats[indices[0]]))
  2184. .attr("x",(i*scale - dateCorrection)/this.msPerPixel)
  2185. .attr("y",50)
  2186. this.markerLines[i].svg.attr("x1",(i*scale - dateCorrection)/this.msPerPixel)
  2187. .attr("x2",(i*scale - dateCorrection)/this.msPerPixel)
  2188. }
  2189. }
  2190. }
  2191. SvgAxis.prototype._getAppropriateScale = function(scales,multipliers) {
  2192. for (var i = 0; i < scales.length; i++) {
  2193. for (var j = 0; j < scales[i].length; j++) {
  2194. if (scales[i][j] * multipliers[i] > this.columnDuration) {
  2195. return [i,j]
  2196. }
  2197. }
  2198. }
  2199. }
  2200. /**
  2201. * @constructor SvgTimeline
  2202. * Create a graph visualization, displaying nodes and edges.
  2203. *
  2204. * @param {Element} container The DOM element in which the Graph will
  2205. * be created. Normally a div element.
  2206. * @param {Object} items An object containing parameters
  2207. * {Array} nodes
  2208. * {Array} edges
  2209. * @param {Object} options Options
  2210. */
  2211. function SvgTimeline (container, items, options) {
  2212. this.constants = {
  2213. width:1400,
  2214. height:400,
  2215. barHeight: 60
  2216. }
  2217. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  2218. this.range = {
  2219. start:now.clone().add('days', -3).valueOf(),
  2220. end: now.clone().add('days', 4).valueOf()
  2221. }
  2222. this.items = {};
  2223. this.sortedItems = [];
  2224. this.activeItems = {};
  2225. this.sortedActiveItems = [];
  2226. this._createItems(items);
  2227. this.container = container;
  2228. this._createSVG();
  2229. this.axis = new SvgAxis(this.range,"svg#main",this.constants);
  2230. var me = this;
  2231. this.hammer = Hammer(document.getElementById("main"), {
  2232. prevent_default: true
  2233. });
  2234. this.hammer.on('tap', me._onTap.bind(me) );
  2235. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  2236. this.hammer.on('hold', me._onHold.bind(me) );
  2237. this.hammer.on('pinch', me._onPinch.bind(me) );
  2238. this.hammer.on('touch', me._onTouch.bind(me) );
  2239. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  2240. this.hammer.on('drag', me._onDrag.bind(me) );
  2241. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  2242. this.hammer.on('release', me._onRelease.bind(me) );
  2243. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  2244. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  2245. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  2246. //this._drawLines();
  2247. this._update();
  2248. }
  2249. SvgTimeline.prototype._createSVG = function() {
  2250. d3.select("div#visualization")
  2251. .append("svg").attr("id","main")
  2252. .attr("width",this.constants.width)
  2253. .attr("height",this.constants.height)
  2254. .attr("style","border:1px solid black")
  2255. };
  2256. SvgTimeline.prototype._createItems = function (items) {
  2257. for (var i = 0; i < items.length; i++) {
  2258. this.items[items[i].id] = new Item(items[i], this.constants);
  2259. this.sortedItems.push(this.items[items[i].id]);
  2260. }
  2261. this._sortItems(this.sortedItems);
  2262. }
  2263. SvgTimeline.prototype._sortItems = function (items) {
  2264. items.sort(function(a,b) {return a.start - b.start});
  2265. }
  2266. SvgTimeline.prototype._getPointer = function (touch) {
  2267. return {
  2268. x: touch.pageX,
  2269. y: touch.pageY
  2270. };
  2271. };
  2272. SvgTimeline.prototype._onTap = function() {};
  2273. SvgTimeline.prototype._onDoubleTap = function() {};
  2274. SvgTimeline.prototype._onHold = function() {};
  2275. SvgTimeline.prototype._onPinch = function() {};
  2276. SvgTimeline.prototype._onTouch = function(event) {};
  2277. SvgTimeline.prototype._onDragStart = function(event) {
  2278. this.initialDragPos = this._getPointer(event.gesture.center);
  2279. };
  2280. SvgTimeline.prototype._onDrag = function(event) {
  2281. var pointer = this._getPointer(event.gesture.center);
  2282. var diffX = pointer.x - this.initialDragPos.x;
  2283. // var diffY = pointer.y - this.initialDragPos.y;
  2284. this.initialDragPos = pointer;
  2285. this.range.start -= diffX * this.axis.msPerPixel;
  2286. this.range.end -= diffX * this.axis.msPerPixel;
  2287. this._update();
  2288. };
  2289. SvgTimeline.prototype._onDragEnd = function() {};
  2290. SvgTimeline.prototype._onRelease = function() {};
  2291. SvgTimeline.prototype._onMouseWheel = function(event) {
  2292. var delta = 0;
  2293. if (event.wheelDelta) { /* IE/Opera. */
  2294. delta = event.wheelDelta/120;
  2295. }
  2296. else if (event.detail) { /* Mozilla case. */
  2297. // In Mozilla, sign of delta is different than in IE.
  2298. // Also, delta is multiple of 3.
  2299. delta = -event.detail/3;
  2300. }
  2301. if (delta) {
  2302. var pointer = {x:event.x, y:event.y}
  2303. var center = this.range.start + this.axis.duration * 0.5;
  2304. var zoomSpeed = 0.1;
  2305. var scrollSpeed = 0.1;
  2306. this.range.start = center - 0.5*(this.axis.duration * (1 - delta*zoomSpeed));
  2307. this.range.end = this.range.start + (this.axis.duration * (1 - delta*zoomSpeed));
  2308. var diffX = delta*(pointer.x - 0.5*this.constants.width);
  2309. // var diffY = pointer.y - this.initialDragPos.y;
  2310. this.range.start -= diffX * this.axis.msPerPixel * scrollSpeed;
  2311. this.range.end -= diffX * this.axis.msPerPixel * scrollSpeed;
  2312. this._update();
  2313. }
  2314. };
  2315. SvgTimeline.prototype._onMouseMoveTitle = function() {};
  2316. SvgTimeline.prototype._update = function() {
  2317. this.axis._update();
  2318. this._getActiveItems();
  2319. this._updateItems();
  2320. };
  2321. SvgTimeline.prototype._getActiveItems = function() {
  2322. // reset all currently active items to inactive
  2323. for (var itemId in this.activeItems) {
  2324. if (this.activeItems.hasOwnProperty(itemId)) {
  2325. this.activeItems[itemId].active = false;
  2326. }
  2327. }
  2328. this.sortedActiveItems = []
  2329. var rangeStart = this.range.start-200*this.axis.msPerPixel
  2330. var rangeEnd = (this.range.end+200*this.axis.msPerPixel)
  2331. for (var itemId in this.items) {
  2332. if (this.items.hasOwnProperty(itemId)) {
  2333. if (this.items[itemId].start >= rangeStart && this.items[itemId].start < rangeEnd ||
  2334. this.items[itemId].end >= rangeStart && this.items[itemId].end < rangeEnd) {
  2335. if (this.items[itemId].active == false) {
  2336. this.activeItems[itemId] = this.items[itemId];
  2337. }
  2338. this.activeItems[itemId].active = true;
  2339. this.sortedActiveItems.push(this.activeItems[itemId]);
  2340. }
  2341. }
  2342. }
  2343. this._sortItems(this.sortedActiveItems);
  2344. // cleanup
  2345. for (var itemId in this.activeItems) {
  2346. if (this.activeItems.hasOwnProperty(itemId)) {
  2347. if (this.activeItems[itemId].active == false) {
  2348. this.activeItems[itemId].svg.remove();
  2349. this.activeItems[itemId].svg = null;
  2350. this.activeItems[itemId].svgLine.remove();
  2351. this.activeItems[itemId].svgLine = null;
  2352. delete this.activeItems[itemId];
  2353. }
  2354. }
  2355. }
  2356. };
  2357. SvgTimeline.prototype._updateItems = function() {
  2358. for (var i = 0; i < this.sortedActiveItems.length; i++) {
  2359. var item = this.sortedActiveItems[i];
  2360. if (item.svg == null) {
  2361. // item.svg = d3.select("svg#main")
  2362. // .append("rect")
  2363. // .attr("class","item")
  2364. // .style("stroke", "rgb(6,120,155)")
  2365. // .style("fill", "rgb(6,120,155)");
  2366. item.svg = d3.select("svg#main")
  2367. .append("foreignObject")
  2368. item.svgContent = item.svg.append("xhtml:body")
  2369. .style("font", "14px 'Helvetica Neue'")
  2370. .style("background-color", "#ff00ff")
  2371. .html("<h1>An HTML Foreign Object in SVG</h1><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eu enim quam. Quisque nisi risus, sagittis quis tempor nec, aliquam eget neque. Nulla bibendum semper lorem non ullamcorper. Nulla non ligula lorem. Praesent porttitor, tellus nec suscipit aliquam, enim elit posuere lorem, at laoreet enim ligula sed tortor. Ut sodales, urna a aliquam semper, nibh diam gravida sapien, sit amet fermentum purus lacus eget massa. Donec ac arcu vel magna consequat pretium et vel ligula. Donec sit amet erat elit. Vivamus eu metus eget est hendrerit rutrum. Curabitur vitae orci et leo interdum egestas ut sit amet dui. In varius enim ut sem posuere in tristique metus ultrices.<p>Integer mollis massa at orci porta vestibulum. Pellentesque dignissim turpis ut tortor ultricies condimentum et quis nibh. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer euismod lorem vulputate dui pharetra luctus. Sed vulputate, nunc quis porttitor scelerisque, dui est varius ipsum, eu blandit mauris nibh pellentesque tortor. Vivamus ultricies ante eget ipsum pulvinar ac tempor turpis mollis. Morbi tortor orci, euismod vel sagittis ac, lobortis nec est. Quisque euismod venenatis felis at dapibus. Vestibulum dignissim nulla ut nisi tristique porttitor. Proin et nunc id arcu cursus dapibus non quis libero. Nunc ligula mi, bibendum non mattis nec, luctus id neque. Suspendisse ut eros lacus. Praesent eget lacus eget risus congue vestibulum. Morbi tincidunt pulvinar lacus sed faucibus. Phasellus sed vestibulum sapien.");
  2372. if (item.end == 0) {
  2373. item.svgLine = d3.select("svg#main")
  2374. .append("line")
  2375. .attr("y1",this.constants.barHeight)
  2376. .style("stroke", "rgb(200,200,255)")
  2377. .style("stroke-width", 3)
  2378. }
  2379. }
  2380. item.svg.attr('width',item.getLength(this.axis.msPerPixel))
  2381. .attr("x",this._getXforItem(item))
  2382. .attr("y",this._getYforItem(item, i))
  2383. .attr('height',25)
  2384. if (item.end == 0) {
  2385. item.svgLine.attr('y2',item.y)
  2386. .attr('x1',item.timeX)
  2387. .attr('x2',item.timeX)
  2388. }
  2389. }
  2390. };
  2391. SvgTimeline.prototype._getXforItem = function(item) {
  2392. item.timeX = (item.start - this.range.start)/this.axis.msPerPixel;
  2393. if (item.end == 0) {
  2394. item.drawX = item.timeX - item.width * 0.5;
  2395. }
  2396. else {
  2397. item.drawX = item.timeX;
  2398. }
  2399. return item.drawX;
  2400. }
  2401. SvgTimeline.prototype._getYforItem = function(item, index) {
  2402. var bounds = 10;
  2403. var startIndex = Math.max(0,index-bounds);
  2404. item.level = 0;
  2405. for (var i = startIndex; i < index; i++) {
  2406. var item2 = this.sortedActiveItems[i];
  2407. if (item.drawX <= (item2.drawX + item2.width + 5) && item2.level == item.level) {
  2408. item.level += 1;
  2409. }
  2410. }
  2411. item.y = 100 + 50*item.level;
  2412. return item.y;
  2413. }
  2414. /**
  2415. * @constructor TimeStep
  2416. * The class TimeStep is an iterator for dates. You provide a start date and an
  2417. * end date. The class itself determines the best scale (step size) based on the
  2418. * provided start Date, end Date, and minimumStep.
  2419. *
  2420. * If minimumStep is provided, the step size is chosen as close as possible
  2421. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2422. * provided, the scale is set to 1 DAY.
  2423. * The minimumStep should correspond with the onscreen size of about 6 characters
  2424. *
  2425. * Alternatively, you can set a scale by hand.
  2426. * After creation, you can initialize the class by executing first(). Then you
  2427. * can iterate from the start date to the end date via next(). You can check if
  2428. * the end date is reached with the function hasNext(). After each step, you can
  2429. * retrieve the current date via getCurrent().
  2430. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2431. * days, to years.
  2432. *
  2433. * Version: 1.2
  2434. *
  2435. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2436. * or new Date(2010, 9, 21, 23, 45, 00)
  2437. * @param {Date} [end] The end date
  2438. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2439. */
  2440. TimeStep = function(start, end, minimumStep) {
  2441. // variables
  2442. this.current = new Date();
  2443. this._start = new Date();
  2444. this._end = new Date();
  2445. this.autoScale = true;
  2446. this.scale = TimeStep.SCALE.DAY;
  2447. this.step = 1;
  2448. // initialize the range
  2449. this.setRange(start, end, minimumStep);
  2450. };
  2451. /// enum scale
  2452. TimeStep.SCALE = {
  2453. MILLISECOND: 1,
  2454. SECOND: 2,
  2455. MINUTE: 3,
  2456. HOUR: 4,
  2457. DAY: 5,
  2458. WEEKDAY: 6,
  2459. MONTH: 7,
  2460. YEAR: 8
  2461. };
  2462. /**
  2463. * Set a new range
  2464. * If minimumStep is provided, the step size is chosen as close as possible
  2465. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2466. * provided, the scale is set to 1 DAY.
  2467. * The minimumStep should correspond with the onscreen size of about 6 characters
  2468. * @param {Date} [start] The start date and time.
  2469. * @param {Date} [end] The end date and time.
  2470. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2471. */
  2472. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2473. if (!(start instanceof Date) || !(end instanceof Date)) {
  2474. throw "No legal start or end date in method setRange";
  2475. }
  2476. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2477. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2478. if (this.autoScale) {
  2479. this.setMinimumStep(minimumStep);
  2480. }
  2481. };
  2482. /**
  2483. * Set the range iterator to the start date.
  2484. */
  2485. TimeStep.prototype.first = function() {
  2486. this.current = new Date(this._start.valueOf());
  2487. this.roundToMinor();
  2488. };
  2489. /**
  2490. * Round the current date to the first minor date value
  2491. * This must be executed once when the current date is set to start Date
  2492. */
  2493. TimeStep.prototype.roundToMinor = function() {
  2494. // round to floor
  2495. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2496. //noinspection FallthroughInSwitchStatementJS
  2497. switch (this.scale) {
  2498. case TimeStep.SCALE.YEAR:
  2499. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2500. this.current.setMonth(0);
  2501. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2502. case TimeStep.SCALE.DAY: // intentional fall through
  2503. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2504. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2505. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2506. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2507. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2508. }
  2509. if (this.step != 1) {
  2510. // round down to the first minor value that is a multiple of the current step size
  2511. switch (this.scale) {
  2512. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2513. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2514. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2515. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2516. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2517. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2518. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2519. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2520. default: break;
  2521. }
  2522. }
  2523. };
  2524. /**
  2525. * Check if the there is a next step
  2526. * @return {boolean} true if the current date has not passed the end date
  2527. */
  2528. TimeStep.prototype.hasNext = function () {
  2529. return (this.current.valueOf() <= this._end.valueOf());
  2530. };
  2531. /**
  2532. * Do the next step
  2533. */
  2534. TimeStep.prototype.next = function() {
  2535. var prev = this.current.valueOf();
  2536. // Two cases, needed to prevent issues with switching daylight savings
  2537. // (end of March and end of October)
  2538. if (this.current.getMonth() < 6) {
  2539. switch (this.scale) {
  2540. case TimeStep.SCALE.MILLISECOND:
  2541. this.current = new Date(this.current.valueOf() + this.step); break;
  2542. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2543. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2544. case TimeStep.SCALE.HOUR:
  2545. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2546. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2547. var h = this.current.getHours();
  2548. this.current.setHours(h - (h % this.step));
  2549. break;
  2550. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2551. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2552. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2553. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2554. default: break;
  2555. }
  2556. }
  2557. else {
  2558. switch (this.scale) {
  2559. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2560. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2561. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2562. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2563. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2564. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2565. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2566. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2567. default: break;
  2568. }
  2569. }
  2570. if (this.step != 1) {
  2571. // round down to the correct major value
  2572. switch (this.scale) {
  2573. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2574. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2575. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2576. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2577. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2578. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2579. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2580. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2581. default: break;
  2582. }
  2583. }
  2584. // safety mechanism: if current time is still unchanged, move to the end
  2585. if (this.current.valueOf() == prev) {
  2586. this.current = new Date(this._end.valueOf());
  2587. }
  2588. };
  2589. /**
  2590. * Get the current datetime
  2591. * @return {Date} current The current date
  2592. */
  2593. TimeStep.prototype.getCurrent = function() {
  2594. return this.current;
  2595. };
  2596. /**
  2597. * Set a custom scale. Autoscaling will be disabled.
  2598. * For example setScale(SCALE.MINUTES, 5) will result
  2599. * in minor steps of 5 minutes, and major steps of an hour.
  2600. *
  2601. * @param {TimeStep.SCALE} newScale
  2602. * A scale. Choose from SCALE.MILLISECOND,
  2603. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2604. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2605. * SCALE.YEAR.
  2606. * @param {Number} newStep A step size, by default 1. Choose for
  2607. * example 1, 2, 5, or 10.
  2608. */
  2609. TimeStep.prototype.setScale = function(newScale, newStep) {
  2610. this.scale = newScale;
  2611. if (newStep > 0) {
  2612. this.step = newStep;
  2613. }
  2614. this.autoScale = false;
  2615. };
  2616. /**
  2617. * Enable or disable autoscaling
  2618. * @param {boolean} enable If true, autoascaling is set true
  2619. */
  2620. TimeStep.prototype.setAutoScale = function (enable) {
  2621. this.autoScale = enable;
  2622. };
  2623. /**
  2624. * Automatically determine the scale that bests fits the provided minimum step
  2625. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2626. */
  2627. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2628. if (minimumStep == undefined) {
  2629. return;
  2630. }
  2631. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2632. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2633. var stepDay = (1000 * 60 * 60 * 24);
  2634. var stepHour = (1000 * 60 * 60);
  2635. var stepMinute = (1000 * 60);
  2636. var stepSecond = (1000);
  2637. var stepMillisecond= (1);
  2638. // find the smallest step that is larger than the provided minimumStep
  2639. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2640. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2641. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2642. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2643. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2644. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2645. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2646. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2647. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2648. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2649. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2650. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2651. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2652. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2653. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2654. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2655. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2656. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2657. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2658. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2659. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2660. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2661. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2662. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2663. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2664. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2665. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2666. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2667. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2668. };
  2669. /**
  2670. * Snap a date to a rounded value.
  2671. * The snap intervals are dependent on the current scale and step.
  2672. * @param {Date} date the date to be snapped.
  2673. * @return {Date} snappedDate
  2674. */
  2675. TimeStep.prototype.snap = function(date) {
  2676. var clone = new Date(date.valueOf());
  2677. if (this.scale == TimeStep.SCALE.YEAR) {
  2678. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2679. clone.setFullYear(Math.round(year / this.step) * this.step);
  2680. clone.setMonth(0);
  2681. clone.setDate(0);
  2682. clone.setHours(0);
  2683. clone.setMinutes(0);
  2684. clone.setSeconds(0);
  2685. clone.setMilliseconds(0);
  2686. }
  2687. else if (this.scale == TimeStep.SCALE.MONTH) {
  2688. if (clone.getDate() > 15) {
  2689. clone.setDate(1);
  2690. clone.setMonth(clone.getMonth() + 1);
  2691. // important: first set Date to 1, after that change the month.
  2692. }
  2693. else {
  2694. clone.setDate(1);
  2695. }
  2696. clone.setHours(0);
  2697. clone.setMinutes(0);
  2698. clone.setSeconds(0);
  2699. clone.setMilliseconds(0);
  2700. }
  2701. else if (this.scale == TimeStep.SCALE.DAY ||
  2702. this.scale == TimeStep.SCALE.WEEKDAY) {
  2703. //noinspection FallthroughInSwitchStatementJS
  2704. switch (this.step) {
  2705. case 5:
  2706. case 2:
  2707. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2708. default:
  2709. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2710. }
  2711. clone.setMinutes(0);
  2712. clone.setSeconds(0);
  2713. clone.setMilliseconds(0);
  2714. }
  2715. else if (this.scale == TimeStep.SCALE.HOUR) {
  2716. switch (this.step) {
  2717. case 4:
  2718. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2719. default:
  2720. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2721. }
  2722. clone.setSeconds(0);
  2723. clone.setMilliseconds(0);
  2724. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2725. //noinspection FallthroughInSwitchStatementJS
  2726. switch (this.step) {
  2727. case 15:
  2728. case 10:
  2729. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2730. clone.setSeconds(0);
  2731. break;
  2732. case 5:
  2733. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2734. default:
  2735. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2736. }
  2737. clone.setMilliseconds(0);
  2738. }
  2739. else if (this.scale == TimeStep.SCALE.SECOND) {
  2740. //noinspection FallthroughInSwitchStatementJS
  2741. switch (this.step) {
  2742. case 15:
  2743. case 10:
  2744. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2745. clone.setMilliseconds(0);
  2746. break;
  2747. case 5:
  2748. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2749. default:
  2750. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2751. }
  2752. }
  2753. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2754. var step = this.step > 5 ? this.step / 2 : 1;
  2755. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2756. }
  2757. return clone;
  2758. };
  2759. /**
  2760. * Check if the current value is a major value (for example when the step
  2761. * is DAY, a major value is each first day of the MONTH)
  2762. * @return {boolean} true if current date is major, else false.
  2763. */
  2764. TimeStep.prototype.isMajor = function() {
  2765. switch (this.scale) {
  2766. case TimeStep.SCALE.MILLISECOND:
  2767. return (this.current.getMilliseconds() == 0);
  2768. case TimeStep.SCALE.SECOND:
  2769. return (this.current.getSeconds() == 0);
  2770. case TimeStep.SCALE.MINUTE:
  2771. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2772. // Note: this is no bug. Major label is equal for both minute and hour scale
  2773. case TimeStep.SCALE.HOUR:
  2774. return (this.current.getHours() == 0);
  2775. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2776. case TimeStep.SCALE.DAY:
  2777. return (this.current.getDate() == 1);
  2778. case TimeStep.SCALE.MONTH:
  2779. return (this.current.getMonth() == 0);
  2780. case TimeStep.SCALE.YEAR:
  2781. return false;
  2782. default:
  2783. return false;
  2784. }
  2785. };
  2786. /**
  2787. * Returns formatted text for the minor axislabel, depending on the current
  2788. * date and the scale. For example when scale is MINUTE, the current time is
  2789. * formatted as "hh:mm".
  2790. * @param {Date} [date] custom date. if not provided, current date is taken
  2791. */
  2792. TimeStep.prototype.getLabelMinor = function(date) {
  2793. if (date == undefined) {
  2794. date = this.current;
  2795. }
  2796. switch (this.scale) {
  2797. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2798. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2799. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2800. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2801. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2802. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2803. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2804. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2805. default: return '';
  2806. }
  2807. };
  2808. /**
  2809. * Returns formatted text for the major axis label, depending on the current
  2810. * date and the scale. For example when scale is MINUTE, the major scale is
  2811. * hours, and the hour will be formatted as "hh".
  2812. * @param {Date} [date] custom date. if not provided, current date is taken
  2813. */
  2814. TimeStep.prototype.getLabelMajor = function(date) {
  2815. if (date == undefined) {
  2816. date = this.current;
  2817. }
  2818. //noinspection FallthroughInSwitchStatementJS
  2819. switch (this.scale) {
  2820. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2821. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2822. case TimeStep.SCALE.MINUTE:
  2823. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2824. case TimeStep.SCALE.WEEKDAY:
  2825. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2826. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2827. case TimeStep.SCALE.YEAR: return '';
  2828. default: return '';
  2829. }
  2830. };
  2831. /**
  2832. * @constructor Stack
  2833. * Stacks items on top of each other.
  2834. * @param {ItemSet} itemset
  2835. * @param {Object} [options]
  2836. */
  2837. function Stack (itemset, options) {
  2838. this.itemset = itemset;
  2839. this.options = options || {};
  2840. this.defaultOptions = {
  2841. order: function (a, b) {
  2842. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2843. // Order: ranges over non-ranges, ranged ordered by width, and
  2844. // lastly ordered by start.
  2845. if (a instanceof ItemRange) {
  2846. if (b instanceof ItemRange) {
  2847. var aInt = (a.data.end - a.data.start);
  2848. var bInt = (b.data.end - b.data.start);
  2849. return (aInt - bInt) || (a.data.start - b.data.start);
  2850. }
  2851. else {
  2852. return -1;
  2853. }
  2854. }
  2855. else {
  2856. if (b instanceof ItemRange) {
  2857. return 1;
  2858. }
  2859. else {
  2860. return (a.data.start - b.data.start);
  2861. }
  2862. }
  2863. },
  2864. margin: {
  2865. item: 10
  2866. }
  2867. };
  2868. this.ordered = []; // ordered items
  2869. }
  2870. /**
  2871. * Set options for the stack
  2872. * @param {Object} options Available options:
  2873. * {ItemSet} itemset
  2874. * {Number} margin
  2875. * {function} order Stacking order
  2876. */
  2877. Stack.prototype.setOptions = function setOptions (options) {
  2878. util.extend(this.options, options);
  2879. // TODO: register on data changes at the connected itemset, and update the changed part only and immediately
  2880. };
  2881. /**
  2882. * Stack the items such that they don't overlap. The items will have a minimal
  2883. * distance equal to options.margin.item.
  2884. */
  2885. Stack.prototype.update = function update() {
  2886. this._order();
  2887. this._stack();
  2888. };
  2889. /**
  2890. * Order the items. If a custom order function has been provided via the options,
  2891. * then this will be used.
  2892. * @private
  2893. */
  2894. Stack.prototype._order = function _order () {
  2895. var items = this.itemset.items;
  2896. if (!items) {
  2897. throw new Error('Cannot stack items: ItemSet does not contain items');
  2898. }
  2899. // TODO: store the sorted items, to have less work later on
  2900. var ordered = [];
  2901. var index = 0;
  2902. // items is a map (no array)
  2903. util.forEach(items, function (item) {
  2904. if (item.visible) {
  2905. ordered[index] = item;
  2906. index++;
  2907. }
  2908. });
  2909. //if a customer stack order function exists, use it.
  2910. var order = this.options.order || this.defaultOptions.order;
  2911. if (!(typeof order === 'function')) {
  2912. throw new Error('Option order must be a function');
  2913. }
  2914. ordered.sort(order);
  2915. this.ordered = ordered;
  2916. };
  2917. /**
  2918. * Adjust vertical positions of the events such that they don't overlap each
  2919. * other.
  2920. * @private
  2921. */
  2922. Stack.prototype._stack = function _stack () {
  2923. var i,
  2924. iMax,
  2925. ordered = this.ordered,
  2926. options = this.options,
  2927. orientation = options.orientation || this.defaultOptions.orientation,
  2928. axisOnTop = (orientation == 'top'),
  2929. margin;
  2930. if (options.margin && options.margin.item !== undefined) {
  2931. margin = options.margin.item;
  2932. }
  2933. else {
  2934. margin = this.defaultOptions.margin.item
  2935. }
  2936. // calculate new, non-overlapping positions
  2937. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2938. var item = ordered[i];
  2939. var collidingItem = null;
  2940. do {
  2941. // TODO: optimize checking for overlap. when there is a gap without items,
  2942. // you only need to check for items from the next item on, not from zero
  2943. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2944. if (collidingItem != null) {
  2945. // There is a collision. Reposition the event above the colliding element
  2946. if (axisOnTop) {
  2947. item.top = collidingItem.top + collidingItem.height + margin;
  2948. }
  2949. else {
  2950. item.top = collidingItem.top - item.height - margin;
  2951. }
  2952. }
  2953. } while (collidingItem);
  2954. }
  2955. };
  2956. /**
  2957. * Check if the destiny position of given item overlaps with any
  2958. * of the other items from index itemStart to itemEnd.
  2959. * @param {Array} items Array with items
  2960. * @param {int} itemIndex Number of the item to be checked for overlap
  2961. * @param {int} itemStart First item to be checked.
  2962. * @param {int} itemEnd Last item to be checked.
  2963. * @return {Object | null} colliding item, or undefined when no collisions
  2964. * @param {Number} margin A minimum required margin.
  2965. * If margin is provided, the two items will be
  2966. * marked colliding when they overlap or
  2967. * when the margin between the two is smaller than
  2968. * the requested margin.
  2969. */
  2970. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2971. itemStart, itemEnd, margin) {
  2972. var collision = this.collision;
  2973. // we loop from end to start, as we suppose that the chance of a
  2974. // collision is larger for items at the end, so check these first.
  2975. var a = items[itemIndex];
  2976. for (var i = itemEnd; i >= itemStart; i--) {
  2977. var b = items[i];
  2978. if (collision(a, b, margin)) {
  2979. if (i != itemIndex) {
  2980. return b;
  2981. }
  2982. }
  2983. }
  2984. return null;
  2985. };
  2986. /**
  2987. * Test if the two provided items collide
  2988. * The items must have parameters left, width, top, and height.
  2989. * @param {Component} a The first item
  2990. * @param {Component} b The second item
  2991. * @param {Number} margin A minimum required margin.
  2992. * If margin is provided, the two items will be
  2993. * marked colliding when they overlap or
  2994. * when the margin between the two is smaller than
  2995. * the requested margin.
  2996. * @return {boolean} true if a and b collide, else false
  2997. */
  2998. Stack.prototype.collision = function collision (a, b, margin) {
  2999. return ((a.left - margin) < (b.left + b.width) &&
  3000. (a.left + a.width + margin) > b.left &&
  3001. (a.top - margin) < (b.top + b.height) &&
  3002. (a.top + a.height + margin) > b.top);
  3003. };
  3004. /**
  3005. * @constructor Range
  3006. * A Range controls a numeric range with a start and end value.
  3007. * The Range adjusts the range based on mouse events or programmatic changes,
  3008. * and triggers events when the range is changing or has been changed.
  3009. * @param {Object} [options] See description at Range.setOptions
  3010. * @extends Controller
  3011. */
  3012. function Range(options) {
  3013. this.id = util.randomUUID();
  3014. this.start = null; // Number
  3015. this.end = null; // Number
  3016. this.options = options || {};
  3017. this.setOptions(options);
  3018. }
  3019. // extend the Range prototype with an event emitter mixin
  3020. Emitter(Range.prototype);
  3021. /**
  3022. * Set options for the range controller
  3023. * @param {Object} options Available options:
  3024. * {Number} min Minimum value for start
  3025. * {Number} max Maximum value for end
  3026. * {Number} zoomMin Set a minimum value for
  3027. * (end - start).
  3028. * {Number} zoomMax Set a maximum value for
  3029. * (end - start).
  3030. */
  3031. Range.prototype.setOptions = function (options) {
  3032. util.extend(this.options, options);
  3033. // re-apply range with new limitations
  3034. if (this.start !== null && this.end !== null) {
  3035. this.setRange(this.start, this.end);
  3036. }
  3037. };
  3038. /**
  3039. * Test whether direction has a valid value
  3040. * @param {String} direction 'horizontal' or 'vertical'
  3041. */
  3042. function validateDirection (direction) {
  3043. if (direction != 'horizontal' && direction != 'vertical') {
  3044. throw new TypeError('Unknown direction "' + direction + '". ' +
  3045. 'Choose "horizontal" or "vertical".');
  3046. }
  3047. }
  3048. /**
  3049. * Add listeners for mouse and touch events to the component
  3050. * @param {Controller} controller
  3051. * @param {Component} component Should be a rootpanel
  3052. * @param {String} event Available events: 'move', 'zoom'
  3053. * @param {String} direction Available directions: 'horizontal', 'vertical'
  3054. */
  3055. Range.prototype.subscribe = function (controller, component, event, direction) {
  3056. var me = this;
  3057. if (event == 'move') {
  3058. // drag start listener
  3059. controller.on('dragstart', function (event) {
  3060. me._onDragStart(event, component);
  3061. });
  3062. // drag listener
  3063. controller.on('drag', function (event) {
  3064. me._onDrag(event, component, direction);
  3065. });
  3066. // drag end listener
  3067. controller.on('dragend', function (event) {
  3068. me._onDragEnd(event, component);
  3069. });
  3070. // ignore dragging when holding
  3071. controller.on('hold', function (event) {
  3072. me._onHold();
  3073. });
  3074. }
  3075. else if (event == 'zoom') {
  3076. // mouse wheel
  3077. function mousewheel (event) {
  3078. me._onMouseWheel(event, component, direction);
  3079. }
  3080. controller.on('mousewheel', mousewheel);
  3081. controller.on('DOMMouseScroll', mousewheel); // For FF
  3082. // pinch
  3083. controller.on('touch', function (event) {
  3084. me._onTouch(event);
  3085. });
  3086. controller.on('pinch', function (event) {
  3087. me._onPinch(event, component, direction);
  3088. });
  3089. }
  3090. else {
  3091. throw new TypeError('Unknown event "' + event + '". ' +
  3092. 'Choose "move" or "zoom".');
  3093. }
  3094. };
  3095. /**
  3096. * Set a new start and end range
  3097. * @param {Number} [start]
  3098. * @param {Number} [end]
  3099. */
  3100. Range.prototype.setRange = function(start, end) {
  3101. var changed = this._applyRange(start, end);
  3102. if (changed) {
  3103. var params = {
  3104. start: this.start,
  3105. end: this.end
  3106. };
  3107. this.emit('rangechange', params);
  3108. this.emit('rangechanged', params);
  3109. }
  3110. };
  3111. /**
  3112. * Set a new start and end range. This method is the same as setRange, but
  3113. * does not trigger a range change and range changed event, and it returns
  3114. * true when the range is changed
  3115. * @param {Number} [start]
  3116. * @param {Number} [end]
  3117. * @return {Boolean} changed
  3118. * @private
  3119. */
  3120. Range.prototype._applyRange = function(start, end) {
  3121. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  3122. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  3123. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  3124. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  3125. diff;
  3126. // check for valid number
  3127. if (isNaN(newStart) || newStart === null) {
  3128. throw new Error('Invalid start "' + start + '"');
  3129. }
  3130. if (isNaN(newEnd) || newEnd === null) {
  3131. throw new Error('Invalid end "' + end + '"');
  3132. }
  3133. // prevent start < end
  3134. if (newEnd < newStart) {
  3135. newEnd = newStart;
  3136. }
  3137. // prevent start < min
  3138. if (min !== null) {
  3139. if (newStart < min) {
  3140. diff = (min - newStart);
  3141. newStart += diff;
  3142. newEnd += diff;
  3143. // prevent end > max
  3144. if (max != null) {
  3145. if (newEnd > max) {
  3146. newEnd = max;
  3147. }
  3148. }
  3149. }
  3150. }
  3151. // prevent end > max
  3152. if (max !== null) {
  3153. if (newEnd > max) {
  3154. diff = (newEnd - max);
  3155. newStart -= diff;
  3156. newEnd -= diff;
  3157. // prevent start < min
  3158. if (min != null) {
  3159. if (newStart < min) {
  3160. newStart = min;
  3161. }
  3162. }
  3163. }
  3164. }
  3165. // prevent (end-start) < zoomMin
  3166. if (this.options.zoomMin !== null) {
  3167. var zoomMin = parseFloat(this.options.zoomMin);
  3168. if (zoomMin < 0) {
  3169. zoomMin = 0;
  3170. }
  3171. if ((newEnd - newStart) < zoomMin) {
  3172. if ((this.end - this.start) === zoomMin) {
  3173. // ignore this action, we are already zoomed to the minimum
  3174. newStart = this.start;
  3175. newEnd = this.end;
  3176. }
  3177. else {
  3178. // zoom to the minimum
  3179. diff = (zoomMin - (newEnd - newStart));
  3180. newStart -= diff / 2;
  3181. newEnd += diff / 2;
  3182. }
  3183. }
  3184. }
  3185. // prevent (end-start) > zoomMax
  3186. if (this.options.zoomMax !== null) {
  3187. var zoomMax = parseFloat(this.options.zoomMax);
  3188. if (zoomMax < 0) {
  3189. zoomMax = 0;
  3190. }
  3191. if ((newEnd - newStart) > zoomMax) {
  3192. if ((this.end - this.start) === zoomMax) {
  3193. // ignore this action, we are already zoomed to the maximum
  3194. newStart = this.start;
  3195. newEnd = this.end;
  3196. }
  3197. else {
  3198. // zoom to the maximum
  3199. diff = ((newEnd - newStart) - zoomMax);
  3200. newStart += diff / 2;
  3201. newEnd -= diff / 2;
  3202. }
  3203. }
  3204. }
  3205. var changed = (this.start != newStart || this.end != newEnd);
  3206. this.start = newStart;
  3207. this.end = newEnd;
  3208. return changed;
  3209. };
  3210. /**
  3211. * Retrieve the current range.
  3212. * @return {Object} An object with start and end properties
  3213. */
  3214. Range.prototype.getRange = function() {
  3215. return {
  3216. start: this.start,
  3217. end: this.end
  3218. };
  3219. };
  3220. /**
  3221. * Calculate the conversion offset and scale for current range, based on
  3222. * the provided width
  3223. * @param {Number} width
  3224. * @returns {{offset: number, scale: number}} conversion
  3225. */
  3226. Range.prototype.conversion = function (width) {
  3227. return Range.conversion(this.start, this.end, width);
  3228. };
  3229. /**
  3230. * Static method to calculate the conversion offset and scale for a range,
  3231. * based on the provided start, end, and width
  3232. * @param {Number} start
  3233. * @param {Number} end
  3234. * @param {Number} width
  3235. * @returns {{offset: number, scale: number}} conversion
  3236. */
  3237. Range.conversion = function (start, end, width) {
  3238. if (width != 0 && (end - start != 0)) {
  3239. return {
  3240. offset: start,
  3241. scale: width / (end - start)
  3242. }
  3243. }
  3244. else {
  3245. return {
  3246. offset: 0,
  3247. scale: 1
  3248. };
  3249. }
  3250. };
  3251. // global (private) object to store drag params
  3252. var touchParams = {};
  3253. /**
  3254. * Start dragging horizontally or vertically
  3255. * @param {Event} event
  3256. * @param {Object} component
  3257. * @private
  3258. */
  3259. Range.prototype._onDragStart = function(event, component) {
  3260. // refuse to drag when we where pinching to prevent the timeline make a jump
  3261. // when releasing the fingers in opposite order from the touch screen
  3262. if (touchParams.ignore) return;
  3263. // TODO: reckon with option movable
  3264. touchParams.start = this.start;
  3265. touchParams.end = this.end;
  3266. var frame = component.frame;
  3267. if (frame) {
  3268. frame.style.cursor = 'move';
  3269. }
  3270. };
  3271. /**
  3272. * Perform dragging operating.
  3273. * @param {Event} event
  3274. * @param {Component} component
  3275. * @param {String} direction 'horizontal' or 'vertical'
  3276. * @private
  3277. */
  3278. Range.prototype._onDrag = function (event, component, direction) {
  3279. validateDirection(direction);
  3280. // TODO: reckon with option movable
  3281. // refuse to drag when we where pinching to prevent the timeline make a jump
  3282. // when releasing the fingers in opposite order from the touch screen
  3283. if (touchParams.ignore) return;
  3284. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3285. interval = (touchParams.end - touchParams.start),
  3286. width = (direction == 'horizontal') ? component.width : component.height,
  3287. diffRange = -delta / width * interval;
  3288. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3289. this.emit('rangechange', {
  3290. start: this.start,
  3291. end: this.end
  3292. });
  3293. };
  3294. /**
  3295. * Stop dragging operating.
  3296. * @param {event} event
  3297. * @param {Component} component
  3298. * @private
  3299. */
  3300. Range.prototype._onDragEnd = function (event, component) {
  3301. // refuse to drag when we where pinching to prevent the timeline make a jump
  3302. // when releasing the fingers in opposite order from the touch screen
  3303. if (touchParams.ignore) return;
  3304. // TODO: reckon with option movable
  3305. if (component.frame) {
  3306. component.frame.style.cursor = 'auto';
  3307. }
  3308. // fire a rangechanged event
  3309. this.emit('rangechanged', {
  3310. start: this.start,
  3311. end: this.end
  3312. });
  3313. };
  3314. /**
  3315. * Event handler for mouse wheel event, used to zoom
  3316. * Code from http://adomas.org/javascript-mouse-wheel/
  3317. * @param {Event} event
  3318. * @param {Component} component
  3319. * @param {String} direction 'horizontal' or 'vertical'
  3320. * @private
  3321. */
  3322. Range.prototype._onMouseWheel = function(event, component, direction) {
  3323. validateDirection(direction);
  3324. // TODO: reckon with option zoomable
  3325. // retrieve delta
  3326. var delta = 0;
  3327. if (event.wheelDelta) { /* IE/Opera. */
  3328. delta = event.wheelDelta / 120;
  3329. } else if (event.detail) { /* Mozilla case. */
  3330. // In Mozilla, sign of delta is different than in IE.
  3331. // Also, delta is multiple of 3.
  3332. delta = -event.detail / 3;
  3333. }
  3334. // If delta is nonzero, handle it.
  3335. // Basically, delta is now positive if wheel was scrolled up,
  3336. // and negative, if wheel was scrolled down.
  3337. if (delta) {
  3338. // perform the zoom action. Delta is normally 1 or -1
  3339. // adjust a negative delta such that zooming in with delta 0.1
  3340. // equals zooming out with a delta -0.1
  3341. var scale;
  3342. if (delta < 0) {
  3343. scale = 1 - (delta / 5);
  3344. }
  3345. else {
  3346. scale = 1 / (1 + (delta / 5)) ;
  3347. }
  3348. // calculate center, the date to zoom around
  3349. var gesture = util.fakeGesture(this, event),
  3350. pointer = getPointer(gesture.center, component.frame),
  3351. pointerDate = this._pointerToDate(component, direction, pointer);
  3352. this.zoom(scale, pointerDate);
  3353. }
  3354. // Prevent default actions caused by mouse wheel
  3355. // (else the page and timeline both zoom and scroll)
  3356. event.preventDefault();
  3357. };
  3358. /**
  3359. * Start of a touch gesture
  3360. * @private
  3361. */
  3362. Range.prototype._onTouch = function (event) {
  3363. touchParams.start = this.start;
  3364. touchParams.end = this.end;
  3365. touchParams.ignore = false;
  3366. touchParams.center = null;
  3367. // don't move the range when dragging a selected event
  3368. // TODO: it's not so neat to have to know about the state of the ItemSet
  3369. var item = ItemSet.itemFromTarget(event);
  3370. if (item && item.selected && this.options.editable) {
  3371. touchParams.ignore = true;
  3372. }
  3373. };
  3374. /**
  3375. * On start of a hold gesture
  3376. * @private
  3377. */
  3378. Range.prototype._onHold = function () {
  3379. touchParams.ignore = true;
  3380. };
  3381. /**
  3382. * Handle pinch event
  3383. * @param {Event} event
  3384. * @param {Component} component
  3385. * @param {String} direction 'horizontal' or 'vertical'
  3386. * @private
  3387. */
  3388. Range.prototype._onPinch = function (event, component, direction) {
  3389. touchParams.ignore = true;
  3390. // TODO: reckon with option zoomable
  3391. if (event.gesture.touches.length > 1) {
  3392. if (!touchParams.center) {
  3393. touchParams.center = getPointer(event.gesture.center, component.frame);
  3394. }
  3395. var scale = 1 / event.gesture.scale,
  3396. initDate = this._pointerToDate(component, direction, touchParams.center),
  3397. center = getPointer(event.gesture.center, component.frame),
  3398. date = this._pointerToDate(component, direction, center),
  3399. delta = date - initDate; // TODO: utilize delta
  3400. // calculate new start and end
  3401. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3402. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3403. // apply new range
  3404. this.setRange(newStart, newEnd);
  3405. }
  3406. };
  3407. /**
  3408. * Helper function to calculate the center date for zooming
  3409. * @param {Component} component
  3410. * @param {{x: Number, y: Number}} pointer
  3411. * @param {String} direction 'horizontal' or 'vertical'
  3412. * @return {number} date
  3413. * @private
  3414. */
  3415. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3416. var conversion;
  3417. if (direction == 'horizontal') {
  3418. var width = component.width;
  3419. conversion = this.conversion(width);
  3420. return pointer.x / conversion.scale + conversion.offset;
  3421. }
  3422. else {
  3423. var height = component.height;
  3424. conversion = this.conversion(height);
  3425. return pointer.y / conversion.scale + conversion.offset;
  3426. }
  3427. };
  3428. /**
  3429. * Get the pointer location relative to the location of the dom element
  3430. * @param {{pageX: Number, pageY: Number}} touch
  3431. * @param {Element} element HTML DOM element
  3432. * @return {{x: Number, y: Number}} pointer
  3433. * @private
  3434. */
  3435. function getPointer (touch, element) {
  3436. return {
  3437. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3438. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3439. };
  3440. }
  3441. /**
  3442. * Zoom the range the given scale in or out. Start and end date will
  3443. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3444. * date around which to zoom.
  3445. * For example, try scale = 0.9 or 1.1
  3446. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3447. * values below 1 will zoom in.
  3448. * @param {Number} [center] Value representing a date around which will
  3449. * be zoomed.
  3450. */
  3451. Range.prototype.zoom = function(scale, center) {
  3452. // if centerDate is not provided, take it half between start Date and end Date
  3453. if (center == null) {
  3454. center = (this.start + this.end) / 2;
  3455. }
  3456. // calculate new start and end
  3457. var newStart = center + (this.start - center) * scale;
  3458. var newEnd = center + (this.end - center) * scale;
  3459. this.setRange(newStart, newEnd);
  3460. };
  3461. /**
  3462. * Move the range with a given delta to the left or right. Start and end
  3463. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3464. * @param {Number} delta Moving amount. Positive value will move right,
  3465. * negative value will move left
  3466. */
  3467. Range.prototype.move = function(delta) {
  3468. // zoom start Date and end Date relative to the centerDate
  3469. var diff = (this.end - this.start);
  3470. // apply new values
  3471. var newStart = this.start + diff * delta;
  3472. var newEnd = this.end + diff * delta;
  3473. // TODO: reckon with min and max range
  3474. this.start = newStart;
  3475. this.end = newEnd;
  3476. };
  3477. /**
  3478. * Move the range to a new center point
  3479. * @param {Number} moveTo New center point of the range
  3480. */
  3481. Range.prototype.moveTo = function(moveTo) {
  3482. var center = (this.start + this.end) / 2;
  3483. var diff = center - moveTo;
  3484. // calculate new start and end
  3485. var newStart = this.start - diff;
  3486. var newEnd = this.end - diff;
  3487. this.setRange(newStart, newEnd);
  3488. };
  3489. /**
  3490. * @constructor Controller
  3491. *
  3492. * A Controller controls the reflows and repaints of all components,
  3493. * and is used as an event bus for all components.
  3494. */
  3495. function Controller () {
  3496. var me = this;
  3497. this.id = util.randomUUID();
  3498. this.components = {};
  3499. /**
  3500. * Listen for a 'request-reflow' event. The controller will schedule a reflow
  3501. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3502. * is false.
  3503. */
  3504. var reflowTimer = null;
  3505. this.on('request-reflow', function requestReflow(force) {
  3506. if (force) {
  3507. me.reflow();
  3508. }
  3509. else {
  3510. if (!reflowTimer) {
  3511. reflowTimer = setTimeout(function () {
  3512. reflowTimer = null;
  3513. me.reflow();
  3514. }, 0);
  3515. }
  3516. }
  3517. });
  3518. /**
  3519. * Request a repaint. The controller will schedule a repaint
  3520. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3521. * is false.
  3522. */
  3523. var repaintTimer = null;
  3524. this.on('request-repaint', function requestRepaint(force) {
  3525. if (force) {
  3526. me.repaint();
  3527. }
  3528. else {
  3529. if (!repaintTimer) {
  3530. repaintTimer = setTimeout(function () {
  3531. repaintTimer = null;
  3532. me.repaint();
  3533. }, 0);
  3534. }
  3535. }
  3536. });
  3537. }
  3538. // Extend controller with Emitter mixin
  3539. Emitter(Controller.prototype);
  3540. /**
  3541. * Add a component to the controller
  3542. * @param {Component} component
  3543. */
  3544. Controller.prototype.add = function add(component) {
  3545. // validate the component
  3546. if (component.id == undefined) {
  3547. throw new Error('Component has no field id');
  3548. }
  3549. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3550. throw new TypeError('Component must be an instance of ' +
  3551. 'prototype Component or Controller');
  3552. }
  3553. // add the component
  3554. component.setController(this);
  3555. this.components[component.id] = component;
  3556. };
  3557. /**
  3558. * Remove a component from the controller
  3559. * @param {Component | String} component
  3560. */
  3561. Controller.prototype.remove = function remove(component) {
  3562. var id;
  3563. for (id in this.components) {
  3564. if (this.components.hasOwnProperty(id)) {
  3565. if (id == component || this.components[id] === component) {
  3566. break;
  3567. }
  3568. }
  3569. }
  3570. if (id) {
  3571. // unregister the controller (gives the component the ability to unregister
  3572. // event listeners and clean up other stuff)
  3573. this.components[id].setController(null);
  3574. delete this.components[id];
  3575. }
  3576. };
  3577. /**
  3578. * Repaint all components
  3579. */
  3580. Controller.prototype.repaint = function repaint() {
  3581. var changed = false;
  3582. // cancel any running repaint request
  3583. if (this.repaintTimer) {
  3584. clearTimeout(this.repaintTimer);
  3585. this.repaintTimer = undefined;
  3586. }
  3587. var done = {};
  3588. function repaint(component, id) {
  3589. if (!(id in done)) {
  3590. // first repaint the components on which this component is dependent
  3591. if (component.depends) {
  3592. component.depends.forEach(function (dep) {
  3593. repaint(dep, dep.id);
  3594. });
  3595. }
  3596. if (component.parent) {
  3597. repaint(component.parent, component.parent.id);
  3598. }
  3599. // repaint the component itself and mark as done
  3600. changed = component.repaint() || changed;
  3601. done[id] = true;
  3602. }
  3603. }
  3604. util.forEach(this.components, repaint);
  3605. this.emit('repaint');
  3606. // immediately reflow when needed
  3607. if (changed) {
  3608. this.reflow();
  3609. }
  3610. // TODO: limit the number of nested reflows/repaints, prevent loop
  3611. };
  3612. /**
  3613. * Reflow all components
  3614. */
  3615. Controller.prototype.reflow = function reflow() {
  3616. var resized = false;
  3617. // cancel any running repaint request
  3618. if (this.reflowTimer) {
  3619. clearTimeout(this.reflowTimer);
  3620. this.reflowTimer = undefined;
  3621. }
  3622. var done = {};
  3623. function reflow(component, id) {
  3624. if (!(id in done)) {
  3625. // first reflow the components on which this component is dependent
  3626. if (component.depends) {
  3627. component.depends.forEach(function (dep) {
  3628. reflow(dep, dep.id);
  3629. });
  3630. }
  3631. if (component.parent) {
  3632. reflow(component.parent, component.parent.id);
  3633. }
  3634. // reflow the component itself and mark as done
  3635. resized = component.reflow() || resized;
  3636. done[id] = true;
  3637. }
  3638. }
  3639. util.forEach(this.components, reflow);
  3640. this.emit('reflow');
  3641. // immediately repaint when needed
  3642. if (resized) {
  3643. this.repaint();
  3644. }
  3645. // TODO: limit the number of nested reflows/repaints, prevent loop
  3646. };
  3647. /**
  3648. * Prototype for visual components
  3649. */
  3650. function Component () {
  3651. this.id = null;
  3652. this.parent = null;
  3653. this.depends = null;
  3654. this.controller = null;
  3655. this.options = null;
  3656. this.frame = null; // main DOM element
  3657. this.top = 0;
  3658. this.left = 0;
  3659. this.width = 0;
  3660. this.height = 0;
  3661. }
  3662. /**
  3663. * Set parameters for the frame. Parameters will be merged in current parameter
  3664. * set.
  3665. * @param {Object} options Available parameters:
  3666. * {String | function} [className]
  3667. * {String | Number | function} [left]
  3668. * {String | Number | function} [top]
  3669. * {String | Number | function} [width]
  3670. * {String | Number | function} [height]
  3671. */
  3672. Component.prototype.setOptions = function setOptions(options) {
  3673. if (options) {
  3674. util.extend(this.options, options);
  3675. if (this.controller) {
  3676. this.requestRepaint();
  3677. this.requestReflow();
  3678. }
  3679. }
  3680. };
  3681. /**
  3682. * Get an option value by name
  3683. * The function will first check this.options object, and else will check
  3684. * this.defaultOptions.
  3685. * @param {String} name
  3686. * @return {*} value
  3687. */
  3688. Component.prototype.getOption = function getOption(name) {
  3689. var value;
  3690. if (this.options) {
  3691. value = this.options[name];
  3692. }
  3693. if (value === undefined && this.defaultOptions) {
  3694. value = this.defaultOptions[name];
  3695. }
  3696. return value;
  3697. };
  3698. /**
  3699. * Set controller for this component, or remove current controller by passing
  3700. * null as parameter value.
  3701. * @param {Controller | null} controller
  3702. */
  3703. Component.prototype.setController = function setController (controller) {
  3704. this.controller = controller || null;
  3705. };
  3706. /**
  3707. * Get controller of this component
  3708. * @return {Controller} controller
  3709. */
  3710. Component.prototype.getController = function getController () {
  3711. return this.controller;
  3712. };
  3713. /**
  3714. * Get the container element of the component, which can be used by a child to
  3715. * add its own widgets. Not all components do have a container for childs, in
  3716. * that case null is returned.
  3717. * @returns {HTMLElement | null} container
  3718. */
  3719. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3720. Component.prototype.getContainer = function getContainer() {
  3721. // should be implemented by the component
  3722. return null;
  3723. };
  3724. /**
  3725. * Get the frame element of the component, the outer HTML DOM element.
  3726. * @returns {HTMLElement | null} frame
  3727. */
  3728. Component.prototype.getFrame = function getFrame() {
  3729. return this.frame;
  3730. };
  3731. /**
  3732. * Repaint the component
  3733. * @return {Boolean} changed
  3734. */
  3735. Component.prototype.repaint = function repaint() {
  3736. // should be implemented by the component
  3737. return false;
  3738. };
  3739. /**
  3740. * Reflow the component
  3741. * @return {Boolean} resized
  3742. */
  3743. Component.prototype.reflow = function reflow() {
  3744. // should be implemented by the component
  3745. return false;
  3746. };
  3747. /**
  3748. * Hide the component from the DOM
  3749. * @return {Boolean} changed
  3750. */
  3751. Component.prototype.hide = function hide() {
  3752. if (this.frame && this.frame.parentNode) {
  3753. this.frame.parentNode.removeChild(this.frame);
  3754. return true;
  3755. }
  3756. else {
  3757. return false;
  3758. }
  3759. };
  3760. /**
  3761. * Show the component in the DOM (when not already visible).
  3762. * A repaint will be executed when the component is not visible
  3763. * @return {Boolean} changed
  3764. */
  3765. Component.prototype.show = function show() {
  3766. if (!this.frame || !this.frame.parentNode) {
  3767. return this.repaint();
  3768. }
  3769. else {
  3770. return false;
  3771. }
  3772. };
  3773. /**
  3774. * Request a repaint. The controller will schedule a repaint
  3775. */
  3776. Component.prototype.requestRepaint = function requestRepaint() {
  3777. if (this.controller) {
  3778. this.controller.emit('request-repaint');
  3779. }
  3780. else {
  3781. throw new Error('Cannot request a repaint: no controller configured');
  3782. // TODO: just do a repaint when no parent is configured?
  3783. }
  3784. };
  3785. /**
  3786. * Request a reflow. The controller will schedule a reflow
  3787. */
  3788. Component.prototype.requestReflow = function requestReflow() {
  3789. if (this.controller) {
  3790. this.controller.emit('request-reflow');
  3791. }
  3792. else {
  3793. throw new Error('Cannot request a reflow: no controller configured');
  3794. // TODO: just do a reflow when no parent is configured?
  3795. }
  3796. };
  3797. /**
  3798. * A panel can contain components
  3799. * @param {Component} [parent]
  3800. * @param {Component[]} [depends] Components on which this components depends
  3801. * (except for the parent)
  3802. * @param {Object} [options] Available parameters:
  3803. * {String | Number | function} [left]
  3804. * {String | Number | function} [top]
  3805. * {String | Number | function} [width]
  3806. * {String | Number | function} [height]
  3807. * {String | function} [className]
  3808. * @constructor Panel
  3809. * @extends Component
  3810. */
  3811. function Panel(parent, depends, options) {
  3812. this.id = util.randomUUID();
  3813. this.parent = parent;
  3814. this.depends = depends;
  3815. this.options = options || {};
  3816. }
  3817. Panel.prototype = new Component();
  3818. /**
  3819. * Set options. Will extend the current options.
  3820. * @param {Object} [options] Available parameters:
  3821. * {String | function} [className]
  3822. * {String | Number | function} [left]
  3823. * {String | Number | function} [top]
  3824. * {String | Number | function} [width]
  3825. * {String | Number | function} [height]
  3826. */
  3827. Panel.prototype.setOptions = Component.prototype.setOptions;
  3828. /**
  3829. * Get the container element of the panel, which can be used by a child to
  3830. * add its own widgets.
  3831. * @returns {HTMLElement} container
  3832. */
  3833. Panel.prototype.getContainer = function () {
  3834. return this.frame;
  3835. };
  3836. /**
  3837. * Repaint the component
  3838. * @return {Boolean} changed
  3839. */
  3840. Panel.prototype.repaint = function () {
  3841. var changed = 0,
  3842. update = util.updateProperty,
  3843. asSize = util.option.asSize,
  3844. options = this.options,
  3845. frame = this.frame;
  3846. if (!frame) {
  3847. frame = document.createElement('div');
  3848. frame.className = 'vpanel';
  3849. var className = options.className;
  3850. if (className) {
  3851. if (typeof className == 'function') {
  3852. util.addClassName(frame, String(className()));
  3853. }
  3854. else {
  3855. util.addClassName(frame, String(className));
  3856. }
  3857. }
  3858. this.frame = frame;
  3859. changed += 1;
  3860. }
  3861. if (!frame.parentNode) {
  3862. if (!this.parent) {
  3863. throw new Error('Cannot repaint panel: no parent attached');
  3864. }
  3865. var parentContainer = this.parent.getContainer();
  3866. if (!parentContainer) {
  3867. throw new Error('Cannot repaint panel: parent has no container element');
  3868. }
  3869. parentContainer.appendChild(frame);
  3870. changed += 1;
  3871. }
  3872. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3873. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3874. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3875. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3876. return (changed > 0);
  3877. };
  3878. /**
  3879. * Reflow the component
  3880. * @return {Boolean} resized
  3881. */
  3882. Panel.prototype.reflow = function () {
  3883. var changed = 0,
  3884. update = util.updateProperty,
  3885. frame = this.frame;
  3886. if (frame) {
  3887. changed += update(this, 'top', frame.offsetTop);
  3888. changed += update(this, 'left', frame.offsetLeft);
  3889. changed += update(this, 'width', frame.offsetWidth);
  3890. changed += update(this, 'height', frame.offsetHeight);
  3891. }
  3892. else {
  3893. changed += 1;
  3894. }
  3895. return (changed > 0);
  3896. };
  3897. /**
  3898. * A root panel can hold components. The root panel must be initialized with
  3899. * a DOM element as container.
  3900. * @param {HTMLElement} container
  3901. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3902. * @constructor RootPanel
  3903. * @extends Panel
  3904. */
  3905. function RootPanel(container, options) {
  3906. this.id = util.randomUUID();
  3907. this.container = container;
  3908. // create functions to be used as DOM event listeners
  3909. var me = this;
  3910. this.hammer = null;
  3911. // create listeners for all interesting events, these events will be emitted
  3912. // via the controller
  3913. var events = [
  3914. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3915. 'dragstart', 'drag', 'dragend',
  3916. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3917. ];
  3918. this.listeners = {};
  3919. events.forEach(function (event) {
  3920. me.listeners[event] = function () {
  3921. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3922. me.controller.emit.apply(me.controller, args);
  3923. };
  3924. });
  3925. this.options = options || {};
  3926. this.defaultOptions = {
  3927. autoResize: true
  3928. };
  3929. }
  3930. RootPanel.prototype = new Panel();
  3931. /**
  3932. * Set options. Will extend the current options.
  3933. * @param {Object} [options] Available parameters:
  3934. * {String | function} [className]
  3935. * {String | Number | function} [left]
  3936. * {String | Number | function} [top]
  3937. * {String | Number | function} [width]
  3938. * {String | Number | function} [height]
  3939. * {Boolean | function} [autoResize]
  3940. */
  3941. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3942. /**
  3943. * Repaint the component
  3944. * @return {Boolean} changed
  3945. */
  3946. RootPanel.prototype.repaint = function () {
  3947. var changed = 0,
  3948. update = util.updateProperty,
  3949. asSize = util.option.asSize,
  3950. options = this.options,
  3951. frame = this.frame;
  3952. if (!frame) {
  3953. frame = document.createElement('div');
  3954. this.frame = frame;
  3955. this._registerListeners();
  3956. changed += 1;
  3957. }
  3958. if (!frame.parentNode) {
  3959. if (!this.container) {
  3960. throw new Error('Cannot repaint root panel: no container attached');
  3961. }
  3962. this.container.appendChild(frame);
  3963. changed += 1;
  3964. }
  3965. frame.className = 'vis timeline rootpanel ' + options.orientation +
  3966. (options.editable ? ' editable' : '');
  3967. var className = options.className;
  3968. if (className) {
  3969. util.addClassName(frame, util.option.asString(className));
  3970. }
  3971. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3972. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3973. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3974. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3975. this._updateWatch();
  3976. return (changed > 0);
  3977. };
  3978. /**
  3979. * Reflow the component
  3980. * @return {Boolean} resized
  3981. */
  3982. RootPanel.prototype.reflow = function () {
  3983. var changed = 0,
  3984. update = util.updateProperty,
  3985. frame = this.frame;
  3986. if (frame) {
  3987. changed += update(this, 'top', frame.offsetTop);
  3988. changed += update(this, 'left', frame.offsetLeft);
  3989. changed += update(this, 'width', frame.offsetWidth);
  3990. changed += update(this, 'height', frame.offsetHeight);
  3991. }
  3992. else {
  3993. changed += 1;
  3994. }
  3995. return (changed > 0);
  3996. };
  3997. /**
  3998. * Update watching for resize, depending on the current option
  3999. * @private
  4000. */
  4001. RootPanel.prototype._updateWatch = function () {
  4002. var autoResize = this.getOption('autoResize');
  4003. if (autoResize) {
  4004. this._watch();
  4005. }
  4006. else {
  4007. this._unwatch();
  4008. }
  4009. };
  4010. /**
  4011. * Watch for changes in the size of the frame. On resize, the Panel will
  4012. * automatically redraw itself.
  4013. * @private
  4014. */
  4015. RootPanel.prototype._watch = function () {
  4016. var me = this;
  4017. this._unwatch();
  4018. var checkSize = function () {
  4019. var autoResize = me.getOption('autoResize');
  4020. if (!autoResize) {
  4021. // stop watching when the option autoResize is changed to false
  4022. me._unwatch();
  4023. return;
  4024. }
  4025. if (me.frame) {
  4026. // check whether the frame is resized
  4027. if ((me.frame.clientWidth != me.width) ||
  4028. (me.frame.clientHeight != me.height)) {
  4029. me.requestReflow();
  4030. }
  4031. }
  4032. };
  4033. // TODO: automatically cleanup the event listener when the frame is deleted
  4034. util.addEventListener(window, 'resize', checkSize);
  4035. this.watchTimer = setInterval(checkSize, 1000);
  4036. };
  4037. /**
  4038. * Stop watching for a resize of the frame.
  4039. * @private
  4040. */
  4041. RootPanel.prototype._unwatch = function () {
  4042. if (this.watchTimer) {
  4043. clearInterval(this.watchTimer);
  4044. this.watchTimer = undefined;
  4045. }
  4046. // TODO: remove event listener on window.resize
  4047. };
  4048. /**
  4049. * Set controller for this component, or remove current controller by passing
  4050. * null as parameter value.
  4051. * @param {Controller | null} controller
  4052. */
  4053. RootPanel.prototype.setController = function setController (controller) {
  4054. this.controller = controller || null;
  4055. if (this.controller) {
  4056. this._registerListeners();
  4057. }
  4058. else {
  4059. this._unregisterListeners();
  4060. }
  4061. };
  4062. /**
  4063. * Register event emitters emitted by the rootpanel
  4064. * @private
  4065. */
  4066. RootPanel.prototype._registerListeners = function () {
  4067. if (this.frame && this.controller && !this.hammer) {
  4068. this.hammer = Hammer(this.frame, {
  4069. prevent_default: true
  4070. });
  4071. for (var event in this.listeners) {
  4072. if (this.listeners.hasOwnProperty(event)) {
  4073. this.hammer.on(event, this.listeners[event]);
  4074. }
  4075. }
  4076. }
  4077. };
  4078. /**
  4079. * Unregister event emitters from the rootpanel
  4080. * @private
  4081. */
  4082. RootPanel.prototype._unregisterListeners = function () {
  4083. if (this.hammer) {
  4084. for (var event in this.listeners) {
  4085. if (this.listeners.hasOwnProperty(event)) {
  4086. this.hammer.off(event, this.listeners[event]);
  4087. }
  4088. }
  4089. this.hammer = null;
  4090. }
  4091. };
  4092. /**
  4093. * A horizontal time axis
  4094. * @param {Component} parent
  4095. * @param {Component[]} [depends] Components on which this components depends
  4096. * (except for the parent)
  4097. * @param {Object} [options] See TimeAxis.setOptions for the available
  4098. * options.
  4099. * @constructor TimeAxis
  4100. * @extends Component
  4101. */
  4102. function TimeAxis (parent, depends, options) {
  4103. this.id = util.randomUUID();
  4104. this.parent = parent;
  4105. this.depends = depends;
  4106. this.dom = {
  4107. majorLines: [],
  4108. majorTexts: [],
  4109. minorLines: [],
  4110. minorTexts: [],
  4111. redundant: {
  4112. majorLines: [],
  4113. majorTexts: [],
  4114. minorLines: [],
  4115. minorTexts: []
  4116. }
  4117. };
  4118. this.props = {
  4119. range: {
  4120. start: 0,
  4121. end: 0,
  4122. minimumStep: 0
  4123. },
  4124. lineTop: 0
  4125. };
  4126. this.options = options || {};
  4127. this.defaultOptions = {
  4128. orientation: 'bottom', // supported: 'top', 'bottom'
  4129. // TODO: implement timeaxis orientations 'left' and 'right'
  4130. showMinorLabels: true,
  4131. showMajorLabels: true
  4132. };
  4133. this.conversion = null;
  4134. this.range = null;
  4135. }
  4136. TimeAxis.prototype = new Component();
  4137. // TODO: comment options
  4138. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  4139. /**
  4140. * Set a range (start and end)
  4141. * @param {Range | Object} range A Range or an object containing start and end.
  4142. */
  4143. TimeAxis.prototype.setRange = function (range) {
  4144. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4145. throw new TypeError('Range must be an instance of Range, ' +
  4146. 'or an object containing start and end.');
  4147. }
  4148. this.range = range;
  4149. };
  4150. /**
  4151. * Convert a position on screen (pixels) to a datetime
  4152. * @param {int} x Position on the screen in pixels
  4153. * @return {Date} time The datetime the corresponds with given position x
  4154. */
  4155. TimeAxis.prototype.toTime = function(x) {
  4156. var conversion = this.conversion;
  4157. return new Date(x / conversion.scale + conversion.offset);
  4158. };
  4159. /**
  4160. * Convert a datetime (Date object) into a position on the screen
  4161. * @param {Date} time A date
  4162. * @return {int} x The position on the screen in pixels which corresponds
  4163. * with the given date.
  4164. * @private
  4165. */
  4166. TimeAxis.prototype.toScreen = function(time) {
  4167. var conversion = this.conversion;
  4168. return (time.valueOf() - conversion.offset) * conversion.scale;
  4169. };
  4170. /**
  4171. * Repaint the component
  4172. * @return {Boolean} changed
  4173. */
  4174. TimeAxis.prototype.repaint = function () {
  4175. var changed = 0,
  4176. update = util.updateProperty,
  4177. asSize = util.option.asSize,
  4178. options = this.options,
  4179. orientation = this.getOption('orientation'),
  4180. props = this.props,
  4181. step = this.step;
  4182. var frame = this.frame;
  4183. if (!frame) {
  4184. frame = document.createElement('div');
  4185. this.frame = frame;
  4186. changed += 1;
  4187. }
  4188. frame.className = 'axis';
  4189. // TODO: custom className?
  4190. if (!frame.parentNode) {
  4191. if (!this.parent) {
  4192. throw new Error('Cannot repaint time axis: no parent attached');
  4193. }
  4194. var parentContainer = this.parent.getContainer();
  4195. if (!parentContainer) {
  4196. throw new Error('Cannot repaint time axis: parent has no container element');
  4197. }
  4198. parentContainer.appendChild(frame);
  4199. changed += 1;
  4200. }
  4201. var parent = frame.parentNode;
  4202. if (parent) {
  4203. var beforeChild = frame.nextSibling;
  4204. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  4205. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  4206. (this.props.parentHeight - this.height) + 'px' :
  4207. '0px';
  4208. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  4209. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4210. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4211. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4212. // get characters width and height
  4213. this._repaintMeasureChars();
  4214. if (this.step) {
  4215. this._repaintStart();
  4216. step.first();
  4217. var xFirstMajorLabel = undefined;
  4218. var max = 0;
  4219. while (step.hasNext() && max < 1000) {
  4220. max++;
  4221. var cur = step.getCurrent(),
  4222. x = this.toScreen(cur),
  4223. isMajor = step.isMajor();
  4224. // TODO: lines must have a width, such that we can create css backgrounds
  4225. if (this.getOption('showMinorLabels')) {
  4226. this._repaintMinorText(x, step.getLabelMinor());
  4227. }
  4228. if (isMajor && this.getOption('showMajorLabels')) {
  4229. if (x > 0) {
  4230. if (xFirstMajorLabel == undefined) {
  4231. xFirstMajorLabel = x;
  4232. }
  4233. this._repaintMajorText(x, step.getLabelMajor());
  4234. }
  4235. this._repaintMajorLine(x);
  4236. }
  4237. else {
  4238. this._repaintMinorLine(x);
  4239. }
  4240. step.next();
  4241. }
  4242. // create a major label on the left when needed
  4243. if (this.getOption('showMajorLabels')) {
  4244. var leftTime = this.toTime(0),
  4245. leftText = step.getLabelMajor(leftTime),
  4246. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  4247. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  4248. this._repaintMajorText(0, leftText);
  4249. }
  4250. }
  4251. this._repaintEnd();
  4252. }
  4253. this._repaintLine();
  4254. // put frame online again
  4255. if (beforeChild) {
  4256. parent.insertBefore(frame, beforeChild);
  4257. }
  4258. else {
  4259. parent.appendChild(frame)
  4260. }
  4261. }
  4262. return (changed > 0);
  4263. };
  4264. /**
  4265. * Start a repaint. Move all DOM elements to a redundant list, where they
  4266. * can be picked for re-use, or can be cleaned up in the end
  4267. * @private
  4268. */
  4269. TimeAxis.prototype._repaintStart = function () {
  4270. var dom = this.dom,
  4271. redundant = dom.redundant;
  4272. redundant.majorLines = dom.majorLines;
  4273. redundant.majorTexts = dom.majorTexts;
  4274. redundant.minorLines = dom.minorLines;
  4275. redundant.minorTexts = dom.minorTexts;
  4276. dom.majorLines = [];
  4277. dom.majorTexts = [];
  4278. dom.minorLines = [];
  4279. dom.minorTexts = [];
  4280. };
  4281. /**
  4282. * End a repaint. Cleanup leftover DOM elements in the redundant list
  4283. * @private
  4284. */
  4285. TimeAxis.prototype._repaintEnd = function () {
  4286. util.forEach(this.dom.redundant, function (arr) {
  4287. while (arr.length) {
  4288. var elem = arr.pop();
  4289. if (elem && elem.parentNode) {
  4290. elem.parentNode.removeChild(elem);
  4291. }
  4292. }
  4293. });
  4294. };
  4295. /**
  4296. * Create a minor label for the axis at position x
  4297. * @param {Number} x
  4298. * @param {String} text
  4299. * @private
  4300. */
  4301. TimeAxis.prototype._repaintMinorText = function (x, text) {
  4302. // reuse redundant label
  4303. var label = this.dom.redundant.minorTexts.shift();
  4304. if (!label) {
  4305. // create new label
  4306. var content = document.createTextNode('');
  4307. label = document.createElement('div');
  4308. label.appendChild(content);
  4309. label.className = 'text minor';
  4310. this.frame.appendChild(label);
  4311. }
  4312. this.dom.minorTexts.push(label);
  4313. label.childNodes[0].nodeValue = text;
  4314. label.style.left = x + 'px';
  4315. label.style.top = this.props.minorLabelTop + 'px';
  4316. //label.title = title; // TODO: this is a heavy operation
  4317. };
  4318. /**
  4319. * Create a Major label for the axis at position x
  4320. * @param {Number} x
  4321. * @param {String} text
  4322. * @private
  4323. */
  4324. TimeAxis.prototype._repaintMajorText = function (x, text) {
  4325. // reuse redundant label
  4326. var label = this.dom.redundant.majorTexts.shift();
  4327. if (!label) {
  4328. // create label
  4329. var content = document.createTextNode(text);
  4330. label = document.createElement('div');
  4331. label.className = 'text major';
  4332. label.appendChild(content);
  4333. this.frame.appendChild(label);
  4334. }
  4335. this.dom.majorTexts.push(label);
  4336. label.childNodes[0].nodeValue = text;
  4337. label.style.top = this.props.majorLabelTop + 'px';
  4338. label.style.left = x + 'px';
  4339. //label.title = title; // TODO: this is a heavy operation
  4340. };
  4341. /**
  4342. * Create a minor line for the axis at position x
  4343. * @param {Number} x
  4344. * @private
  4345. */
  4346. TimeAxis.prototype._repaintMinorLine = function (x) {
  4347. // reuse redundant line
  4348. var line = this.dom.redundant.minorLines.shift();
  4349. if (!line) {
  4350. // create vertical line
  4351. line = document.createElement('div');
  4352. line.className = 'grid vertical minor';
  4353. this.frame.appendChild(line);
  4354. }
  4355. this.dom.minorLines.push(line);
  4356. var props = this.props;
  4357. line.style.top = props.minorLineTop + 'px';
  4358. line.style.height = props.minorLineHeight + 'px';
  4359. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4360. };
  4361. /**
  4362. * Create a Major line for the axis at position x
  4363. * @param {Number} x
  4364. * @private
  4365. */
  4366. TimeAxis.prototype._repaintMajorLine = function (x) {
  4367. // reuse redundant line
  4368. var line = this.dom.redundant.majorLines.shift();
  4369. if (!line) {
  4370. // create vertical line
  4371. line = document.createElement('DIV');
  4372. line.className = 'grid vertical major';
  4373. this.frame.appendChild(line);
  4374. }
  4375. this.dom.majorLines.push(line);
  4376. var props = this.props;
  4377. line.style.top = props.majorLineTop + 'px';
  4378. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4379. line.style.height = props.majorLineHeight + 'px';
  4380. };
  4381. /**
  4382. * Repaint the horizontal line for the axis
  4383. * @private
  4384. */
  4385. TimeAxis.prototype._repaintLine = function() {
  4386. var line = this.dom.line,
  4387. frame = this.frame,
  4388. options = this.options;
  4389. // line before all axis elements
  4390. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4391. if (line) {
  4392. // put this line at the end of all childs
  4393. frame.removeChild(line);
  4394. frame.appendChild(line);
  4395. }
  4396. else {
  4397. // create the axis line
  4398. line = document.createElement('div');
  4399. line.className = 'grid horizontal major';
  4400. frame.appendChild(line);
  4401. this.dom.line = line;
  4402. }
  4403. line.style.top = this.props.lineTop + 'px';
  4404. }
  4405. else {
  4406. if (line && line.parentElement) {
  4407. frame.removeChild(line.line);
  4408. delete this.dom.line;
  4409. }
  4410. }
  4411. };
  4412. /**
  4413. * Create characters used to determine the size of text on the axis
  4414. * @private
  4415. */
  4416. TimeAxis.prototype._repaintMeasureChars = function () {
  4417. // calculate the width and height of a single character
  4418. // this is used to calculate the step size, and also the positioning of the
  4419. // axis
  4420. var dom = this.dom,
  4421. text;
  4422. if (!dom.measureCharMinor) {
  4423. text = document.createTextNode('0');
  4424. var measureCharMinor = document.createElement('DIV');
  4425. measureCharMinor.className = 'text minor measure';
  4426. measureCharMinor.appendChild(text);
  4427. this.frame.appendChild(measureCharMinor);
  4428. dom.measureCharMinor = measureCharMinor;
  4429. }
  4430. if (!dom.measureCharMajor) {
  4431. text = document.createTextNode('0');
  4432. var measureCharMajor = document.createElement('DIV');
  4433. measureCharMajor.className = 'text major measure';
  4434. measureCharMajor.appendChild(text);
  4435. this.frame.appendChild(measureCharMajor);
  4436. dom.measureCharMajor = measureCharMajor;
  4437. }
  4438. };
  4439. /**
  4440. * Reflow the component
  4441. * @return {Boolean} resized
  4442. */
  4443. TimeAxis.prototype.reflow = function () {
  4444. var changed = 0,
  4445. update = util.updateProperty,
  4446. frame = this.frame,
  4447. range = this.range;
  4448. if (!range) {
  4449. throw new Error('Cannot repaint time axis: no range configured');
  4450. }
  4451. if (frame) {
  4452. changed += update(this, 'top', frame.offsetTop);
  4453. changed += update(this, 'left', frame.offsetLeft);
  4454. // calculate size of a character
  4455. var props = this.props,
  4456. showMinorLabels = this.getOption('showMinorLabels'),
  4457. showMajorLabels = this.getOption('showMajorLabels'),
  4458. measureCharMinor = this.dom.measureCharMinor,
  4459. measureCharMajor = this.dom.measureCharMajor;
  4460. if (measureCharMinor) {
  4461. props.minorCharHeight = measureCharMinor.clientHeight;
  4462. props.minorCharWidth = measureCharMinor.clientWidth;
  4463. }
  4464. if (measureCharMajor) {
  4465. props.majorCharHeight = measureCharMajor.clientHeight;
  4466. props.majorCharWidth = measureCharMajor.clientWidth;
  4467. }
  4468. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4469. if (parentHeight != props.parentHeight) {
  4470. props.parentHeight = parentHeight;
  4471. changed += 1;
  4472. }
  4473. switch (this.getOption('orientation')) {
  4474. case 'bottom':
  4475. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4476. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4477. props.minorLabelTop = 0;
  4478. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4479. props.minorLineTop = -this.top;
  4480. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4481. props.minorLineWidth = 1; // TODO: really calculate width
  4482. props.majorLineTop = -this.top;
  4483. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4484. props.majorLineWidth = 1; // TODO: really calculate width
  4485. props.lineTop = 0;
  4486. break;
  4487. case 'top':
  4488. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4489. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4490. props.majorLabelTop = 0;
  4491. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4492. props.minorLineTop = props.minorLabelTop;
  4493. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4494. props.minorLineWidth = 1; // TODO: really calculate width
  4495. props.majorLineTop = 0;
  4496. props.majorLineHeight = Math.max(parentHeight - this.top);
  4497. props.majorLineWidth = 1; // TODO: really calculate width
  4498. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4499. break;
  4500. default:
  4501. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4502. }
  4503. var height = props.minorLabelHeight + props.majorLabelHeight;
  4504. changed += update(this, 'width', frame.offsetWidth);
  4505. changed += update(this, 'height', height);
  4506. // calculate range and step
  4507. this._updateConversion();
  4508. var start = util.convert(range.start, 'Number'),
  4509. end = util.convert(range.end, 'Number'),
  4510. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4511. -this.toTime(0).valueOf();
  4512. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4513. changed += update(props.range, 'start', start);
  4514. changed += update(props.range, 'end', end);
  4515. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4516. }
  4517. return (changed > 0);
  4518. };
  4519. /**
  4520. * Calculate the scale and offset to convert a position on screen to the
  4521. * corresponding date and vice versa.
  4522. * After the method _updateConversion is executed once, the methods toTime
  4523. * and toScreen can be used.
  4524. * @private
  4525. */
  4526. TimeAxis.prototype._updateConversion = function() {
  4527. var range = this.range;
  4528. if (!range) {
  4529. throw new Error('No range configured');
  4530. }
  4531. if (range.conversion) {
  4532. this.conversion = range.conversion(this.width);
  4533. }
  4534. else {
  4535. this.conversion = Range.conversion(range.start, range.end, this.width);
  4536. }
  4537. };
  4538. /**
  4539. * Snap a date to a rounded value.
  4540. * The snap intervals are dependent on the current scale and step.
  4541. * @param {Date} date the date to be snapped.
  4542. * @return {Date} snappedDate
  4543. */
  4544. TimeAxis.prototype.snap = function snap (date) {
  4545. return this.step.snap(date);
  4546. };
  4547. /**
  4548. * A current time bar
  4549. * @param {Component} parent
  4550. * @param {Component[]} [depends] Components on which this components depends
  4551. * (except for the parent)
  4552. * @param {Object} [options] Available parameters:
  4553. * {Boolean} [showCurrentTime]
  4554. * @constructor CurrentTime
  4555. * @extends Component
  4556. */
  4557. function CurrentTime (parent, depends, options) {
  4558. this.id = util.randomUUID();
  4559. this.parent = parent;
  4560. this.depends = depends;
  4561. this.options = options || {};
  4562. this.defaultOptions = {
  4563. showCurrentTime: false
  4564. };
  4565. }
  4566. CurrentTime.prototype = new Component();
  4567. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4568. /**
  4569. * Get the container element of the bar, which can be used by a child to
  4570. * add its own widgets.
  4571. * @returns {HTMLElement} container
  4572. */
  4573. CurrentTime.prototype.getContainer = function () {
  4574. return this.frame;
  4575. };
  4576. /**
  4577. * Repaint the component
  4578. * @return {Boolean} changed
  4579. */
  4580. CurrentTime.prototype.repaint = function () {
  4581. var bar = this.frame,
  4582. parent = this.parent,
  4583. parentContainer = parent.parent.getContainer();
  4584. if (!parent) {
  4585. throw new Error('Cannot repaint bar: no parent attached');
  4586. }
  4587. if (!parentContainer) {
  4588. throw new Error('Cannot repaint bar: parent has no container element');
  4589. }
  4590. if (!this.getOption('showCurrentTime')) {
  4591. if (bar) {
  4592. parentContainer.removeChild(bar);
  4593. delete this.frame;
  4594. }
  4595. return false;
  4596. }
  4597. if (!bar) {
  4598. bar = document.createElement('div');
  4599. bar.className = 'currenttime';
  4600. bar.style.position = 'absolute';
  4601. bar.style.top = '0px';
  4602. bar.style.height = '100%';
  4603. parentContainer.appendChild(bar);
  4604. this.frame = bar;
  4605. }
  4606. if (!parent.conversion) {
  4607. parent._updateConversion();
  4608. }
  4609. var now = new Date();
  4610. var x = parent.toScreen(now);
  4611. bar.style.left = x + 'px';
  4612. bar.title = 'Current time: ' + now;
  4613. // start a timer to adjust for the new time
  4614. if (this.currentTimeTimer !== undefined) {
  4615. clearTimeout(this.currentTimeTimer);
  4616. delete this.currentTimeTimer;
  4617. }
  4618. var timeline = this;
  4619. var interval = 1 / parent.conversion.scale / 2;
  4620. if (interval < 30) {
  4621. interval = 30;
  4622. }
  4623. this.currentTimeTimer = setTimeout(function() {
  4624. timeline.repaint();
  4625. }, interval);
  4626. return false;
  4627. };
  4628. /**
  4629. * A custom time bar
  4630. * @param {Component} parent
  4631. * @param {Component[]} [depends] Components on which this components depends
  4632. * (except for the parent)
  4633. * @param {Object} [options] Available parameters:
  4634. * {Boolean} [showCustomTime]
  4635. * @constructor CustomTime
  4636. * @extends Component
  4637. */
  4638. function CustomTime (parent, depends, options) {
  4639. this.id = util.randomUUID();
  4640. this.parent = parent;
  4641. this.depends = depends;
  4642. this.options = options || {};
  4643. this.defaultOptions = {
  4644. showCustomTime: false
  4645. };
  4646. this.customTime = new Date();
  4647. this.eventParams = {}; // stores state parameters while dragging the bar
  4648. }
  4649. CustomTime.prototype = new Component();
  4650. Emitter(CustomTime.prototype);
  4651. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4652. /**
  4653. * Get the container element of the bar, which can be used by a child to
  4654. * add its own widgets.
  4655. * @returns {HTMLElement} container
  4656. */
  4657. CustomTime.prototype.getContainer = function () {
  4658. return this.frame;
  4659. };
  4660. /**
  4661. * Repaint the component
  4662. * @return {Boolean} changed
  4663. */
  4664. CustomTime.prototype.repaint = function () {
  4665. var bar = this.frame,
  4666. parent = this.parent;
  4667. if (!parent) {
  4668. throw new Error('Cannot repaint bar: no parent attached');
  4669. }
  4670. var parentContainer = parent.parent.getContainer();
  4671. if (!parentContainer) {
  4672. throw new Error('Cannot repaint bar: parent has no container element');
  4673. }
  4674. if (!this.getOption('showCustomTime')) {
  4675. if (bar) {
  4676. parentContainer.removeChild(bar);
  4677. delete this.frame;
  4678. }
  4679. return false;
  4680. }
  4681. if (!bar) {
  4682. bar = document.createElement('div');
  4683. bar.className = 'customtime';
  4684. bar.style.position = 'absolute';
  4685. bar.style.top = '0px';
  4686. bar.style.height = '100%';
  4687. parentContainer.appendChild(bar);
  4688. var drag = document.createElement('div');
  4689. drag.style.position = 'relative';
  4690. drag.style.top = '0px';
  4691. drag.style.left = '-10px';
  4692. drag.style.height = '100%';
  4693. drag.style.width = '20px';
  4694. bar.appendChild(drag);
  4695. this.frame = bar;
  4696. // attach event listeners
  4697. this.hammer = Hammer(bar, {
  4698. prevent_default: true
  4699. });
  4700. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4701. this.hammer.on('drag', this._onDrag.bind(this));
  4702. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4703. }
  4704. if (!parent.conversion) {
  4705. parent._updateConversion();
  4706. }
  4707. var x = parent.toScreen(this.customTime);
  4708. bar.style.left = x + 'px';
  4709. bar.title = 'Time: ' + this.customTime;
  4710. return false;
  4711. };
  4712. /**
  4713. * Set custom time.
  4714. * @param {Date} time
  4715. */
  4716. CustomTime.prototype.setCustomTime = function(time) {
  4717. this.customTime = new Date(time.valueOf());
  4718. this.repaint();
  4719. };
  4720. /**
  4721. * Retrieve the current custom time.
  4722. * @return {Date} customTime
  4723. */
  4724. CustomTime.prototype.getCustomTime = function() {
  4725. return new Date(this.customTime.valueOf());
  4726. };
  4727. /**
  4728. * Start moving horizontally
  4729. * @param {Event} event
  4730. * @private
  4731. */
  4732. CustomTime.prototype._onDragStart = function(event) {
  4733. this.eventParams.customTime = this.customTime;
  4734. event.stopPropagation();
  4735. event.preventDefault();
  4736. };
  4737. /**
  4738. * Perform moving operating.
  4739. * @param {Event} event
  4740. * @private
  4741. */
  4742. CustomTime.prototype._onDrag = function (event) {
  4743. var deltaX = event.gesture.deltaX,
  4744. x = this.parent.toScreen(this.eventParams.customTime) + deltaX,
  4745. time = this.parent.toTime(x);
  4746. this.setCustomTime(time);
  4747. // fire a timechange event
  4748. if (this.controller) {
  4749. this.controller.emit('timechange', {
  4750. time: this.customTime
  4751. })
  4752. }
  4753. event.stopPropagation();
  4754. event.preventDefault();
  4755. };
  4756. /**
  4757. * Stop moving operating.
  4758. * @param {event} event
  4759. * @private
  4760. */
  4761. CustomTime.prototype._onDragEnd = function (event) {
  4762. // fire a timechanged event
  4763. if (this.controller) {
  4764. this.controller.emit('timechanged', {
  4765. time: this.customTime
  4766. })
  4767. }
  4768. event.stopPropagation();
  4769. event.preventDefault();
  4770. };
  4771. /**
  4772. * An ItemSet holds a set of items and ranges which can be displayed in a
  4773. * range. The width is determined by the parent of the ItemSet, and the height
  4774. * is determined by the size of the items.
  4775. * @param {Component} parent
  4776. * @param {Component[]} [depends] Components on which this components depends
  4777. * (except for the parent)
  4778. * @param {Object} [options] See ItemSet.setOptions for the available
  4779. * options.
  4780. * @constructor ItemSet
  4781. * @extends Panel
  4782. */
  4783. // TODO: improve performance by replacing all Array.forEach with a for loop
  4784. function ItemSet(parent, depends, options) {
  4785. this.id = util.randomUUID();
  4786. this.parent = parent;
  4787. this.depends = depends;
  4788. // event listeners
  4789. this.eventListeners = {
  4790. dragstart: this._onDragStart.bind(this),
  4791. drag: this._onDrag.bind(this),
  4792. dragend: this._onDragEnd.bind(this)
  4793. };
  4794. // one options object is shared by this itemset and all its items
  4795. this.options = options || {};
  4796. this.defaultOptions = {
  4797. type: 'box',
  4798. align: 'center',
  4799. orientation: 'bottom',
  4800. margin: {
  4801. axis: 20,
  4802. item: 10
  4803. },
  4804. padding: 5
  4805. };
  4806. this.dom = {};
  4807. var me = this;
  4808. this.itemsData = null; // DataSet
  4809. this.range = null; // Range or Object {start: number, end: number}
  4810. // data change listeners
  4811. this.listeners = {
  4812. 'add': function (event, params, senderId) {
  4813. if (senderId != me.id) {
  4814. me._onAdd(params.items);
  4815. }
  4816. },
  4817. 'update': function (event, params, senderId) {
  4818. if (senderId != me.id) {
  4819. me._onUpdate(params.items);
  4820. }
  4821. },
  4822. 'remove': function (event, params, senderId) {
  4823. if (senderId != me.id) {
  4824. me._onRemove(params.items);
  4825. }
  4826. }
  4827. };
  4828. this.items = {}; // object with an Item for every data item
  4829. this.selection = []; // list with the ids of all selected nodes
  4830. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4831. this.stack = new Stack(this, Object.create(this.options));
  4832. this.conversion = null;
  4833. this.touchParams = {}; // stores properties while dragging
  4834. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4835. }
  4836. ItemSet.prototype = new Panel();
  4837. // available item types will be registered here
  4838. ItemSet.types = {
  4839. box: ItemBox,
  4840. range: ItemRange,
  4841. rangeoverflow: ItemRangeOverflow,
  4842. point: ItemPoint
  4843. };
  4844. /**
  4845. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4846. * @param {Object} [options] The following options are available:
  4847. * {String | function} [className]
  4848. * class name for the itemset
  4849. * {String} [type]
  4850. * Default type for the items. Choose from 'box'
  4851. * (default), 'point', or 'range'. The default
  4852. * Style can be overwritten by individual items.
  4853. * {String} align
  4854. * Alignment for the items, only applicable for
  4855. * ItemBox. Choose 'center' (default), 'left', or
  4856. * 'right'.
  4857. * {String} orientation
  4858. * Orientation of the item set. Choose 'top' or
  4859. * 'bottom' (default).
  4860. * {Number} margin.axis
  4861. * Margin between the axis and the items in pixels.
  4862. * Default is 20.
  4863. * {Number} margin.item
  4864. * Margin between items in pixels. Default is 10.
  4865. * {Number} padding
  4866. * Padding of the contents of an item in pixels.
  4867. * Must correspond with the items css. Default is 5.
  4868. * {Function} snap
  4869. * Function to let items snap to nice dates when
  4870. * dragging items.
  4871. */
  4872. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4873. /**
  4874. * Set controller for this component
  4875. * @param {Controller | null} controller
  4876. */
  4877. ItemSet.prototype.setController = function setController (controller) {
  4878. var event;
  4879. // unregister old event listeners
  4880. if (this.controller) {
  4881. for (event in this.eventListeners) {
  4882. if (this.eventListeners.hasOwnProperty(event)) {
  4883. this.controller.off(event, this.eventListeners[event]);
  4884. }
  4885. }
  4886. }
  4887. this.controller = controller || null;
  4888. // register new event listeners
  4889. if (this.controller) {
  4890. for (event in this.eventListeners) {
  4891. if (this.eventListeners.hasOwnProperty(event)) {
  4892. this.controller.on(event, this.eventListeners[event]);
  4893. }
  4894. }
  4895. }
  4896. };
  4897. // attach event listeners for dragging items to the controller
  4898. (function (me) {
  4899. var _controller = null;
  4900. var _onDragStart = null;
  4901. var _onDrag = null;
  4902. var _onDragEnd = null;
  4903. Object.defineProperty(me, 'controller', {
  4904. get: function () {
  4905. return _controller;
  4906. },
  4907. set: function (controller) {
  4908. }
  4909. });
  4910. }) (this);
  4911. /**
  4912. * Set range (start and end).
  4913. * @param {Range | Object} range A Range or an object containing start and end.
  4914. */
  4915. ItemSet.prototype.setRange = function setRange(range) {
  4916. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4917. throw new TypeError('Range must be an instance of Range, ' +
  4918. 'or an object containing start and end.');
  4919. }
  4920. this.range = range;
  4921. };
  4922. /**
  4923. * Set selected items by their id. Replaces the current selection
  4924. * Unknown id's are silently ignored.
  4925. * @param {Array} [ids] An array with zero or more id's of the items to be
  4926. * selected. If ids is an empty array, all items will be
  4927. * unselected.
  4928. */
  4929. ItemSet.prototype.setSelection = function setSelection(ids) {
  4930. var i, ii, id, item, selection;
  4931. if (ids) {
  4932. if (!Array.isArray(ids)) {
  4933. throw new TypeError('Array expected');
  4934. }
  4935. // unselect currently selected items
  4936. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4937. id = this.selection[i];
  4938. item = this.items[id];
  4939. if (item) item.unselect();
  4940. }
  4941. // select items
  4942. this.selection = [];
  4943. for (i = 0, ii = ids.length; i < ii; i++) {
  4944. id = ids[i];
  4945. item = this.items[id];
  4946. if (item) {
  4947. this.selection.push(id);
  4948. item.select();
  4949. }
  4950. }
  4951. if (this.controller) {
  4952. this.requestRepaint();
  4953. }
  4954. }
  4955. };
  4956. /**
  4957. * Get the selected items by their id
  4958. * @return {Array} ids The ids of the selected items
  4959. */
  4960. ItemSet.prototype.getSelection = function getSelection() {
  4961. return this.selection.concat([]);
  4962. };
  4963. /**
  4964. * Deselect a selected item
  4965. * @param {String | Number} id
  4966. * @private
  4967. */
  4968. ItemSet.prototype._deselect = function _deselect(id) {
  4969. var selection = this.selection;
  4970. for (var i = 0, ii = selection.length; i < ii; i++) {
  4971. if (selection[i] == id) { // non-strict comparison!
  4972. selection.splice(i, 1);
  4973. break;
  4974. }
  4975. }
  4976. };
  4977. /**
  4978. * Repaint the component
  4979. * @return {Boolean} changed
  4980. */
  4981. ItemSet.prototype.repaint = function repaint() {
  4982. var changed = 0,
  4983. update = util.updateProperty,
  4984. asSize = util.option.asSize,
  4985. options = this.options,
  4986. orientation = this.getOption('orientation'),
  4987. defaultOptions = this.defaultOptions,
  4988. frame = this.frame;
  4989. if (!frame) {
  4990. frame = document.createElement('div');
  4991. frame.className = 'itemset';
  4992. frame['timeline-itemset'] = this;
  4993. var className = options.className;
  4994. if (className) {
  4995. util.addClassName(frame, util.option.asString(className));
  4996. }
  4997. // create background panel
  4998. var background = document.createElement('div');
  4999. background.className = 'background';
  5000. frame.appendChild(background);
  5001. this.dom.background = background;
  5002. // create foreground panel
  5003. var foreground = document.createElement('div');
  5004. foreground.className = 'foreground';
  5005. frame.appendChild(foreground);
  5006. this.dom.foreground = foreground;
  5007. // create axis panel
  5008. var axis = document.createElement('div');
  5009. axis.className = 'itemset-axis';
  5010. //frame.appendChild(axis);
  5011. this.dom.axis = axis;
  5012. this.frame = frame;
  5013. changed += 1;
  5014. }
  5015. if (!this.parent) {
  5016. throw new Error('Cannot repaint itemset: no parent attached');
  5017. }
  5018. var parentContainer = this.parent.getContainer();
  5019. if (!parentContainer) {
  5020. throw new Error('Cannot repaint itemset: parent has no container element');
  5021. }
  5022. if (!frame.parentNode) {
  5023. parentContainer.appendChild(frame);
  5024. changed += 1;
  5025. }
  5026. if (!this.dom.axis.parentNode) {
  5027. parentContainer.appendChild(this.dom.axis);
  5028. changed += 1;
  5029. }
  5030. // reposition frame
  5031. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  5032. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  5033. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  5034. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  5035. // reposition axis
  5036. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  5037. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  5038. if (orientation == 'bottom') {
  5039. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  5040. }
  5041. else { // orientation == 'top'
  5042. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  5043. }
  5044. this._updateConversion();
  5045. var me = this,
  5046. queue = this.queue,
  5047. itemsData = this.itemsData,
  5048. items = this.items,
  5049. dataOptions = {
  5050. // TODO: cleanup
  5051. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  5052. };
  5053. // show/hide added/changed/removed items
  5054. for (var id in queue) {
  5055. if (queue.hasOwnProperty(id)) {
  5056. var entry = queue[id],
  5057. item = items[id],
  5058. action = entry.action;
  5059. //noinspection FallthroughInSwitchStatementJS
  5060. switch (action) {
  5061. case 'add':
  5062. case 'update':
  5063. var itemData = itemsData && itemsData.get(id, dataOptions);
  5064. if (itemData) {
  5065. var type = itemData.type ||
  5066. (itemData.start && itemData.end && 'range') ||
  5067. options.type ||
  5068. 'box';
  5069. var constructor = ItemSet.types[type];
  5070. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  5071. if (item) {
  5072. // update item
  5073. if (!constructor || !(item instanceof constructor)) {
  5074. // item type has changed, hide and delete the item
  5075. changed += item.hide();
  5076. item = null;
  5077. }
  5078. else {
  5079. item.data = itemData; // TODO: create a method item.setData ?
  5080. changed++;
  5081. }
  5082. }
  5083. if (!item) {
  5084. // create item
  5085. if (constructor) {
  5086. item = new constructor(me, itemData, options, defaultOptions);
  5087. item.id = entry.id; // we take entry.id, as id itself is stringified
  5088. changed++;
  5089. }
  5090. else {
  5091. throw new TypeError('Unknown item type "' + type + '"');
  5092. }
  5093. }
  5094. // force a repaint (not only a reposition)
  5095. item.repaint();
  5096. items[id] = item;
  5097. }
  5098. // update queue
  5099. delete queue[id];
  5100. break;
  5101. case 'remove':
  5102. if (item) {
  5103. // remove the item from the set selected items
  5104. if (item.selected) {
  5105. me._deselect(id);
  5106. }
  5107. // remove DOM of the item
  5108. changed += item.hide();
  5109. }
  5110. // update lists
  5111. delete items[id];
  5112. delete queue[id];
  5113. break;
  5114. default:
  5115. console.log('Error: unknown action "' + action + '"');
  5116. }
  5117. }
  5118. }
  5119. // reposition all items. Show items only when in the visible area
  5120. util.forEach(this.items, function (item) {
  5121. if (item.visible) {
  5122. changed += item.show();
  5123. item.reposition();
  5124. }
  5125. else {
  5126. changed += item.hide();
  5127. }
  5128. });
  5129. return (changed > 0);
  5130. };
  5131. /**
  5132. * Get the foreground container element
  5133. * @return {HTMLElement} foreground
  5134. */
  5135. ItemSet.prototype.getForeground = function getForeground() {
  5136. return this.dom.foreground;
  5137. };
  5138. /**
  5139. * Get the background container element
  5140. * @return {HTMLElement} background
  5141. */
  5142. ItemSet.prototype.getBackground = function getBackground() {
  5143. return this.dom.background;
  5144. };
  5145. /**
  5146. * Get the axis container element
  5147. * @return {HTMLElement} axis
  5148. */
  5149. ItemSet.prototype.getAxis = function getAxis() {
  5150. return this.dom.axis;
  5151. };
  5152. /**
  5153. * Reflow the component
  5154. * @return {Boolean} resized
  5155. */
  5156. ItemSet.prototype.reflow = function reflow () {
  5157. var changed = 0,
  5158. options = this.options,
  5159. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  5160. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  5161. update = util.updateProperty,
  5162. asNumber = util.option.asNumber,
  5163. asSize = util.option.asSize,
  5164. frame = this.frame;
  5165. if (frame) {
  5166. this._updateConversion();
  5167. util.forEach(this.items, function (item) {
  5168. changed += item.reflow();
  5169. });
  5170. // TODO: stack.update should be triggered via an event, in stack itself
  5171. // TODO: only update the stack when there are changed items
  5172. this.stack.update();
  5173. var maxHeight = asNumber(options.maxHeight);
  5174. var fixedHeight = (asSize(options.height) != null);
  5175. var height;
  5176. if (fixedHeight) {
  5177. height = frame.offsetHeight;
  5178. }
  5179. else {
  5180. // height is not specified, determine the height from the height and positioned items
  5181. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  5182. if (visibleItems.length) {
  5183. var min = visibleItems[0].top;
  5184. var max = visibleItems[0].top + visibleItems[0].height;
  5185. util.forEach(visibleItems, function (item) {
  5186. min = Math.min(min, item.top);
  5187. max = Math.max(max, (item.top + item.height));
  5188. });
  5189. height = (max - min) + marginAxis + marginItem;
  5190. }
  5191. else {
  5192. height = marginAxis + marginItem;
  5193. }
  5194. }
  5195. if (maxHeight != null) {
  5196. height = Math.min(height, maxHeight);
  5197. }
  5198. changed += update(this, 'height', height);
  5199. // calculate height from items
  5200. changed += update(this, 'top', frame.offsetTop);
  5201. changed += update(this, 'left', frame.offsetLeft);
  5202. changed += update(this, 'width', frame.offsetWidth);
  5203. }
  5204. else {
  5205. changed += 1;
  5206. }
  5207. return (changed > 0);
  5208. };
  5209. /**
  5210. * Hide this component from the DOM
  5211. * @return {Boolean} changed
  5212. */
  5213. ItemSet.prototype.hide = function hide() {
  5214. var changed = false;
  5215. // remove the DOM
  5216. if (this.frame && this.frame.parentNode) {
  5217. this.frame.parentNode.removeChild(this.frame);
  5218. changed = true;
  5219. }
  5220. if (this.dom.axis && this.dom.axis.parentNode) {
  5221. this.dom.axis.parentNode.removeChild(this.dom.axis);
  5222. changed = true;
  5223. }
  5224. return changed;
  5225. };
  5226. /**
  5227. * Set items
  5228. * @param {vis.DataSet | null} items
  5229. */
  5230. ItemSet.prototype.setItems = function setItems(items) {
  5231. var me = this,
  5232. ids,
  5233. oldItemsData = this.itemsData;
  5234. // replace the dataset
  5235. if (!items) {
  5236. this.itemsData = null;
  5237. }
  5238. else if (items instanceof DataSet || items instanceof DataView) {
  5239. this.itemsData = items;
  5240. }
  5241. else {
  5242. throw new TypeError('Data must be an instance of DataSet');
  5243. }
  5244. if (oldItemsData) {
  5245. // unsubscribe from old dataset
  5246. util.forEach(this.listeners, function (callback, event) {
  5247. oldItemsData.unsubscribe(event, callback);
  5248. });
  5249. // remove all drawn items
  5250. ids = oldItemsData.getIds();
  5251. this._onRemove(ids);
  5252. }
  5253. if (this.itemsData) {
  5254. // subscribe to new dataset
  5255. var id = this.id;
  5256. util.forEach(this.listeners, function (callback, event) {
  5257. me.itemsData.on(event, callback, id);
  5258. });
  5259. // draw all new items
  5260. ids = this.itemsData.getIds();
  5261. this._onAdd(ids);
  5262. }
  5263. };
  5264. /**
  5265. * Get the current items items
  5266. * @returns {vis.DataSet | null}
  5267. */
  5268. ItemSet.prototype.getItems = function getItems() {
  5269. return this.itemsData;
  5270. };
  5271. /**
  5272. * Remove an item by its id
  5273. * @param {String | Number} id
  5274. */
  5275. ItemSet.prototype.removeItem = function removeItem (id) {
  5276. var item = this.itemsData.get(id),
  5277. dataset = this._myDataSet();
  5278. if (item) {
  5279. // confirm deletion
  5280. this.options.onRemove(item, function (item) {
  5281. if (item) {
  5282. dataset.remove(item);
  5283. }
  5284. });
  5285. }
  5286. };
  5287. /**
  5288. * Handle updated items
  5289. * @param {Number[]} ids
  5290. * @private
  5291. */
  5292. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  5293. this._toQueue('update', ids);
  5294. };
  5295. /**
  5296. * Handle changed items
  5297. * @param {Number[]} ids
  5298. * @private
  5299. */
  5300. ItemSet.prototype._onAdd = function _onAdd(ids) {
  5301. this._toQueue('add', ids);
  5302. };
  5303. /**
  5304. * Handle removed items
  5305. * @param {Number[]} ids
  5306. * @private
  5307. */
  5308. ItemSet.prototype._onRemove = function _onRemove(ids) {
  5309. this._toQueue('remove', ids);
  5310. };
  5311. /**
  5312. * Put items in the queue to be added/updated/remove
  5313. * @param {String} action can be 'add', 'update', 'remove'
  5314. * @param {Number[]} ids
  5315. */
  5316. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  5317. var queue = this.queue;
  5318. ids.forEach(function (id) {
  5319. queue[id] = {
  5320. id: id,
  5321. action: action
  5322. };
  5323. });
  5324. if (this.controller) {
  5325. //this.requestReflow();
  5326. this.requestRepaint();
  5327. }
  5328. };
  5329. /**
  5330. * Calculate the scale and offset to convert a position on screen to the
  5331. * corresponding date and vice versa.
  5332. * After the method _updateConversion is executed once, the methods toTime
  5333. * and toScreen can be used.
  5334. * @private
  5335. */
  5336. ItemSet.prototype._updateConversion = function _updateConversion() {
  5337. var range = this.range;
  5338. if (!range) {
  5339. throw new Error('No range configured');
  5340. }
  5341. if (range.conversion) {
  5342. this.conversion = range.conversion(this.width);
  5343. }
  5344. else {
  5345. this.conversion = Range.conversion(range.start, range.end, this.width);
  5346. }
  5347. };
  5348. /**
  5349. * Convert a position on screen (pixels) to a datetime
  5350. * Before this method can be used, the method _updateConversion must be
  5351. * executed once.
  5352. * @param {int} x Position on the screen in pixels
  5353. * @return {Date} time The datetime the corresponds with given position x
  5354. */
  5355. ItemSet.prototype.toTime = function toTime(x) {
  5356. var conversion = this.conversion;
  5357. return new Date(x / conversion.scale + conversion.offset);
  5358. };
  5359. /**
  5360. * Convert a datetime (Date object) into a position on the screen
  5361. * Before this method can be used, the method _updateConversion must be
  5362. * executed once.
  5363. * @param {Date} time A date
  5364. * @return {int} x The position on the screen in pixels which corresponds
  5365. * with the given date.
  5366. */
  5367. ItemSet.prototype.toScreen = function toScreen(time) {
  5368. var conversion = this.conversion;
  5369. return (time.valueOf() - conversion.offset) * conversion.scale;
  5370. };
  5371. /**
  5372. * Start dragging the selected events
  5373. * @param {Event} event
  5374. * @private
  5375. */
  5376. ItemSet.prototype._onDragStart = function (event) {
  5377. if (!this.options.editable) {
  5378. return;
  5379. }
  5380. var item = ItemSet.itemFromTarget(event),
  5381. me = this;
  5382. if (item && item.selected) {
  5383. var dragLeftItem = event.target.dragLeftItem;
  5384. var dragRightItem = event.target.dragRightItem;
  5385. if (dragLeftItem) {
  5386. this.touchParams.itemProps = [{
  5387. item: dragLeftItem,
  5388. start: item.data.start.valueOf()
  5389. }];
  5390. }
  5391. else if (dragRightItem) {
  5392. this.touchParams.itemProps = [{
  5393. item: dragRightItem,
  5394. end: item.data.end.valueOf()
  5395. }];
  5396. }
  5397. else {
  5398. this.touchParams.itemProps = this.getSelection().map(function (id) {
  5399. var item = me.items[id];
  5400. var props = {
  5401. item: item
  5402. };
  5403. if ('start' in item.data) {
  5404. props.start = item.data.start.valueOf()
  5405. }
  5406. if ('end' in item.data) {
  5407. props.end = item.data.end.valueOf()
  5408. }
  5409. return props;
  5410. });
  5411. }
  5412. event.stopPropagation();
  5413. }
  5414. };
  5415. /**
  5416. * Drag selected items
  5417. * @param {Event} event
  5418. * @private
  5419. */
  5420. ItemSet.prototype._onDrag = function (event) {
  5421. if (this.touchParams.itemProps) {
  5422. var snap = this.options.snap || null,
  5423. deltaX = event.gesture.deltaX,
  5424. offset = deltaX / this.conversion.scale;
  5425. // move
  5426. this.touchParams.itemProps.forEach(function (props) {
  5427. if ('start' in props) {
  5428. var start = new Date(props.start + offset);
  5429. props.item.data.start = snap ? snap(start) : start;
  5430. }
  5431. if ('end' in props) {
  5432. var end = new Date(props.end + offset);
  5433. props.item.data.end = snap ? snap(end) : end;
  5434. }
  5435. });
  5436. // TODO: implement onMoving handler
  5437. // TODO: implement dragging from one group to another
  5438. this.requestReflow();
  5439. event.stopPropagation();
  5440. }
  5441. };
  5442. /**
  5443. * End of dragging selected items
  5444. * @param {Event} event
  5445. * @private
  5446. */
  5447. ItemSet.prototype._onDragEnd = function (event) {
  5448. if (this.touchParams.itemProps) {
  5449. // prepare a change set for the changed items
  5450. var changes = [],
  5451. me = this,
  5452. dataset = this._myDataSet(),
  5453. type;
  5454. this.touchParams.itemProps.forEach(function (props) {
  5455. var id = props.item.id,
  5456. item = me.itemsData.get(id);
  5457. var changed = false;
  5458. if ('start' in props.item.data) {
  5459. changed = (props.start != props.item.data.start.valueOf());
  5460. item.start = util.convert(props.item.data.start, dataset.convert['start']);
  5461. }
  5462. if ('end' in props.item.data) {
  5463. changed = changed || (props.end != props.item.data.end.valueOf());
  5464. item.end = util.convert(props.item.data.end, dataset.convert['end']);
  5465. }
  5466. // only apply changes when start or end is actually changed
  5467. if (changed) {
  5468. me.options.onMove(item, function (item) {
  5469. if (item) {
  5470. // apply changes
  5471. changes.push(item);
  5472. }
  5473. else {
  5474. // restore original values
  5475. if ('start' in props) props.item.data.start = props.start;
  5476. if ('end' in props) props.item.data.end = props.end;
  5477. me.requestReflow();
  5478. }
  5479. });
  5480. }
  5481. });
  5482. this.touchParams.itemProps = null;
  5483. // apply the changes to the data (if there are changes)
  5484. if (changes.length) {
  5485. dataset.update(changes);
  5486. }
  5487. event.stopPropagation();
  5488. }
  5489. };
  5490. /**
  5491. * Find an item from an event target:
  5492. * searches for the attribute 'timeline-item' in the event target's element tree
  5493. * @param {Event} event
  5494. * @return {Item | null} item
  5495. */
  5496. ItemSet.itemFromTarget = function itemFromTarget (event) {
  5497. var target = event.target;
  5498. while (target) {
  5499. if (target.hasOwnProperty('timeline-item')) {
  5500. return target['timeline-item'];
  5501. }
  5502. target = target.parentNode;
  5503. }
  5504. return null;
  5505. };
  5506. /**
  5507. * Find the ItemSet from an event target:
  5508. * searches for the attribute 'timeline-itemset' in the event target's element tree
  5509. * @param {Event} event
  5510. * @return {ItemSet | null} item
  5511. */
  5512. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  5513. var target = event.target;
  5514. while (target) {
  5515. if (target.hasOwnProperty('timeline-itemset')) {
  5516. return target['timeline-itemset'];
  5517. }
  5518. target = target.parentNode;
  5519. }
  5520. return null;
  5521. };
  5522. /**
  5523. * Find the DataSet to which this ItemSet is connected
  5524. * @returns {null | DataSet} dataset
  5525. * @private
  5526. */
  5527. ItemSet.prototype._myDataSet = function _myDataSet() {
  5528. // find the root DataSet
  5529. var dataset = this.itemsData;
  5530. while (dataset instanceof DataView) {
  5531. dataset = dataset.data;
  5532. }
  5533. return dataset;
  5534. };
  5535. /**
  5536. * @constructor Item
  5537. * @param {ItemSet} parent
  5538. * @param {Object} data Object containing (optional) parameters type,
  5539. * start, end, content, group, className.
  5540. * @param {Object} [options] Options to set initial property values
  5541. * @param {Object} [defaultOptions] default options
  5542. * // TODO: describe available options
  5543. */
  5544. function Item (parent, data, options, defaultOptions) {
  5545. this.parent = parent;
  5546. this.data = data;
  5547. this.dom = null;
  5548. this.options = options || {};
  5549. this.defaultOptions = defaultOptions || {};
  5550. this.selected = false;
  5551. this.visible = false;
  5552. this.top = 0;
  5553. this.left = 0;
  5554. this.width = 0;
  5555. this.height = 0;
  5556. this.offset = 0;
  5557. }
  5558. /**
  5559. * Select current item
  5560. */
  5561. Item.prototype.select = function select() {
  5562. this.selected = true;
  5563. if (this.visible) this.repaint();
  5564. };
  5565. /**
  5566. * Unselect current item
  5567. */
  5568. Item.prototype.unselect = function unselect() {
  5569. this.selected = false;
  5570. if (this.visible) this.repaint();
  5571. };
  5572. /**
  5573. * Show the Item in the DOM (when not already visible)
  5574. * @return {Boolean} changed
  5575. */
  5576. Item.prototype.show = function show() {
  5577. return false;
  5578. };
  5579. /**
  5580. * Hide the Item from the DOM (when visible)
  5581. * @return {Boolean} changed
  5582. */
  5583. Item.prototype.hide = function hide() {
  5584. return false;
  5585. };
  5586. /**
  5587. * Repaint the item
  5588. * @return {Boolean} changed
  5589. */
  5590. Item.prototype.repaint = function repaint() {
  5591. // should be implemented by the item
  5592. return false;
  5593. };
  5594. /**
  5595. * Reflow the item
  5596. * @return {Boolean} resized
  5597. */
  5598. Item.prototype.reflow = function reflow() {
  5599. // should be implemented by the item
  5600. return false;
  5601. };
  5602. /**
  5603. * Give the item a display offset in pixels
  5604. * @param {Number} offset Offset on screen in pixels
  5605. */
  5606. Item.prototype.setOffset = function setOffset(offset) {
  5607. this.offset = offset;
  5608. };
  5609. /**
  5610. * Repaint a delete button on the top right of the item when the item is selected
  5611. * @param {HTMLElement} anchor
  5612. * @private
  5613. */
  5614. Item.prototype._repaintDeleteButton = function (anchor) {
  5615. if (this.selected && this.options.editable && !this.dom.deleteButton) {
  5616. // create and show button
  5617. var parent = this.parent;
  5618. var id = this.id;
  5619. var deleteButton = document.createElement('div');
  5620. deleteButton.className = 'delete';
  5621. deleteButton.title = 'Delete this item';
  5622. Hammer(deleteButton, {
  5623. preventDefault: true
  5624. }).on('tap', function (event) {
  5625. parent.removeItem(id);
  5626. event.stopPropagation();
  5627. });
  5628. anchor.appendChild(deleteButton);
  5629. this.dom.deleteButton = deleteButton;
  5630. }
  5631. else if (!this.selected && this.dom.deleteButton) {
  5632. // remove button
  5633. if (this.dom.deleteButton.parentNode) {
  5634. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  5635. }
  5636. this.dom.deleteButton = null;
  5637. }
  5638. };
  5639. /**
  5640. * @constructor ItemBox
  5641. * @extends Item
  5642. * @param {ItemSet} parent
  5643. * @param {Object} data Object containing parameters start
  5644. * content, className.
  5645. * @param {Object} [options] Options to set initial property values
  5646. * @param {Object} [defaultOptions] default options
  5647. * // TODO: describe available options
  5648. */
  5649. function ItemBox (parent, data, options, defaultOptions) {
  5650. this.props = {
  5651. dot: {
  5652. left: 0,
  5653. top: 0,
  5654. width: 0,
  5655. height: 0
  5656. },
  5657. line: {
  5658. top: 0,
  5659. left: 0,
  5660. width: 0,
  5661. height: 0
  5662. }
  5663. };
  5664. Item.call(this, parent, data, options, defaultOptions);
  5665. }
  5666. ItemBox.prototype = new Item (null, null);
  5667. /**
  5668. * Repaint the item
  5669. * @return {Boolean} changed
  5670. */
  5671. ItemBox.prototype.repaint = function repaint() {
  5672. // TODO: make an efficient repaint
  5673. var changed = false;
  5674. var dom = this.dom;
  5675. if (!dom) {
  5676. this._create();
  5677. dom = this.dom;
  5678. changed = true;
  5679. }
  5680. if (dom) {
  5681. if (!this.parent) {
  5682. throw new Error('Cannot repaint item: no parent attached');
  5683. }
  5684. if (!dom.box.parentNode) {
  5685. var foreground = this.parent.getForeground();
  5686. if (!foreground) {
  5687. throw new Error('Cannot repaint time axis: ' +
  5688. 'parent has no foreground container element');
  5689. }
  5690. foreground.appendChild(dom.box);
  5691. changed = true;
  5692. }
  5693. if (!dom.line.parentNode) {
  5694. var background = this.parent.getBackground();
  5695. if (!background) {
  5696. throw new Error('Cannot repaint time axis: ' +
  5697. 'parent has no background container element');
  5698. }
  5699. background.appendChild(dom.line);
  5700. changed = true;
  5701. }
  5702. if (!dom.dot.parentNode) {
  5703. var axis = this.parent.getAxis();
  5704. if (!background) {
  5705. throw new Error('Cannot repaint time axis: ' +
  5706. 'parent has no axis container element');
  5707. }
  5708. axis.appendChild(dom.dot);
  5709. changed = true;
  5710. }
  5711. this._repaintDeleteButton(dom.box);
  5712. // update contents
  5713. if (this.data.content != this.content) {
  5714. this.content = this.data.content;
  5715. if (this.content instanceof Element) {
  5716. dom.content.innerHTML = '';
  5717. dom.content.appendChild(this.content);
  5718. }
  5719. else if (this.data.content != undefined) {
  5720. dom.content.innerHTML = this.content;
  5721. }
  5722. else {
  5723. throw new Error('Property "content" missing in item ' + this.data.id);
  5724. }
  5725. changed = true;
  5726. }
  5727. // update class
  5728. var className = (this.data.className? ' ' + this.data.className : '') +
  5729. (this.selected ? ' selected' : '');
  5730. if (this.className != className) {
  5731. this.className = className;
  5732. dom.box.className = 'item box' + className;
  5733. dom.line.className = 'item line' + className;
  5734. dom.dot.className = 'item dot' + className;
  5735. changed = true;
  5736. }
  5737. }
  5738. return changed;
  5739. };
  5740. /**
  5741. * Show the item in the DOM (when not already visible). The items DOM will
  5742. * be created when needed.
  5743. * @return {Boolean} changed
  5744. */
  5745. ItemBox.prototype.show = function show() {
  5746. if (!this.dom || !this.dom.box.parentNode) {
  5747. return this.repaint();
  5748. }
  5749. else {
  5750. return false;
  5751. }
  5752. };
  5753. /**
  5754. * Hide the item from the DOM (when visible)
  5755. * @return {Boolean} changed
  5756. */
  5757. ItemBox.prototype.hide = function hide() {
  5758. var changed = false,
  5759. dom = this.dom;
  5760. if (dom) {
  5761. if (dom.box.parentNode) {
  5762. dom.box.parentNode.removeChild(dom.box);
  5763. changed = true;
  5764. }
  5765. if (dom.line.parentNode) {
  5766. dom.line.parentNode.removeChild(dom.line);
  5767. }
  5768. if (dom.dot.parentNode) {
  5769. dom.dot.parentNode.removeChild(dom.dot);
  5770. }
  5771. }
  5772. return changed;
  5773. };
  5774. /**
  5775. * Reflow the item: calculate its actual size and position from the DOM
  5776. * @return {boolean} resized returns true if the axis is resized
  5777. * @override
  5778. */
  5779. ItemBox.prototype.reflow = function reflow() {
  5780. var changed = 0,
  5781. update,
  5782. dom,
  5783. props,
  5784. options,
  5785. margin,
  5786. start,
  5787. align,
  5788. orientation,
  5789. top,
  5790. left,
  5791. data,
  5792. range;
  5793. if (this.data.start == undefined) {
  5794. throw new Error('Property "start" missing in item ' + this.data.id);
  5795. }
  5796. data = this.data;
  5797. range = this.parent && this.parent.range;
  5798. if (data && range) {
  5799. // TODO: account for the width of the item
  5800. var interval = (range.end - range.start);
  5801. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5802. }
  5803. else {
  5804. this.visible = false;
  5805. }
  5806. if (this.visible) {
  5807. dom = this.dom;
  5808. if (dom) {
  5809. update = util.updateProperty;
  5810. props = this.props;
  5811. options = this.options;
  5812. start = this.parent.toScreen(this.data.start) + this.offset;
  5813. align = options.align || this.defaultOptions.align;
  5814. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5815. orientation = options.orientation || this.defaultOptions.orientation;
  5816. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5817. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5818. changed += update(props.line, 'width', dom.line.offsetWidth);
  5819. changed += update(props.line, 'height', dom.line.offsetHeight);
  5820. changed += update(props.line, 'top', dom.line.offsetTop);
  5821. changed += update(this, 'width', dom.box.offsetWidth);
  5822. changed += update(this, 'height', dom.box.offsetHeight);
  5823. if (align == 'right') {
  5824. left = start - this.width;
  5825. }
  5826. else if (align == 'left') {
  5827. left = start;
  5828. }
  5829. else {
  5830. // default or 'center'
  5831. left = start - this.width / 2;
  5832. }
  5833. changed += update(this, 'left', left);
  5834. changed += update(props.line, 'left', start - props.line.width / 2);
  5835. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5836. changed += update(props.dot, 'top', -props.dot.height / 2);
  5837. if (orientation == 'top') {
  5838. top = margin;
  5839. changed += update(this, 'top', top);
  5840. }
  5841. else {
  5842. // default or 'bottom'
  5843. var parentHeight = this.parent.height;
  5844. top = parentHeight - this.height - margin;
  5845. changed += update(this, 'top', top);
  5846. }
  5847. }
  5848. else {
  5849. changed += 1;
  5850. }
  5851. }
  5852. return (changed > 0);
  5853. };
  5854. /**
  5855. * Create an items DOM
  5856. * @private
  5857. */
  5858. ItemBox.prototype._create = function _create() {
  5859. var dom = this.dom;
  5860. if (!dom) {
  5861. this.dom = dom = {};
  5862. // create the box
  5863. dom.box = document.createElement('DIV');
  5864. // className is updated in repaint()
  5865. // contents box (inside the background box). used for making margins
  5866. dom.content = document.createElement('DIV');
  5867. dom.content.className = 'content';
  5868. dom.box.appendChild(dom.content);
  5869. // line to axis
  5870. dom.line = document.createElement('DIV');
  5871. dom.line.className = 'line';
  5872. // dot on axis
  5873. dom.dot = document.createElement('DIV');
  5874. dom.dot.className = 'dot';
  5875. // attach this item as attribute
  5876. dom.box['timeline-item'] = this;
  5877. }
  5878. };
  5879. /**
  5880. * Reposition the item, recalculate its left, top, and width, using the current
  5881. * range and size of the items itemset
  5882. * @override
  5883. */
  5884. ItemBox.prototype.reposition = function reposition() {
  5885. var dom = this.dom,
  5886. props = this.props,
  5887. orientation = this.options.orientation || this.defaultOptions.orientation;
  5888. if (dom) {
  5889. var box = dom.box,
  5890. line = dom.line,
  5891. dot = dom.dot;
  5892. box.style.left = this.left + 'px';
  5893. box.style.top = this.top + 'px';
  5894. line.style.left = props.line.left + 'px';
  5895. if (orientation == 'top') {
  5896. line.style.top = 0 + 'px';
  5897. line.style.height = this.top + 'px';
  5898. }
  5899. else {
  5900. // orientation 'bottom'
  5901. line.style.top = (this.top + this.height) + 'px';
  5902. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5903. this.props.dot.height / 2, 0) + 'px';
  5904. }
  5905. dot.style.left = props.dot.left + 'px';
  5906. dot.style.top = props.dot.top + 'px';
  5907. }
  5908. };
  5909. /**
  5910. * @constructor ItemPoint
  5911. * @extends Item
  5912. * @param {ItemSet} parent
  5913. * @param {Object} data Object containing parameters start
  5914. * content, className.
  5915. * @param {Object} [options] Options to set initial property values
  5916. * @param {Object} [defaultOptions] default options
  5917. * // TODO: describe available options
  5918. */
  5919. function ItemPoint (parent, data, options, defaultOptions) {
  5920. this.props = {
  5921. dot: {
  5922. top: 0,
  5923. width: 0,
  5924. height: 0
  5925. },
  5926. content: {
  5927. height: 0,
  5928. marginLeft: 0
  5929. }
  5930. };
  5931. Item.call(this, parent, data, options, defaultOptions);
  5932. }
  5933. ItemPoint.prototype = new Item (null, null);
  5934. /**
  5935. * Repaint the item
  5936. * @return {Boolean} changed
  5937. */
  5938. ItemPoint.prototype.repaint = function repaint() {
  5939. // TODO: make an efficient repaint
  5940. var changed = false;
  5941. var dom = this.dom;
  5942. if (!dom) {
  5943. this._create();
  5944. dom = this.dom;
  5945. changed = true;
  5946. }
  5947. if (dom) {
  5948. if (!this.parent) {
  5949. throw new Error('Cannot repaint item: no parent attached');
  5950. }
  5951. var foreground = this.parent.getForeground();
  5952. if (!foreground) {
  5953. throw new Error('Cannot repaint time axis: ' +
  5954. 'parent has no foreground container element');
  5955. }
  5956. if (!dom.point.parentNode) {
  5957. foreground.appendChild(dom.point);
  5958. foreground.appendChild(dom.point);
  5959. changed = true;
  5960. }
  5961. // update contents
  5962. if (this.data.content != this.content) {
  5963. this.content = this.data.content;
  5964. if (this.content instanceof Element) {
  5965. dom.content.innerHTML = '';
  5966. dom.content.appendChild(this.content);
  5967. }
  5968. else if (this.data.content != undefined) {
  5969. dom.content.innerHTML = this.content;
  5970. }
  5971. else {
  5972. throw new Error('Property "content" missing in item ' + this.data.id);
  5973. }
  5974. changed = true;
  5975. }
  5976. this._repaintDeleteButton(dom.point);
  5977. // update class
  5978. var className = (this.data.className? ' ' + this.data.className : '') +
  5979. (this.selected ? ' selected' : '');
  5980. if (this.className != className) {
  5981. this.className = className;
  5982. dom.point.className = 'item point' + className;
  5983. changed = true;
  5984. }
  5985. }
  5986. return changed;
  5987. };
  5988. /**
  5989. * Show the item in the DOM (when not already visible). The items DOM will
  5990. * be created when needed.
  5991. * @return {Boolean} changed
  5992. */
  5993. ItemPoint.prototype.show = function show() {
  5994. if (!this.dom || !this.dom.point.parentNode) {
  5995. return this.repaint();
  5996. }
  5997. else {
  5998. return false;
  5999. }
  6000. };
  6001. /**
  6002. * Hide the item from the DOM (when visible)
  6003. * @return {Boolean} changed
  6004. */
  6005. ItemPoint.prototype.hide = function hide() {
  6006. var changed = false,
  6007. dom = this.dom;
  6008. if (dom) {
  6009. if (dom.point.parentNode) {
  6010. dom.point.parentNode.removeChild(dom.point);
  6011. changed = true;
  6012. }
  6013. }
  6014. return changed;
  6015. };
  6016. /**
  6017. * Reflow the item: calculate its actual size from the DOM
  6018. * @return {boolean} resized returns true if the axis is resized
  6019. * @override
  6020. */
  6021. ItemPoint.prototype.reflow = function reflow() {
  6022. var changed = 0,
  6023. update,
  6024. dom,
  6025. props,
  6026. options,
  6027. margin,
  6028. orientation,
  6029. start,
  6030. top,
  6031. data,
  6032. range;
  6033. if (this.data.start == undefined) {
  6034. throw new Error('Property "start" missing in item ' + this.data.id);
  6035. }
  6036. data = this.data;
  6037. range = this.parent && this.parent.range;
  6038. if (data && range) {
  6039. // TODO: account for the width of the item
  6040. var interval = (range.end - range.start);
  6041. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  6042. }
  6043. else {
  6044. this.visible = false;
  6045. }
  6046. if (this.visible) {
  6047. dom = this.dom;
  6048. if (dom) {
  6049. update = util.updateProperty;
  6050. props = this.props;
  6051. options = this.options;
  6052. orientation = options.orientation || this.defaultOptions.orientation;
  6053. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  6054. start = this.parent.toScreen(this.data.start) + this.offset;
  6055. changed += update(this, 'width', dom.point.offsetWidth);
  6056. changed += update(this, 'height', dom.point.offsetHeight);
  6057. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  6058. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  6059. changed += update(props.content, 'height', dom.content.offsetHeight);
  6060. if (orientation == 'top') {
  6061. top = margin;
  6062. }
  6063. else {
  6064. // default or 'bottom'
  6065. var parentHeight = this.parent.height;
  6066. top = Math.max(parentHeight - this.height - margin, 0);
  6067. }
  6068. changed += update(this, 'top', top);
  6069. changed += update(this, 'left', start - props.dot.width / 2);
  6070. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  6071. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  6072. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  6073. }
  6074. else {
  6075. changed += 1;
  6076. }
  6077. }
  6078. return (changed > 0);
  6079. };
  6080. /**
  6081. * Create an items DOM
  6082. * @private
  6083. */
  6084. ItemPoint.prototype._create = function _create() {
  6085. var dom = this.dom;
  6086. if (!dom) {
  6087. this.dom = dom = {};
  6088. // background box
  6089. dom.point = document.createElement('div');
  6090. // className is updated in repaint()
  6091. // contents box, right from the dot
  6092. dom.content = document.createElement('div');
  6093. dom.content.className = 'content';
  6094. dom.point.appendChild(dom.content);
  6095. // dot at start
  6096. dom.dot = document.createElement('div');
  6097. dom.dot.className = 'dot';
  6098. dom.point.appendChild(dom.dot);
  6099. // attach this item as attribute
  6100. dom.point['timeline-item'] = this;
  6101. }
  6102. };
  6103. /**
  6104. * Reposition the item, recalculate its left, top, and width, using the current
  6105. * range and size of the items itemset
  6106. * @override
  6107. */
  6108. ItemPoint.prototype.reposition = function reposition() {
  6109. var dom = this.dom,
  6110. props = this.props;
  6111. if (dom) {
  6112. dom.point.style.top = this.top + 'px';
  6113. dom.point.style.left = this.left + 'px';
  6114. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  6115. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  6116. dom.dot.style.top = props.dot.top + 'px';
  6117. }
  6118. };
  6119. /**
  6120. * @constructor ItemRange
  6121. * @extends Item
  6122. * @param {ItemSet} parent
  6123. * @param {Object} data Object containing parameters start, end
  6124. * content, className.
  6125. * @param {Object} [options] Options to set initial property values
  6126. * @param {Object} [defaultOptions] default options
  6127. * // TODO: describe available options
  6128. */
  6129. function ItemRange (parent, data, options, defaultOptions) {
  6130. this.props = {
  6131. content: {
  6132. left: 0,
  6133. width: 0
  6134. }
  6135. };
  6136. Item.call(this, parent, data, options, defaultOptions);
  6137. }
  6138. ItemRange.prototype = new Item (null, null);
  6139. /**
  6140. * Repaint the item
  6141. * @return {Boolean} changed
  6142. */
  6143. ItemRange.prototype.repaint = function repaint() {
  6144. // TODO: make an efficient repaint
  6145. var changed = false;
  6146. var dom = this.dom;
  6147. if (!dom) {
  6148. this._create();
  6149. dom = this.dom;
  6150. changed = true;
  6151. }
  6152. if (dom) {
  6153. if (!this.parent) {
  6154. throw new Error('Cannot repaint item: no parent attached');
  6155. }
  6156. var foreground = this.parent.getForeground();
  6157. if (!foreground) {
  6158. throw new Error('Cannot repaint time axis: ' +
  6159. 'parent has no foreground container element');
  6160. }
  6161. if (!dom.box.parentNode) {
  6162. foreground.appendChild(dom.box);
  6163. changed = true;
  6164. }
  6165. // update content
  6166. if (this.data.content != this.content) {
  6167. this.content = this.data.content;
  6168. if (this.content instanceof Element) {
  6169. dom.content.innerHTML = '';
  6170. dom.content.appendChild(this.content);
  6171. }
  6172. else if (this.data.content != undefined) {
  6173. dom.content.innerHTML = this.content;
  6174. }
  6175. else {
  6176. throw new Error('Property "content" missing in item ' + this.data.id);
  6177. }
  6178. changed = true;
  6179. }
  6180. this._repaintDeleteButton(dom.box);
  6181. this._repaintDragLeft();
  6182. this._repaintDragRight();
  6183. // update class
  6184. var className = (this.data.className ? (' ' + this.data.className) : '') +
  6185. (this.selected ? ' selected' : '');
  6186. if (this.className != className) {
  6187. this.className = className;
  6188. dom.box.className = 'item range' + className;
  6189. changed = true;
  6190. }
  6191. }
  6192. return changed;
  6193. };
  6194. /**
  6195. * Show the item in the DOM (when not already visible). The items DOM will
  6196. * be created when needed.
  6197. * @return {Boolean} changed
  6198. */
  6199. ItemRange.prototype.show = function show() {
  6200. if (!this.dom || !this.dom.box.parentNode) {
  6201. return this.repaint();
  6202. }
  6203. else {
  6204. return false;
  6205. }
  6206. };
  6207. /**
  6208. * Hide the item from the DOM (when visible)
  6209. * @return {Boolean} changed
  6210. */
  6211. ItemRange.prototype.hide = function hide() {
  6212. var changed = false,
  6213. dom = this.dom;
  6214. if (dom) {
  6215. if (dom.box.parentNode) {
  6216. dom.box.parentNode.removeChild(dom.box);
  6217. changed = true;
  6218. }
  6219. }
  6220. return changed;
  6221. };
  6222. /**
  6223. * Reflow the item: calculate its actual size from the DOM
  6224. * @return {boolean} resized returns true if the axis is resized
  6225. * @override
  6226. */
  6227. ItemRange.prototype.reflow = function reflow() {
  6228. var changed = 0,
  6229. dom,
  6230. props,
  6231. options,
  6232. margin,
  6233. padding,
  6234. parent,
  6235. start,
  6236. end,
  6237. data,
  6238. range,
  6239. update,
  6240. box,
  6241. parentWidth,
  6242. contentLeft,
  6243. orientation,
  6244. top;
  6245. if (this.data.start == undefined) {
  6246. throw new Error('Property "start" missing in item ' + this.data.id);
  6247. }
  6248. if (this.data.end == undefined) {
  6249. throw new Error('Property "end" missing in item ' + this.data.id);
  6250. }
  6251. data = this.data;
  6252. range = this.parent && this.parent.range;
  6253. if (data && range) {
  6254. // TODO: account for the width of the item. Take some margin
  6255. this.visible = (data.start < range.end) && (data.end > range.start);
  6256. }
  6257. else {
  6258. this.visible = false;
  6259. }
  6260. if (this.visible) {
  6261. dom = this.dom;
  6262. if (dom) {
  6263. props = this.props;
  6264. options = this.options;
  6265. parent = this.parent;
  6266. start = parent.toScreen(this.data.start) + this.offset;
  6267. end = parent.toScreen(this.data.end) + this.offset;
  6268. update = util.updateProperty;
  6269. box = dom.box;
  6270. parentWidth = parent.width;
  6271. orientation = options.orientation || this.defaultOptions.orientation;
  6272. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  6273. padding = options.padding || this.defaultOptions.padding;
  6274. changed += update(props.content, 'width', dom.content.offsetWidth);
  6275. changed += update(this, 'height', box.offsetHeight);
  6276. // limit the width of the this, as browsers cannot draw very wide divs
  6277. if (start < -parentWidth) {
  6278. start = -parentWidth;
  6279. }
  6280. if (end > 2 * parentWidth) {
  6281. end = 2 * parentWidth;
  6282. }
  6283. // when range exceeds left of the window, position the contents at the left of the visible area
  6284. if (start < 0) {
  6285. contentLeft = Math.min(-start,
  6286. (end - start - props.content.width - 2 * padding));
  6287. // TODO: remove the need for options.padding. it's terrible.
  6288. }
  6289. else {
  6290. contentLeft = 0;
  6291. }
  6292. changed += update(props.content, 'left', contentLeft);
  6293. if (orientation == 'top') {
  6294. top = margin;
  6295. changed += update(this, 'top', top);
  6296. }
  6297. else {
  6298. // default or 'bottom'
  6299. top = parent.height - this.height - margin;
  6300. changed += update(this, 'top', top);
  6301. }
  6302. changed += update(this, 'left', start);
  6303. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  6304. }
  6305. else {
  6306. changed += 1;
  6307. }
  6308. }
  6309. return (changed > 0);
  6310. };
  6311. /**
  6312. * Create an items DOM
  6313. * @private
  6314. */
  6315. ItemRange.prototype._create = function _create() {
  6316. var dom = this.dom;
  6317. if (!dom) {
  6318. this.dom = dom = {};
  6319. // background box
  6320. dom.box = document.createElement('div');
  6321. // className is updated in repaint()
  6322. // contents box
  6323. dom.content = document.createElement('div');
  6324. dom.content.className = 'content';
  6325. dom.box.appendChild(dom.content);
  6326. // attach this item as attribute
  6327. dom.box['timeline-item'] = this;
  6328. }
  6329. };
  6330. /**
  6331. * Reposition the item, recalculate its left, top, and width, using the current
  6332. * range and size of the items itemset
  6333. * @override
  6334. */
  6335. ItemRange.prototype.reposition = function reposition() {
  6336. var dom = this.dom,
  6337. props = this.props;
  6338. if (dom) {
  6339. dom.box.style.top = this.top + 'px';
  6340. dom.box.style.left = this.left + 'px';
  6341. dom.box.style.width = this.width + 'px';
  6342. dom.content.style.left = props.content.left + 'px';
  6343. }
  6344. };
  6345. /**
  6346. * Repaint a drag area on the left side of the range when the range is selected
  6347. * @private
  6348. */
  6349. ItemRange.prototype._repaintDragLeft = function () {
  6350. if (this.selected && this.options.editable && !this.dom.dragLeft) {
  6351. // create and show drag area
  6352. var dragLeft = document.createElement('div');
  6353. dragLeft.className = 'drag-left';
  6354. dragLeft.dragLeftItem = this;
  6355. // TODO: this should be redundant?
  6356. Hammer(dragLeft, {
  6357. preventDefault: true
  6358. }).on('drag', function () {
  6359. //console.log('drag left')
  6360. });
  6361. this.dom.box.appendChild(dragLeft);
  6362. this.dom.dragLeft = dragLeft;
  6363. }
  6364. else if (!this.selected && this.dom.dragLeft) {
  6365. // delete drag area
  6366. if (this.dom.dragLeft.parentNode) {
  6367. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  6368. }
  6369. this.dom.dragLeft = null;
  6370. }
  6371. };
  6372. /**
  6373. * Repaint a drag area on the right side of the range when the range is selected
  6374. * @private
  6375. */
  6376. ItemRange.prototype._repaintDragRight = function () {
  6377. if (this.selected && this.options.editable && !this.dom.dragRight) {
  6378. // create and show drag area
  6379. var dragRight = document.createElement('div');
  6380. dragRight.className = 'drag-right';
  6381. dragRight.dragRightItem = this;
  6382. // TODO: this should be redundant?
  6383. Hammer(dragRight, {
  6384. preventDefault: true
  6385. }).on('drag', function () {
  6386. //console.log('drag right')
  6387. });
  6388. this.dom.box.appendChild(dragRight);
  6389. this.dom.dragRight = dragRight;
  6390. }
  6391. else if (!this.selected && this.dom.dragRight) {
  6392. // delete drag area
  6393. if (this.dom.dragRight.parentNode) {
  6394. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  6395. }
  6396. this.dom.dragRight = null;
  6397. }
  6398. };
  6399. /**
  6400. * @constructor ItemRangeOverflow
  6401. * @extends ItemRange
  6402. * @param {ItemSet} parent
  6403. * @param {Object} data Object containing parameters start, end
  6404. * content, className.
  6405. * @param {Object} [options] Options to set initial property values
  6406. * @param {Object} [defaultOptions] default options
  6407. * // TODO: describe available options
  6408. */
  6409. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  6410. this.props = {
  6411. content: {
  6412. left: 0,
  6413. width: 0
  6414. }
  6415. };
  6416. // define a private property _width, which is the with of the range box
  6417. // adhering to the ranges start and end date. The property width has a
  6418. // getter which returns the max of border width and content width
  6419. this._width = 0;
  6420. Object.defineProperty(this, 'width', {
  6421. get: function () {
  6422. return (this.props.content && this._width < this.props.content.width) ?
  6423. this.props.content.width :
  6424. this._width;
  6425. },
  6426. set: function (width) {
  6427. this._width = width;
  6428. }
  6429. });
  6430. ItemRange.call(this, parent, data, options, defaultOptions);
  6431. }
  6432. ItemRangeOverflow.prototype = new ItemRange (null, null);
  6433. /**
  6434. * Repaint the item
  6435. * @return {Boolean} changed
  6436. */
  6437. ItemRangeOverflow.prototype.repaint = function repaint() {
  6438. // TODO: make an efficient repaint
  6439. var changed = false;
  6440. var dom = this.dom;
  6441. if (!dom) {
  6442. this._create();
  6443. dom = this.dom;
  6444. changed = true;
  6445. }
  6446. if (dom) {
  6447. if (!this.parent) {
  6448. throw new Error('Cannot repaint item: no parent attached');
  6449. }
  6450. var foreground = this.parent.getForeground();
  6451. if (!foreground) {
  6452. throw new Error('Cannot repaint time axis: ' +
  6453. 'parent has no foreground container element');
  6454. }
  6455. if (!dom.box.parentNode) {
  6456. foreground.appendChild(dom.box);
  6457. changed = true;
  6458. }
  6459. // update content
  6460. if (this.data.content != this.content) {
  6461. this.content = this.data.content;
  6462. if (this.content instanceof Element) {
  6463. dom.content.innerHTML = '';
  6464. dom.content.appendChild(this.content);
  6465. }
  6466. else if (this.data.content != undefined) {
  6467. dom.content.innerHTML = this.content;
  6468. }
  6469. else {
  6470. throw new Error('Property "content" missing in item ' + this.id);
  6471. }
  6472. changed = true;
  6473. }
  6474. this._repaintDeleteButton(dom.box);
  6475. this._repaintDragLeft();
  6476. this._repaintDragRight();
  6477. // update class
  6478. var className = (this.data.className? ' ' + this.data.className : '') +
  6479. (this.selected ? ' selected' : '');
  6480. if (this.className != className) {
  6481. this.className = className;
  6482. dom.box.className = 'item rangeoverflow' + className;
  6483. changed = true;
  6484. }
  6485. }
  6486. return changed;
  6487. };
  6488. /**
  6489. * Reposition the item, recalculate its left, top, and width, using the current
  6490. * range and size of the items itemset
  6491. * @override
  6492. */
  6493. ItemRangeOverflow.prototype.reposition = function reposition() {
  6494. var dom = this.dom,
  6495. props = this.props;
  6496. if (dom) {
  6497. dom.box.style.top = this.top + 'px';
  6498. dom.box.style.left = this.left + 'px';
  6499. dom.box.style.width = this._width + 'px';
  6500. dom.content.style.left = props.content.left + 'px';
  6501. }
  6502. };
  6503. /**
  6504. * @constructor Group
  6505. * @param {GroupSet} parent
  6506. * @param {Number | String} groupId
  6507. * @param {Object} [options] Options to set initial property values
  6508. * // TODO: describe available options
  6509. * @extends Component
  6510. */
  6511. function Group (parent, groupId, options) {
  6512. this.id = util.randomUUID();
  6513. this.parent = parent;
  6514. this.groupId = groupId;
  6515. this.itemset = null; // ItemSet
  6516. this.options = options || {};
  6517. this.options.top = 0;
  6518. this.props = {
  6519. label: {
  6520. width: 0,
  6521. height: 0
  6522. }
  6523. };
  6524. this.top = 0;
  6525. this.left = 0;
  6526. this.width = 0;
  6527. this.height = 0;
  6528. }
  6529. Group.prototype = new Component();
  6530. // TODO: comment
  6531. Group.prototype.setOptions = Component.prototype.setOptions;
  6532. /**
  6533. * Get the container element of the panel, which can be used by a child to
  6534. * add its own widgets.
  6535. * @returns {HTMLElement} container
  6536. */
  6537. Group.prototype.getContainer = function () {
  6538. return this.parent.getContainer();
  6539. };
  6540. /**
  6541. * Set item set for the group. The group will create a view on the itemset,
  6542. * filtered by the groups id.
  6543. * @param {DataSet | DataView} items
  6544. */
  6545. Group.prototype.setItems = function setItems(items) {
  6546. if (this.itemset) {
  6547. // remove current item set
  6548. this.itemset.hide();
  6549. this.itemset.setItems();
  6550. this.parent.controller.remove(this.itemset);
  6551. this.itemset = null;
  6552. }
  6553. if (items) {
  6554. var groupId = this.groupId;
  6555. var itemsetOptions = Object.create(this.options);
  6556. this.itemset = new ItemSet(this, null, itemsetOptions);
  6557. this.itemset.setRange(this.parent.range);
  6558. this.view = new DataView(items, {
  6559. filter: function (item) {
  6560. return item.group == groupId;
  6561. }
  6562. });
  6563. this.itemset.setItems(this.view);
  6564. this.parent.controller.add(this.itemset);
  6565. }
  6566. };
  6567. /**
  6568. * Set selected items by their id. Replaces the current selection.
  6569. * Unknown id's are silently ignored.
  6570. * @param {Array} [ids] An array with zero or more id's of the items to be
  6571. * selected. If ids is an empty array, all items will be
  6572. * unselected.
  6573. */
  6574. Group.prototype.setSelection = function setSelection(ids) {
  6575. if (this.itemset) this.itemset.setSelection(ids);
  6576. };
  6577. /**
  6578. * Get the selected items by their id
  6579. * @return {Array} ids The ids of the selected items
  6580. */
  6581. Group.prototype.getSelection = function getSelection() {
  6582. return this.itemset ? this.itemset.getSelection() : [];
  6583. };
  6584. /**
  6585. * Repaint the item
  6586. * @return {Boolean} changed
  6587. */
  6588. Group.prototype.repaint = function repaint() {
  6589. return false;
  6590. };
  6591. /**
  6592. * Reflow the item
  6593. * @return {Boolean} resized
  6594. */
  6595. Group.prototype.reflow = function reflow() {
  6596. var changed = 0,
  6597. update = util.updateProperty;
  6598. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  6599. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  6600. // TODO: reckon with the height of the group label
  6601. if (this.label) {
  6602. var inner = this.label.firstChild;
  6603. changed += update(this.props.label, 'width', inner.clientWidth);
  6604. changed += update(this.props.label, 'height', inner.clientHeight);
  6605. }
  6606. else {
  6607. changed += update(this.props.label, 'width', 0);
  6608. changed += update(this.props.label, 'height', 0);
  6609. }
  6610. return (changed > 0);
  6611. };
  6612. /**
  6613. * An GroupSet holds a set of groups
  6614. * @param {Component} parent
  6615. * @param {Component[]} [depends] Components on which this components depends
  6616. * (except for the parent)
  6617. * @param {Object} [options] See GroupSet.setOptions for the available
  6618. * options.
  6619. * @constructor GroupSet
  6620. * @extends Panel
  6621. */
  6622. function GroupSet(parent, depends, options) {
  6623. this.id = util.randomUUID();
  6624. this.parent = parent;
  6625. this.depends = depends;
  6626. this.options = options || {};
  6627. this.range = null; // Range or Object {start: number, end: number}
  6628. this.itemsData = null; // DataSet with items
  6629. this.groupsData = null; // DataSet with groups
  6630. this.groups = {}; // map with groups
  6631. this.dom = {};
  6632. this.props = {
  6633. labels: {
  6634. width: 0
  6635. }
  6636. };
  6637. // TODO: implement right orientation of the labels
  6638. // changes in groups are queued key/value map containing id/action
  6639. this.queue = {};
  6640. var me = this;
  6641. this.listeners = {
  6642. 'add': function (event, params) {
  6643. me._onAdd(params.items);
  6644. },
  6645. 'update': function (event, params) {
  6646. me._onUpdate(params.items);
  6647. },
  6648. 'remove': function (event, params) {
  6649. me._onRemove(params.items);
  6650. }
  6651. };
  6652. }
  6653. GroupSet.prototype = new Panel();
  6654. /**
  6655. * Set options for the GroupSet. Existing options will be extended/overwritten.
  6656. * @param {Object} [options] The following options are available:
  6657. * {String | function} groupsOrder
  6658. * TODO: describe options
  6659. */
  6660. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  6661. GroupSet.prototype.setRange = function (range) {
  6662. // TODO: implement setRange
  6663. };
  6664. /**
  6665. * Set items
  6666. * @param {vis.DataSet | null} items
  6667. */
  6668. GroupSet.prototype.setItems = function setItems(items) {
  6669. this.itemsData = items;
  6670. for (var id in this.groups) {
  6671. if (this.groups.hasOwnProperty(id)) {
  6672. var group = this.groups[id];
  6673. group.setItems(items);
  6674. }
  6675. }
  6676. };
  6677. /**
  6678. * Get items
  6679. * @return {vis.DataSet | null} items
  6680. */
  6681. GroupSet.prototype.getItems = function getItems() {
  6682. return this.itemsData;
  6683. };
  6684. /**
  6685. * Set range (start and end).
  6686. * @param {Range | Object} range A Range or an object containing start and end.
  6687. */
  6688. GroupSet.prototype.setRange = function setRange(range) {
  6689. this.range = range;
  6690. };
  6691. /**
  6692. * Set groups
  6693. * @param {vis.DataSet} groups
  6694. */
  6695. GroupSet.prototype.setGroups = function setGroups(groups) {
  6696. var me = this,
  6697. ids;
  6698. // unsubscribe from current dataset
  6699. if (this.groupsData) {
  6700. util.forEach(this.listeners, function (callback, event) {
  6701. me.groupsData.unsubscribe(event, callback);
  6702. });
  6703. // remove all drawn groups
  6704. ids = this.groupsData.getIds();
  6705. this._onRemove(ids);
  6706. }
  6707. // replace the dataset
  6708. if (!groups) {
  6709. this.groupsData = null;
  6710. }
  6711. else if (groups instanceof DataSet) {
  6712. this.groupsData = groups;
  6713. }
  6714. else {
  6715. this.groupsData = new DataSet({
  6716. convert: {
  6717. start: 'Date',
  6718. end: 'Date'
  6719. }
  6720. });
  6721. this.groupsData.add(groups);
  6722. }
  6723. if (this.groupsData) {
  6724. // subscribe to new dataset
  6725. var id = this.id;
  6726. util.forEach(this.listeners, function (callback, event) {
  6727. me.groupsData.on(event, callback, id);
  6728. });
  6729. // draw all new groups
  6730. ids = this.groupsData.getIds();
  6731. this._onAdd(ids);
  6732. }
  6733. };
  6734. /**
  6735. * Get groups
  6736. * @return {vis.DataSet | null} groups
  6737. */
  6738. GroupSet.prototype.getGroups = function getGroups() {
  6739. return this.groupsData;
  6740. };
  6741. /**
  6742. * Set selected items by their id. Replaces the current selection.
  6743. * Unknown id's are silently ignored.
  6744. * @param {Array} [ids] An array with zero or more id's of the items to be
  6745. * selected. If ids is an empty array, all items will be
  6746. * unselected.
  6747. */
  6748. GroupSet.prototype.setSelection = function setSelection(ids) {
  6749. var selection = [],
  6750. groups = this.groups;
  6751. // iterate over each of the groups
  6752. for (var id in groups) {
  6753. if (groups.hasOwnProperty(id)) {
  6754. var group = groups[id];
  6755. group.setSelection(ids);
  6756. }
  6757. }
  6758. return selection;
  6759. };
  6760. /**
  6761. * Get the selected items by their id
  6762. * @return {Array} ids The ids of the selected items
  6763. */
  6764. GroupSet.prototype.getSelection = function getSelection() {
  6765. var selection = [],
  6766. groups = this.groups;
  6767. // iterate over each of the groups
  6768. for (var id in groups) {
  6769. if (groups.hasOwnProperty(id)) {
  6770. var group = groups[id];
  6771. selection = selection.concat(group.getSelection());
  6772. }
  6773. }
  6774. return selection;
  6775. };
  6776. /**
  6777. * Repaint the component
  6778. * @return {Boolean} changed
  6779. */
  6780. GroupSet.prototype.repaint = function repaint() {
  6781. var changed = 0,
  6782. i, id, group, label,
  6783. update = util.updateProperty,
  6784. asSize = util.option.asSize,
  6785. asElement = util.option.asElement,
  6786. options = this.options,
  6787. frame = this.dom.frame,
  6788. labels = this.dom.labels,
  6789. labelSet = this.dom.labelSet;
  6790. // create frame
  6791. if (!this.parent) {
  6792. throw new Error('Cannot repaint groupset: no parent attached');
  6793. }
  6794. var parentContainer = this.parent.getContainer();
  6795. if (!parentContainer) {
  6796. throw new Error('Cannot repaint groupset: parent has no container element');
  6797. }
  6798. if (!frame) {
  6799. frame = document.createElement('div');
  6800. frame.className = 'groupset';
  6801. frame['timeline-groupset'] = this;
  6802. this.dom.frame = frame;
  6803. var className = options.className;
  6804. if (className) {
  6805. util.addClassName(frame, util.option.asString(className));
  6806. }
  6807. changed += 1;
  6808. }
  6809. if (!frame.parentNode) {
  6810. parentContainer.appendChild(frame);
  6811. changed += 1;
  6812. }
  6813. // create labels
  6814. var labelContainer = asElement(options.labelContainer);
  6815. if (!labelContainer) {
  6816. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6817. }
  6818. if (!labels) {
  6819. labels = document.createElement('div');
  6820. labels.className = 'labels';
  6821. this.dom.labels = labels;
  6822. }
  6823. if (!labelSet) {
  6824. labelSet = document.createElement('div');
  6825. labelSet.className = 'label-set';
  6826. labels.appendChild(labelSet);
  6827. this.dom.labelSet = labelSet;
  6828. }
  6829. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6830. if (labels.parentNode) {
  6831. labels.parentNode.removeChild(labels.parentNode);
  6832. }
  6833. labelContainer.appendChild(labels);
  6834. }
  6835. // reposition frame
  6836. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6837. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6838. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6839. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6840. // reposition labels
  6841. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6842. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6843. var me = this,
  6844. queue = this.queue,
  6845. groups = this.groups,
  6846. groupsData = this.groupsData;
  6847. // show/hide added/changed/removed groups
  6848. var ids = Object.keys(queue);
  6849. if (ids.length) {
  6850. ids.forEach(function (id) {
  6851. var action = queue[id];
  6852. var group = groups[id];
  6853. //noinspection FallthroughInSwitchStatementJS
  6854. switch (action) {
  6855. case 'add':
  6856. case 'update':
  6857. if (!group) {
  6858. var groupOptions = Object.create(me.options);
  6859. util.extend(groupOptions, {
  6860. height: null,
  6861. maxHeight: null
  6862. });
  6863. group = new Group(me, id, groupOptions);
  6864. group.setItems(me.itemsData); // attach items data
  6865. groups[id] = group;
  6866. me.controller.add(group);
  6867. }
  6868. // TODO: update group data
  6869. group.data = groupsData.get(id);
  6870. delete queue[id];
  6871. break;
  6872. case 'remove':
  6873. if (group) {
  6874. group.setItems(); // detach items data
  6875. delete groups[id];
  6876. me.controller.remove(group);
  6877. }
  6878. // update lists
  6879. delete queue[id];
  6880. break;
  6881. default:
  6882. console.log('Error: unknown action "' + action + '"');
  6883. }
  6884. });
  6885. // the groupset depends on each of the groups
  6886. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6887. // TODO: apply dependencies of the groupset
  6888. // update the top positions of the groups in the correct order
  6889. var orderedGroups = this.groupsData.getIds({
  6890. order: this.options.groupOrder
  6891. });
  6892. for (i = 0; i < orderedGroups.length; i++) {
  6893. (function (group, prevGroup) {
  6894. var top = 0;
  6895. if (prevGroup) {
  6896. top = function () {
  6897. // TODO: top must reckon with options.maxHeight
  6898. return prevGroup.top + prevGroup.height;
  6899. }
  6900. }
  6901. group.setOptions({
  6902. top: top
  6903. });
  6904. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6905. }
  6906. // (re)create the labels
  6907. while (labelSet.firstChild) {
  6908. labelSet.removeChild(labelSet.firstChild);
  6909. }
  6910. for (i = 0; i < orderedGroups.length; i++) {
  6911. id = orderedGroups[i];
  6912. label = this._createLabel(id);
  6913. labelSet.appendChild(label);
  6914. }
  6915. changed++;
  6916. }
  6917. // reposition the labels
  6918. // TODO: labels are not displayed correctly when orientation=='top'
  6919. // TODO: width of labelPanel is not immediately updated on a change in groups
  6920. for (id in groups) {
  6921. if (groups.hasOwnProperty(id)) {
  6922. group = groups[id];
  6923. label = group.label;
  6924. if (label) {
  6925. label.style.top = group.top + 'px';
  6926. label.style.height = group.height + 'px';
  6927. }
  6928. }
  6929. }
  6930. return (changed > 0);
  6931. };
  6932. /**
  6933. * Create a label for group with given id
  6934. * @param {Number} id
  6935. * @return {Element} label
  6936. * @private
  6937. */
  6938. GroupSet.prototype._createLabel = function(id) {
  6939. var group = this.groups[id];
  6940. var label = document.createElement('div');
  6941. label.className = 'vlabel';
  6942. var inner = document.createElement('div');
  6943. inner.className = 'inner';
  6944. label.appendChild(inner);
  6945. var content = group.data && group.data.content;
  6946. if (content instanceof Element) {
  6947. inner.appendChild(content);
  6948. }
  6949. else if (content != undefined) {
  6950. inner.innerHTML = content;
  6951. }
  6952. var className = group.data && group.data.className;
  6953. if (className) {
  6954. util.addClassName(label, className);
  6955. }
  6956. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6957. return label;
  6958. };
  6959. /**
  6960. * Get container element
  6961. * @return {HTMLElement} container
  6962. */
  6963. GroupSet.prototype.getContainer = function getContainer() {
  6964. return this.dom.frame;
  6965. };
  6966. /**
  6967. * Get the width of the group labels
  6968. * @return {Number} width
  6969. */
  6970. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6971. return this.props.labels.width;
  6972. };
  6973. /**
  6974. * Reflow the component
  6975. * @return {Boolean} resized
  6976. */
  6977. GroupSet.prototype.reflow = function reflow() {
  6978. var changed = 0,
  6979. id, group,
  6980. options = this.options,
  6981. update = util.updateProperty,
  6982. asNumber = util.option.asNumber,
  6983. asSize = util.option.asSize,
  6984. frame = this.dom.frame;
  6985. if (frame) {
  6986. var maxHeight = asNumber(options.maxHeight);
  6987. var fixedHeight = (asSize(options.height) != null);
  6988. var height;
  6989. if (fixedHeight) {
  6990. height = frame.offsetHeight;
  6991. }
  6992. else {
  6993. // height is not specified, calculate the sum of the height of all groups
  6994. height = 0;
  6995. for (id in this.groups) {
  6996. if (this.groups.hasOwnProperty(id)) {
  6997. group = this.groups[id];
  6998. height += group.height;
  6999. }
  7000. }
  7001. }
  7002. if (maxHeight != null) {
  7003. height = Math.min(height, maxHeight);
  7004. }
  7005. changed += update(this, 'height', height);
  7006. changed += update(this, 'top', frame.offsetTop);
  7007. changed += update(this, 'left', frame.offsetLeft);
  7008. changed += update(this, 'width', frame.offsetWidth);
  7009. }
  7010. // calculate the maximum width of the labels
  7011. var width = 0;
  7012. for (id in this.groups) {
  7013. if (this.groups.hasOwnProperty(id)) {
  7014. group = this.groups[id];
  7015. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  7016. width = Math.max(width, labelWidth);
  7017. }
  7018. }
  7019. changed += update(this.props.labels, 'width', width);
  7020. return (changed > 0);
  7021. };
  7022. /**
  7023. * Hide the component from the DOM
  7024. * @return {Boolean} changed
  7025. */
  7026. GroupSet.prototype.hide = function hide() {
  7027. if (this.dom.frame && this.dom.frame.parentNode) {
  7028. this.dom.frame.parentNode.removeChild(this.dom.frame);
  7029. return true;
  7030. }
  7031. else {
  7032. return false;
  7033. }
  7034. };
  7035. /**
  7036. * Show the component in the DOM (when not already visible).
  7037. * A repaint will be executed when the component is not visible
  7038. * @return {Boolean} changed
  7039. */
  7040. GroupSet.prototype.show = function show() {
  7041. if (!this.dom.frame || !this.dom.frame.parentNode) {
  7042. return this.repaint();
  7043. }
  7044. else {
  7045. return false;
  7046. }
  7047. };
  7048. /**
  7049. * Handle updated groups
  7050. * @param {Number[]} ids
  7051. * @private
  7052. */
  7053. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  7054. this._toQueue(ids, 'update');
  7055. };
  7056. /**
  7057. * Handle changed groups
  7058. * @param {Number[]} ids
  7059. * @private
  7060. */
  7061. GroupSet.prototype._onAdd = function _onAdd(ids) {
  7062. this._toQueue(ids, 'add');
  7063. };
  7064. /**
  7065. * Handle removed groups
  7066. * @param {Number[]} ids
  7067. * @private
  7068. */
  7069. GroupSet.prototype._onRemove = function _onRemove(ids) {
  7070. this._toQueue(ids, 'remove');
  7071. };
  7072. /**
  7073. * Put groups in the queue to be added/updated/remove
  7074. * @param {Number[]} ids
  7075. * @param {String} action can be 'add', 'update', 'remove'
  7076. */
  7077. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  7078. var queue = this.queue;
  7079. ids.forEach(function (id) {
  7080. queue[id] = action;
  7081. });
  7082. if (this.controller) {
  7083. //this.requestReflow();
  7084. this.requestRepaint();
  7085. }
  7086. };
  7087. /**
  7088. * Find the Group from an event target:
  7089. * searches for the attribute 'timeline-groupset' in the event target's element
  7090. * tree, then finds the right group in this groupset
  7091. * @param {Event} event
  7092. * @return {Group | null} group
  7093. */
  7094. GroupSet.groupFromTarget = function groupFromTarget (event) {
  7095. var groupset,
  7096. target = event.target;
  7097. while (target) {
  7098. if (target.hasOwnProperty('timeline-groupset')) {
  7099. groupset = target['timeline-groupset'];
  7100. break;
  7101. }
  7102. target = target.parentNode;
  7103. }
  7104. if (groupset) {
  7105. for (var groupId in groupset.groups) {
  7106. if (groupset.groups.hasOwnProperty(groupId)) {
  7107. var group = groupset.groups[groupId];
  7108. if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) {
  7109. return group;
  7110. }
  7111. }
  7112. }
  7113. }
  7114. return null;
  7115. };
  7116. /**
  7117. * Create a timeline visualization
  7118. * @param {HTMLElement} container
  7119. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  7120. * @param {Object} [options] See Timeline.setOptions for the available options.
  7121. * @constructor
  7122. */
  7123. function Timeline (container, items, options) {
  7124. var me = this;
  7125. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  7126. this.options = {
  7127. orientation: 'bottom',
  7128. autoResize: true,
  7129. editable: false,
  7130. selectable: true,
  7131. snap: null, // will be specified after timeaxis is created
  7132. min: null,
  7133. max: null,
  7134. zoomMin: 10, // milliseconds
  7135. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  7136. // moveable: true, // TODO: option moveable
  7137. // zoomable: true, // TODO: option zoomable
  7138. showMinorLabels: true,
  7139. showMajorLabels: true,
  7140. showCurrentTime: false,
  7141. showCustomTime: false,
  7142. onAdd: function (item, callback) {
  7143. callback(item);
  7144. },
  7145. onUpdate: function (item, callback) {
  7146. callback(item);
  7147. },
  7148. onMove: function (item, callback) {
  7149. callback(item);
  7150. },
  7151. onRemove: function (item, callback) {
  7152. callback(item);
  7153. }
  7154. };
  7155. // controller
  7156. this.controller = new Controller();
  7157. // root panel
  7158. if (!container) {
  7159. throw new Error('No container element provided');
  7160. }
  7161. var rootOptions = Object.create(this.options);
  7162. rootOptions.height = function () {
  7163. // TODO: change to height
  7164. if (me.options.height) {
  7165. // fixed height
  7166. return me.options.height;
  7167. }
  7168. else {
  7169. // auto height
  7170. return (me.timeaxis.height + me.content.height) + 'px';
  7171. }
  7172. };
  7173. this.rootPanel = new RootPanel(container, rootOptions);
  7174. this.controller.add(this.rootPanel);
  7175. // single select (or unselect) when tapping an item
  7176. this.controller.on('tap', this._onSelectItem.bind(this));
  7177. // multi select when holding mouse/touch, or on ctrl+click
  7178. this.controller.on('hold', this._onMultiSelectItem.bind(this));
  7179. // add item on doubletap
  7180. this.controller.on('doubletap', this._onAddItem.bind(this));
  7181. // item panel
  7182. var itemOptions = Object.create(this.options);
  7183. itemOptions.left = function () {
  7184. return me.labelPanel.width;
  7185. };
  7186. itemOptions.width = function () {
  7187. return me.rootPanel.width - me.labelPanel.width;
  7188. };
  7189. itemOptions.top = null;
  7190. itemOptions.height = null;
  7191. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  7192. this.controller.add(this.itemPanel);
  7193. // label panel
  7194. var labelOptions = Object.create(this.options);
  7195. labelOptions.top = null;
  7196. labelOptions.left = null;
  7197. labelOptions.height = null;
  7198. labelOptions.width = function () {
  7199. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  7200. return me.content.getLabelsWidth();
  7201. }
  7202. else {
  7203. return 0;
  7204. }
  7205. };
  7206. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  7207. this.controller.add(this.labelPanel);
  7208. // range
  7209. var rangeOptions = Object.create(this.options);
  7210. this.range = new Range(rangeOptions);
  7211. this.range.setRange(
  7212. now.clone().add('days', -3).valueOf(),
  7213. now.clone().add('days', 4).valueOf()
  7214. );
  7215. this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
  7216. this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
  7217. this.range.on('rangechange', function (properties) {
  7218. var force = true;
  7219. me.controller.emit('rangechange', properties);
  7220. me.controller.emit('request-reflow', force);
  7221. });
  7222. this.range.on('rangechanged', function (properties) {
  7223. var force = true;
  7224. me.controller.emit('rangechanged', properties);
  7225. me.controller.emit('request-reflow', force);
  7226. });
  7227. // time axis
  7228. var timeaxisOptions = Object.create(rootOptions);
  7229. timeaxisOptions.range = this.range;
  7230. timeaxisOptions.left = null;
  7231. timeaxisOptions.top = null;
  7232. timeaxisOptions.width = '100%';
  7233. timeaxisOptions.height = null;
  7234. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  7235. this.timeaxis.setRange(this.range);
  7236. this.controller.add(this.timeaxis);
  7237. this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
  7238. // current time bar
  7239. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  7240. this.controller.add(this.currenttime);
  7241. // custom time bar
  7242. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  7243. this.controller.add(this.customtime);
  7244. // create groupset
  7245. this.setGroups(null);
  7246. this.itemsData = null; // DataSet
  7247. this.groupsData = null; // DataSet
  7248. // apply options
  7249. if (options) {
  7250. this.setOptions(options);
  7251. }
  7252. // create itemset and groupset
  7253. if (items) {
  7254. this.setItems(items);
  7255. }
  7256. }
  7257. /**
  7258. * Add an event listener to the timeline
  7259. * @param {String} event Available events: select, rangechange, rangechanged,
  7260. * timechange, timechanged
  7261. * @param {function} callback
  7262. */
  7263. Timeline.prototype.on = function on (event, callback) {
  7264. this.controller.on(event, callback);
  7265. };
  7266. /**
  7267. * Add an event listener from the timeline
  7268. * @param {String} event
  7269. * @param {function} callback
  7270. */
  7271. Timeline.prototype.off = function off (event, callback) {
  7272. this.controller.off(event, callback);
  7273. };
  7274. /**
  7275. * Set options
  7276. * @param {Object} options TODO: describe the available options
  7277. */
  7278. Timeline.prototype.setOptions = function (options) {
  7279. util.extend(this.options, options);
  7280. // force update of range (apply new min/max etc.)
  7281. // both start and end are optional
  7282. this.range.setRange(options.start, options.end);
  7283. if ('editable' in options || 'selectable' in options) {
  7284. if (this.options.selectable) {
  7285. // force update of selection
  7286. this.setSelection(this.getSelection());
  7287. }
  7288. else {
  7289. // remove selection
  7290. this.setSelection([]);
  7291. }
  7292. }
  7293. // validate the callback functions
  7294. var validateCallback = (function (fn) {
  7295. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  7296. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  7297. }
  7298. }).bind(this);
  7299. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  7300. this.controller.reflow();
  7301. this.controller.repaint();
  7302. };
  7303. /**
  7304. * Set a custom time bar
  7305. * @param {Date} time
  7306. */
  7307. Timeline.prototype.setCustomTime = function (time) {
  7308. if (!this.customtime) {
  7309. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7310. }
  7311. this.customtime.setCustomTime(time);
  7312. };
  7313. /**
  7314. * Retrieve the current custom time.
  7315. * @return {Date} customTime
  7316. */
  7317. Timeline.prototype.getCustomTime = function() {
  7318. if (!this.customtime) {
  7319. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7320. }
  7321. return this.customtime.getCustomTime();
  7322. };
  7323. /**
  7324. * Set items
  7325. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  7326. */
  7327. Timeline.prototype.setItems = function(items) {
  7328. var initialLoad = (this.itemsData == null);
  7329. // convert to type DataSet when needed
  7330. var newDataSet;
  7331. if (!items) {
  7332. newDataSet = null;
  7333. }
  7334. else if (items instanceof DataSet) {
  7335. newDataSet = items;
  7336. }
  7337. if (!(items instanceof DataSet)) {
  7338. newDataSet = new DataSet({
  7339. convert: {
  7340. start: 'Date',
  7341. end: 'Date'
  7342. }
  7343. });
  7344. newDataSet.add(items);
  7345. }
  7346. // set items
  7347. this.itemsData = newDataSet;
  7348. this.content.setItems(newDataSet);
  7349. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  7350. // apply the data range as range
  7351. var dataRange = this.getItemRange();
  7352. // add 5% space on both sides
  7353. var start = dataRange.min;
  7354. var end = dataRange.max;
  7355. if (start != null && end != null) {
  7356. var interval = (end.valueOf() - start.valueOf());
  7357. if (interval <= 0) {
  7358. // prevent an empty interval
  7359. interval = 24 * 60 * 60 * 1000; // 1 day
  7360. }
  7361. start = new Date(start.valueOf() - interval * 0.05);
  7362. end = new Date(end.valueOf() + interval * 0.05);
  7363. }
  7364. // override specified start and/or end date
  7365. if (this.options.start != undefined) {
  7366. start = util.convert(this.options.start, 'Date');
  7367. }
  7368. if (this.options.end != undefined) {
  7369. end = util.convert(this.options.end, 'Date');
  7370. }
  7371. // apply range if there is a min or max available
  7372. if (start != null || end != null) {
  7373. this.range.setRange(start, end);
  7374. }
  7375. }
  7376. };
  7377. /**
  7378. * Set groups
  7379. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  7380. */
  7381. Timeline.prototype.setGroups = function(groups) {
  7382. var me = this;
  7383. this.groupsData = groups;
  7384. // switch content type between ItemSet or GroupSet when needed
  7385. var Type = this.groupsData ? GroupSet : ItemSet;
  7386. if (!(this.content instanceof Type)) {
  7387. // remove old content set
  7388. if (this.content) {
  7389. this.content.hide();
  7390. if (this.content.setItems) {
  7391. this.content.setItems(); // disconnect from items
  7392. }
  7393. if (this.content.setGroups) {
  7394. this.content.setGroups(); // disconnect from groups
  7395. }
  7396. this.controller.remove(this.content);
  7397. }
  7398. // create new content set
  7399. var options = Object.create(this.options);
  7400. util.extend(options, {
  7401. top: function () {
  7402. if (me.options.orientation == 'top') {
  7403. return me.timeaxis.height;
  7404. }
  7405. else {
  7406. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  7407. }
  7408. },
  7409. left: null,
  7410. width: '100%',
  7411. height: function () {
  7412. if (me.options.height) {
  7413. // fixed height
  7414. return me.itemPanel.height - me.timeaxis.height;
  7415. }
  7416. else {
  7417. // auto height
  7418. return null;
  7419. }
  7420. },
  7421. maxHeight: function () {
  7422. // TODO: change maxHeight to be a css string like '100%' or '300px'
  7423. if (me.options.maxHeight) {
  7424. if (!util.isNumber(me.options.maxHeight)) {
  7425. throw new TypeError('Number expected for property maxHeight');
  7426. }
  7427. return me.options.maxHeight - me.timeaxis.height;
  7428. }
  7429. else {
  7430. return null;
  7431. }
  7432. },
  7433. labelContainer: function () {
  7434. return me.labelPanel.getContainer();
  7435. }
  7436. });
  7437. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  7438. if (this.content.setRange) {
  7439. this.content.setRange(this.range);
  7440. }
  7441. if (this.content.setItems) {
  7442. this.content.setItems(this.itemsData);
  7443. }
  7444. if (this.content.setGroups) {
  7445. this.content.setGroups(this.groupsData);
  7446. }
  7447. this.controller.add(this.content);
  7448. }
  7449. };
  7450. /**
  7451. * Get the data range of the item set.
  7452. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  7453. * When no minimum is found, min==null
  7454. * When no maximum is found, max==null
  7455. */
  7456. Timeline.prototype.getItemRange = function getItemRange() {
  7457. // calculate min from start filed
  7458. var itemsData = this.itemsData,
  7459. min = null,
  7460. max = null;
  7461. if (itemsData) {
  7462. // calculate the minimum value of the field 'start'
  7463. var minItem = itemsData.min('start');
  7464. min = minItem ? minItem.start.valueOf() : null;
  7465. // calculate maximum value of fields 'start' and 'end'
  7466. var maxStartItem = itemsData.max('start');
  7467. if (maxStartItem) {
  7468. max = maxStartItem.start.valueOf();
  7469. }
  7470. var maxEndItem = itemsData.max('end');
  7471. if (maxEndItem) {
  7472. if (max == null) {
  7473. max = maxEndItem.end.valueOf();
  7474. }
  7475. else {
  7476. max = Math.max(max, maxEndItem.end.valueOf());
  7477. }
  7478. }
  7479. }
  7480. return {
  7481. min: (min != null) ? new Date(min) : null,
  7482. max: (max != null) ? new Date(max) : null
  7483. };
  7484. };
  7485. /**
  7486. * Set selected items by their id. Replaces the current selection
  7487. * Unknown id's are silently ignored.
  7488. * @param {Array} [ids] An array with zero or more id's of the items to be
  7489. * selected. If ids is an empty array, all items will be
  7490. * unselected.
  7491. */
  7492. Timeline.prototype.setSelection = function setSelection (ids) {
  7493. if (this.content) this.content.setSelection(ids);
  7494. };
  7495. /**
  7496. * Get the selected items by their id
  7497. * @return {Array} ids The ids of the selected items
  7498. */
  7499. Timeline.prototype.getSelection = function getSelection() {
  7500. return this.content ? this.content.getSelection() : [];
  7501. };
  7502. /**
  7503. * Set the visible window. Both parameters are optional, you can change only
  7504. * start or only end.
  7505. * @param {Date | Number | String} [start] Start date of visible window
  7506. * @param {Date | Number | String} [end] End date of visible window
  7507. */
  7508. Timeline.prototype.setWindow = function setWindow(start, end) {
  7509. this.range.setRange(start, end);
  7510. };
  7511. /**
  7512. * Get the visible window
  7513. * @return {{start: Date, end: Date}} Visible range
  7514. */
  7515. Timeline.prototype.getWindow = function setWindow() {
  7516. var range = this.range.getRange();
  7517. return {
  7518. start: new Date(range.start),
  7519. end: new Date(range.end)
  7520. };
  7521. };
  7522. /**
  7523. * Handle selecting/deselecting an item when tapping it
  7524. * @param {Event} event
  7525. * @private
  7526. */
  7527. // TODO: move this function to ItemSet
  7528. Timeline.prototype._onSelectItem = function (event) {
  7529. if (!this.options.selectable) return;
  7530. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  7531. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  7532. if (ctrlKey || shiftKey) {
  7533. this._onMultiSelectItem(event);
  7534. return;
  7535. }
  7536. var item = ItemSet.itemFromTarget(event);
  7537. var selection = item ? [item.id] : [];
  7538. this.setSelection(selection);
  7539. this.controller.emit('select', {
  7540. items: this.getSelection()
  7541. });
  7542. event.stopPropagation();
  7543. };
  7544. /**
  7545. * Handle creation and updates of an item on double tap
  7546. * @param event
  7547. * @private
  7548. */
  7549. Timeline.prototype._onAddItem = function (event) {
  7550. if (!this.options.selectable) return;
  7551. if (!this.options.editable) return;
  7552. var me = this,
  7553. item = ItemSet.itemFromTarget(event);
  7554. if (item) {
  7555. // update item
  7556. // execute async handler to update the item (or cancel it)
  7557. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  7558. this.options.onUpdate(itemData, function (itemData) {
  7559. if (itemData) {
  7560. me.itemsData.update(itemData);
  7561. }
  7562. });
  7563. }
  7564. else {
  7565. // add item
  7566. var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
  7567. var x = event.gesture.center.pageX - xAbs;
  7568. var newItem = {
  7569. start: this.timeaxis.snap(this._toTime(x)),
  7570. content: 'new item'
  7571. };
  7572. var id = util.randomUUID();
  7573. newItem[this.itemsData.fieldId] = id;
  7574. var group = GroupSet.groupFromTarget(event);
  7575. if (group) {
  7576. newItem.group = group.groupId;
  7577. }
  7578. // execute async handler to customize (or cancel) adding an item
  7579. this.options.onAdd(newItem, function (item) {
  7580. if (item) {
  7581. me.itemsData.add(newItem);
  7582. // select the created item after it is repainted
  7583. me.controller.once('repaint', function () {
  7584. me.setSelection([id]);
  7585. me.controller.emit('select', {
  7586. items: me.getSelection()
  7587. });
  7588. }.bind(me));
  7589. }
  7590. });
  7591. }
  7592. };
  7593. /**
  7594. * Handle selecting/deselecting multiple items when holding an item
  7595. * @param {Event} event
  7596. * @private
  7597. */
  7598. // TODO: move this function to ItemSet
  7599. Timeline.prototype._onMultiSelectItem = function (event) {
  7600. if (!this.options.selectable) return;
  7601. var selection,
  7602. item = ItemSet.itemFromTarget(event);
  7603. if (item) {
  7604. // multi select items
  7605. selection = this.getSelection(); // current selection
  7606. var index = selection.indexOf(item.id);
  7607. if (index == -1) {
  7608. // item is not yet selected -> select it
  7609. selection.push(item.id);
  7610. }
  7611. else {
  7612. // item is already selected -> deselect it
  7613. selection.splice(index, 1);
  7614. }
  7615. this.setSelection(selection);
  7616. this.controller.emit('select', {
  7617. items: this.getSelection()
  7618. });
  7619. event.stopPropagation();
  7620. }
  7621. };
  7622. /**
  7623. * Convert a position on screen (pixels) to a datetime
  7624. * @param {int} x Position on the screen in pixels
  7625. * @return {Date} time The datetime the corresponds with given position x
  7626. * @private
  7627. */
  7628. Timeline.prototype._toTime = function _toTime(x) {
  7629. var conversion = this.range.conversion(this.content.width);
  7630. return new Date(x / conversion.scale + conversion.offset);
  7631. };
  7632. /**
  7633. * Convert a datetime (Date object) into a position on the screen
  7634. * @param {Date} time A date
  7635. * @return {int} x The position on the screen in pixels which corresponds
  7636. * with the given date.
  7637. * @private
  7638. */
  7639. Timeline.prototype._toScreen = function _toScreen(time) {
  7640. var conversion = this.range.conversion(this.content.width);
  7641. return (time.valueOf() - conversion.offset) * conversion.scale;
  7642. };
  7643. (function(exports) {
  7644. /**
  7645. * Parse a text source containing data in DOT language into a JSON object.
  7646. * The object contains two lists: one with nodes and one with edges.
  7647. *
  7648. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  7649. *
  7650. * @param {String} data Text containing a graph in DOT-notation
  7651. * @return {Object} graph An object containing two parameters:
  7652. * {Object[]} nodes
  7653. * {Object[]} edges
  7654. */
  7655. function parseDOT (data) {
  7656. dot = data;
  7657. return parseGraph();
  7658. }
  7659. // token types enumeration
  7660. var TOKENTYPE = {
  7661. NULL : 0,
  7662. DELIMITER : 1,
  7663. IDENTIFIER: 2,
  7664. UNKNOWN : 3
  7665. };
  7666. // map with all delimiters
  7667. var DELIMITERS = {
  7668. '{': true,
  7669. '}': true,
  7670. '[': true,
  7671. ']': true,
  7672. ';': true,
  7673. '=': true,
  7674. ',': true,
  7675. '->': true,
  7676. '--': true
  7677. };
  7678. var dot = ''; // current dot file
  7679. var index = 0; // current index in dot file
  7680. var c = ''; // current token character in expr
  7681. var token = ''; // current token
  7682. var tokenType = TOKENTYPE.NULL; // type of the token
  7683. /**
  7684. * Get the first character from the dot file.
  7685. * The character is stored into the char c. If the end of the dot file is
  7686. * reached, the function puts an empty string in c.
  7687. */
  7688. function first() {
  7689. index = 0;
  7690. c = dot.charAt(0);
  7691. }
  7692. /**
  7693. * Get the next character from the dot file.
  7694. * The character is stored into the char c. If the end of the dot file is
  7695. * reached, the function puts an empty string in c.
  7696. */
  7697. function next() {
  7698. index++;
  7699. c = dot.charAt(index);
  7700. }
  7701. /**
  7702. * Preview the next character from the dot file.
  7703. * @return {String} cNext
  7704. */
  7705. function nextPreview() {
  7706. return dot.charAt(index + 1);
  7707. }
  7708. /**
  7709. * Test whether given character is alphabetic or numeric
  7710. * @param {String} c
  7711. * @return {Boolean} isAlphaNumeric
  7712. */
  7713. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  7714. function isAlphaNumeric(c) {
  7715. return regexAlphaNumeric.test(c);
  7716. }
  7717. /**
  7718. * Merge all properties of object b into object b
  7719. * @param {Object} a
  7720. * @param {Object} b
  7721. * @return {Object} a
  7722. */
  7723. function merge (a, b) {
  7724. if (!a) {
  7725. a = {};
  7726. }
  7727. if (b) {
  7728. for (var name in b) {
  7729. if (b.hasOwnProperty(name)) {
  7730. a[name] = b[name];
  7731. }
  7732. }
  7733. }
  7734. return a;
  7735. }
  7736. /**
  7737. * Set a value in an object, where the provided parameter name can be a
  7738. * path with nested parameters. For example:
  7739. *
  7740. * var obj = {a: 2};
  7741. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  7742. *
  7743. * @param {Object} obj
  7744. * @param {String} path A parameter name or dot-separated parameter path,
  7745. * like "color.highlight.border".
  7746. * @param {*} value
  7747. */
  7748. function setValue(obj, path, value) {
  7749. var keys = path.split('.');
  7750. var o = obj;
  7751. while (keys.length) {
  7752. var key = keys.shift();
  7753. if (keys.length) {
  7754. // this isn't the end point
  7755. if (!o[key]) {
  7756. o[key] = {};
  7757. }
  7758. o = o[key];
  7759. }
  7760. else {
  7761. // this is the end point
  7762. o[key] = value;
  7763. }
  7764. }
  7765. }
  7766. /**
  7767. * Add a node to a graph object. If there is already a node with
  7768. * the same id, their attributes will be merged.
  7769. * @param {Object} graph
  7770. * @param {Object} node
  7771. */
  7772. function addNode(graph, node) {
  7773. var i, len;
  7774. var current = null;
  7775. // find root graph (in case of subgraph)
  7776. var graphs = [graph]; // list with all graphs from current graph to root graph
  7777. var root = graph;
  7778. while (root.parent) {
  7779. graphs.push(root.parent);
  7780. root = root.parent;
  7781. }
  7782. // find existing node (at root level) by its id
  7783. if (root.nodes) {
  7784. for (i = 0, len = root.nodes.length; i < len; i++) {
  7785. if (node.id === root.nodes[i].id) {
  7786. current = root.nodes[i];
  7787. break;
  7788. }
  7789. }
  7790. }
  7791. if (!current) {
  7792. // this is a new node
  7793. current = {
  7794. id: node.id
  7795. };
  7796. if (graph.node) {
  7797. // clone default attributes
  7798. current.attr = merge(current.attr, graph.node);
  7799. }
  7800. }
  7801. // add node to this (sub)graph and all its parent graphs
  7802. for (i = graphs.length - 1; i >= 0; i--) {
  7803. var g = graphs[i];
  7804. if (!g.nodes) {
  7805. g.nodes = [];
  7806. }
  7807. if (g.nodes.indexOf(current) == -1) {
  7808. g.nodes.push(current);
  7809. }
  7810. }
  7811. // merge attributes
  7812. if (node.attr) {
  7813. current.attr = merge(current.attr, node.attr);
  7814. }
  7815. }
  7816. /**
  7817. * Add an edge to a graph object
  7818. * @param {Object} graph
  7819. * @param {Object} edge
  7820. */
  7821. function addEdge(graph, edge) {
  7822. if (!graph.edges) {
  7823. graph.edges = [];
  7824. }
  7825. graph.edges.push(edge);
  7826. if (graph.edge) {
  7827. var attr = merge({}, graph.edge); // clone default attributes
  7828. edge.attr = merge(attr, edge.attr); // merge attributes
  7829. }
  7830. }
  7831. /**
  7832. * Create an edge to a graph object
  7833. * @param {Object} graph
  7834. * @param {String | Number | Object} from
  7835. * @param {String | Number | Object} to
  7836. * @param {String} type
  7837. * @param {Object | null} attr
  7838. * @return {Object} edge
  7839. */
  7840. function createEdge(graph, from, to, type, attr) {
  7841. var edge = {
  7842. from: from,
  7843. to: to,
  7844. type: type
  7845. };
  7846. if (graph.edge) {
  7847. edge.attr = merge({}, graph.edge); // clone default attributes
  7848. }
  7849. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7850. return edge;
  7851. }
  7852. /**
  7853. * Get next token in the current dot file.
  7854. * The token and token type are available as token and tokenType
  7855. */
  7856. function getToken() {
  7857. tokenType = TOKENTYPE.NULL;
  7858. token = '';
  7859. // skip over whitespaces
  7860. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7861. next();
  7862. }
  7863. do {
  7864. var isComment = false;
  7865. // skip comment
  7866. if (c == '#') {
  7867. // find the previous non-space character
  7868. var i = index - 1;
  7869. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7870. i--;
  7871. }
  7872. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7873. // the # is at the start of a line, this is indeed a line comment
  7874. while (c != '' && c != '\n') {
  7875. next();
  7876. }
  7877. isComment = true;
  7878. }
  7879. }
  7880. if (c == '/' && nextPreview() == '/') {
  7881. // skip line comment
  7882. while (c != '' && c != '\n') {
  7883. next();
  7884. }
  7885. isComment = true;
  7886. }
  7887. if (c == '/' && nextPreview() == '*') {
  7888. // skip block comment
  7889. while (c != '') {
  7890. if (c == '*' && nextPreview() == '/') {
  7891. // end of block comment found. skip these last two characters
  7892. next();
  7893. next();
  7894. break;
  7895. }
  7896. else {
  7897. next();
  7898. }
  7899. }
  7900. isComment = true;
  7901. }
  7902. // skip over whitespaces
  7903. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7904. next();
  7905. }
  7906. }
  7907. while (isComment);
  7908. // check for end of dot file
  7909. if (c == '') {
  7910. // token is still empty
  7911. tokenType = TOKENTYPE.DELIMITER;
  7912. return;
  7913. }
  7914. // check for delimiters consisting of 2 characters
  7915. var c2 = c + nextPreview();
  7916. if (DELIMITERS[c2]) {
  7917. tokenType = TOKENTYPE.DELIMITER;
  7918. token = c2;
  7919. next();
  7920. next();
  7921. return;
  7922. }
  7923. // check for delimiters consisting of 1 character
  7924. if (DELIMITERS[c]) {
  7925. tokenType = TOKENTYPE.DELIMITER;
  7926. token = c;
  7927. next();
  7928. return;
  7929. }
  7930. // check for an identifier (number or string)
  7931. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7932. if (isAlphaNumeric(c) || c == '-') {
  7933. token += c;
  7934. next();
  7935. while (isAlphaNumeric(c)) {
  7936. token += c;
  7937. next();
  7938. }
  7939. if (token == 'false') {
  7940. token = false; // convert to boolean
  7941. }
  7942. else if (token == 'true') {
  7943. token = true; // convert to boolean
  7944. }
  7945. else if (!isNaN(Number(token))) {
  7946. token = Number(token); // convert to number
  7947. }
  7948. tokenType = TOKENTYPE.IDENTIFIER;
  7949. return;
  7950. }
  7951. // check for a string enclosed by double quotes
  7952. if (c == '"') {
  7953. next();
  7954. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7955. token += c;
  7956. if (c == '"') { // skip the escape character
  7957. next();
  7958. }
  7959. next();
  7960. }
  7961. if (c != '"') {
  7962. throw newSyntaxError('End of string " expected');
  7963. }
  7964. next();
  7965. tokenType = TOKENTYPE.IDENTIFIER;
  7966. return;
  7967. }
  7968. // something unknown is found, wrong characters, a syntax error
  7969. tokenType = TOKENTYPE.UNKNOWN;
  7970. while (c != '') {
  7971. token += c;
  7972. next();
  7973. }
  7974. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7975. }
  7976. /**
  7977. * Parse a graph.
  7978. * @returns {Object} graph
  7979. */
  7980. function parseGraph() {
  7981. var graph = {};
  7982. first();
  7983. getToken();
  7984. // optional strict keyword
  7985. if (token == 'strict') {
  7986. graph.strict = true;
  7987. getToken();
  7988. }
  7989. // graph or digraph keyword
  7990. if (token == 'graph' || token == 'digraph') {
  7991. graph.type = token;
  7992. getToken();
  7993. }
  7994. // optional graph id
  7995. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7996. graph.id = token;
  7997. getToken();
  7998. }
  7999. // open angle bracket
  8000. if (token != '{') {
  8001. throw newSyntaxError('Angle bracket { expected');
  8002. }
  8003. getToken();
  8004. // statements
  8005. parseStatements(graph);
  8006. // close angle bracket
  8007. if (token != '}') {
  8008. throw newSyntaxError('Angle bracket } expected');
  8009. }
  8010. getToken();
  8011. // end of file
  8012. if (token !== '') {
  8013. throw newSyntaxError('End of file expected');
  8014. }
  8015. getToken();
  8016. // remove temporary default properties
  8017. delete graph.node;
  8018. delete graph.edge;
  8019. delete graph.graph;
  8020. return graph;
  8021. }
  8022. /**
  8023. * Parse a list with statements.
  8024. * @param {Object} graph
  8025. */
  8026. function parseStatements (graph) {
  8027. while (token !== '' && token != '}') {
  8028. parseStatement(graph);
  8029. if (token == ';') {
  8030. getToken();
  8031. }
  8032. }
  8033. }
  8034. /**
  8035. * Parse a single statement. Can be a an attribute statement, node
  8036. * statement, a series of node statements and edge statements, or a
  8037. * parameter.
  8038. * @param {Object} graph
  8039. */
  8040. function parseStatement(graph) {
  8041. // parse subgraph
  8042. var subgraph = parseSubgraph(graph);
  8043. if (subgraph) {
  8044. // edge statements
  8045. parseEdge(graph, subgraph);
  8046. return;
  8047. }
  8048. // parse an attribute statement
  8049. var attr = parseAttributeStatement(graph);
  8050. if (attr) {
  8051. return;
  8052. }
  8053. // parse node
  8054. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8055. throw newSyntaxError('Identifier expected');
  8056. }
  8057. var id = token; // id can be a string or a number
  8058. getToken();
  8059. if (token == '=') {
  8060. // id statement
  8061. getToken();
  8062. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8063. throw newSyntaxError('Identifier expected');
  8064. }
  8065. graph[id] = token;
  8066. getToken();
  8067. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  8068. }
  8069. else {
  8070. parseNodeStatement(graph, id);
  8071. }
  8072. }
  8073. /**
  8074. * Parse a subgraph
  8075. * @param {Object} graph parent graph object
  8076. * @return {Object | null} subgraph
  8077. */
  8078. function parseSubgraph (graph) {
  8079. var subgraph = null;
  8080. // optional subgraph keyword
  8081. if (token == 'subgraph') {
  8082. subgraph = {};
  8083. subgraph.type = 'subgraph';
  8084. getToken();
  8085. // optional graph id
  8086. if (tokenType == TOKENTYPE.IDENTIFIER) {
  8087. subgraph.id = token;
  8088. getToken();
  8089. }
  8090. }
  8091. // open angle bracket
  8092. if (token == '{') {
  8093. getToken();
  8094. if (!subgraph) {
  8095. subgraph = {};
  8096. }
  8097. subgraph.parent = graph;
  8098. subgraph.node = graph.node;
  8099. subgraph.edge = graph.edge;
  8100. subgraph.graph = graph.graph;
  8101. // statements
  8102. parseStatements(subgraph);
  8103. // close angle bracket
  8104. if (token != '}') {
  8105. throw newSyntaxError('Angle bracket } expected');
  8106. }
  8107. getToken();
  8108. // remove temporary default properties
  8109. delete subgraph.node;
  8110. delete subgraph.edge;
  8111. delete subgraph.graph;
  8112. delete subgraph.parent;
  8113. // register at the parent graph
  8114. if (!graph.subgraphs) {
  8115. graph.subgraphs = [];
  8116. }
  8117. graph.subgraphs.push(subgraph);
  8118. }
  8119. return subgraph;
  8120. }
  8121. /**
  8122. * parse an attribute statement like "node [shape=circle fontSize=16]".
  8123. * Available keywords are 'node', 'edge', 'graph'.
  8124. * The previous list with default attributes will be replaced
  8125. * @param {Object} graph
  8126. * @returns {String | null} keyword Returns the name of the parsed attribute
  8127. * (node, edge, graph), or null if nothing
  8128. * is parsed.
  8129. */
  8130. function parseAttributeStatement (graph) {
  8131. // attribute statements
  8132. if (token == 'node') {
  8133. getToken();
  8134. // node attributes
  8135. graph.node = parseAttributeList();
  8136. return 'node';
  8137. }
  8138. else if (token == 'edge') {
  8139. getToken();
  8140. // edge attributes
  8141. graph.edge = parseAttributeList();
  8142. return 'edge';
  8143. }
  8144. else if (token == 'graph') {
  8145. getToken();
  8146. // graph attributes
  8147. graph.graph = parseAttributeList();
  8148. return 'graph';
  8149. }
  8150. return null;
  8151. }
  8152. /**
  8153. * parse a node statement
  8154. * @param {Object} graph
  8155. * @param {String | Number} id
  8156. */
  8157. function parseNodeStatement(graph, id) {
  8158. // node statement
  8159. var node = {
  8160. id: id
  8161. };
  8162. var attr = parseAttributeList();
  8163. if (attr) {
  8164. node.attr = attr;
  8165. }
  8166. addNode(graph, node);
  8167. // edge statements
  8168. parseEdge(graph, id);
  8169. }
  8170. /**
  8171. * Parse an edge or a series of edges
  8172. * @param {Object} graph
  8173. * @param {String | Number} from Id of the from node
  8174. */
  8175. function parseEdge(graph, from) {
  8176. while (token == '->' || token == '--') {
  8177. var to;
  8178. var type = token;
  8179. getToken();
  8180. var subgraph = parseSubgraph(graph);
  8181. if (subgraph) {
  8182. to = subgraph;
  8183. }
  8184. else {
  8185. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8186. throw newSyntaxError('Identifier or subgraph expected');
  8187. }
  8188. to = token;
  8189. addNode(graph, {
  8190. id: to
  8191. });
  8192. getToken();
  8193. }
  8194. // parse edge attributes
  8195. var attr = parseAttributeList();
  8196. // create edge
  8197. var edge = createEdge(graph, from, to, type, attr);
  8198. addEdge(graph, edge);
  8199. from = to;
  8200. }
  8201. }
  8202. /**
  8203. * Parse a set with attributes,
  8204. * for example [label="1.000", shape=solid]
  8205. * @return {Object | null} attr
  8206. */
  8207. function parseAttributeList() {
  8208. var attr = null;
  8209. while (token == '[') {
  8210. getToken();
  8211. attr = {};
  8212. while (token !== '' && token != ']') {
  8213. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8214. throw newSyntaxError('Attribute name expected');
  8215. }
  8216. var name = token;
  8217. getToken();
  8218. if (token != '=') {
  8219. throw newSyntaxError('Equal sign = expected');
  8220. }
  8221. getToken();
  8222. if (tokenType != TOKENTYPE.IDENTIFIER) {
  8223. throw newSyntaxError('Attribute value expected');
  8224. }
  8225. var value = token;
  8226. setValue(attr, name, value); // name can be a path
  8227. getToken();
  8228. if (token ==',') {
  8229. getToken();
  8230. }
  8231. }
  8232. if (token != ']') {
  8233. throw newSyntaxError('Bracket ] expected');
  8234. }
  8235. getToken();
  8236. }
  8237. return attr;
  8238. }
  8239. /**
  8240. * Create a syntax error with extra information on current token and index.
  8241. * @param {String} message
  8242. * @returns {SyntaxError} err
  8243. */
  8244. function newSyntaxError(message) {
  8245. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  8246. }
  8247. /**
  8248. * Chop off text after a maximum length
  8249. * @param {String} text
  8250. * @param {Number} maxLength
  8251. * @returns {String}
  8252. */
  8253. function chop (text, maxLength) {
  8254. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  8255. }
  8256. /**
  8257. * Execute a function fn for each pair of elements in two arrays
  8258. * @param {Array | *} array1
  8259. * @param {Array | *} array2
  8260. * @param {function} fn
  8261. */
  8262. function forEach2(array1, array2, fn) {
  8263. if (array1 instanceof Array) {
  8264. array1.forEach(function (elem1) {
  8265. if (array2 instanceof Array) {
  8266. array2.forEach(function (elem2) {
  8267. fn(elem1, elem2);
  8268. });
  8269. }
  8270. else {
  8271. fn(elem1, array2);
  8272. }
  8273. });
  8274. }
  8275. else {
  8276. if (array2 instanceof Array) {
  8277. array2.forEach(function (elem2) {
  8278. fn(array1, elem2);
  8279. });
  8280. }
  8281. else {
  8282. fn(array1, array2);
  8283. }
  8284. }
  8285. }
  8286. /**
  8287. * Convert a string containing a graph in DOT language into a map containing
  8288. * with nodes and edges in the format of graph.
  8289. * @param {String} data Text containing a graph in DOT-notation
  8290. * @return {Object} graphData
  8291. */
  8292. function DOTToGraph (data) {
  8293. // parse the DOT file
  8294. var dotData = parseDOT(data);
  8295. var graphData = {
  8296. nodes: [],
  8297. edges: [],
  8298. options: {}
  8299. };
  8300. // copy the nodes
  8301. if (dotData.nodes) {
  8302. dotData.nodes.forEach(function (dotNode) {
  8303. var graphNode = {
  8304. id: dotNode.id,
  8305. label: String(dotNode.label || dotNode.id)
  8306. };
  8307. merge(graphNode, dotNode.attr);
  8308. if (graphNode.image) {
  8309. graphNode.shape = 'image';
  8310. }
  8311. graphData.nodes.push(graphNode);
  8312. });
  8313. }
  8314. // copy the edges
  8315. if (dotData.edges) {
  8316. /**
  8317. * Convert an edge in DOT format to an edge with VisGraph format
  8318. * @param {Object} dotEdge
  8319. * @returns {Object} graphEdge
  8320. */
  8321. function convertEdge(dotEdge) {
  8322. var graphEdge = {
  8323. from: dotEdge.from,
  8324. to: dotEdge.to
  8325. };
  8326. merge(graphEdge, dotEdge.attr);
  8327. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  8328. return graphEdge;
  8329. }
  8330. dotData.edges.forEach(function (dotEdge) {
  8331. var from, to;
  8332. if (dotEdge.from instanceof Object) {
  8333. from = dotEdge.from.nodes;
  8334. }
  8335. else {
  8336. from = {
  8337. id: dotEdge.from
  8338. }
  8339. }
  8340. if (dotEdge.to instanceof Object) {
  8341. to = dotEdge.to.nodes;
  8342. }
  8343. else {
  8344. to = {
  8345. id: dotEdge.to
  8346. }
  8347. }
  8348. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  8349. dotEdge.from.edges.forEach(function (subEdge) {
  8350. var graphEdge = convertEdge(subEdge);
  8351. graphData.edges.push(graphEdge);
  8352. });
  8353. }
  8354. forEach2(from, to, function (from, to) {
  8355. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  8356. var graphEdge = convertEdge(subEdge);
  8357. graphData.edges.push(graphEdge);
  8358. });
  8359. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  8360. dotEdge.to.edges.forEach(function (subEdge) {
  8361. var graphEdge = convertEdge(subEdge);
  8362. graphData.edges.push(graphEdge);
  8363. });
  8364. }
  8365. });
  8366. }
  8367. // copy the options
  8368. if (dotData.attr) {
  8369. graphData.options = dotData.attr;
  8370. }
  8371. return graphData;
  8372. }
  8373. // exports
  8374. exports.parseDOT = parseDOT;
  8375. exports.DOTToGraph = DOTToGraph;
  8376. })(typeof util !== 'undefined' ? util : exports);
  8377. /**
  8378. * Canvas shapes used by the Graph
  8379. */
  8380. if (typeof CanvasRenderingContext2D !== 'undefined') {
  8381. /**
  8382. * Draw a circle shape
  8383. */
  8384. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  8385. this.beginPath();
  8386. this.arc(x, y, r, 0, 2*Math.PI, false);
  8387. };
  8388. /**
  8389. * Draw a square shape
  8390. * @param {Number} x horizontal center
  8391. * @param {Number} y vertical center
  8392. * @param {Number} r size, width and height of the square
  8393. */
  8394. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  8395. this.beginPath();
  8396. this.rect(x - r, y - r, r * 2, r * 2);
  8397. };
  8398. /**
  8399. * Draw a triangle shape
  8400. * @param {Number} x horizontal center
  8401. * @param {Number} y vertical center
  8402. * @param {Number} r radius, half the length of the sides of the triangle
  8403. */
  8404. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  8405. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8406. this.beginPath();
  8407. var s = r * 2;
  8408. var s2 = s / 2;
  8409. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8410. var h = Math.sqrt(s * s - s2 * s2); // height
  8411. this.moveTo(x, y - (h - ir));
  8412. this.lineTo(x + s2, y + ir);
  8413. this.lineTo(x - s2, y + ir);
  8414. this.lineTo(x, y - (h - ir));
  8415. this.closePath();
  8416. };
  8417. /**
  8418. * Draw a triangle shape in downward orientation
  8419. * @param {Number} x horizontal center
  8420. * @param {Number} y vertical center
  8421. * @param {Number} r radius
  8422. */
  8423. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  8424. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8425. this.beginPath();
  8426. var s = r * 2;
  8427. var s2 = s / 2;
  8428. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8429. var h = Math.sqrt(s * s - s2 * s2); // height
  8430. this.moveTo(x, y + (h - ir));
  8431. this.lineTo(x + s2, y - ir);
  8432. this.lineTo(x - s2, y - ir);
  8433. this.lineTo(x, y + (h - ir));
  8434. this.closePath();
  8435. };
  8436. /**
  8437. * Draw a star shape, a star with 5 points
  8438. * @param {Number} x horizontal center
  8439. * @param {Number} y vertical center
  8440. * @param {Number} r radius, half the length of the sides of the triangle
  8441. */
  8442. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  8443. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  8444. this.beginPath();
  8445. for (var n = 0; n < 10; n++) {
  8446. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  8447. this.lineTo(
  8448. x + radius * Math.sin(n * 2 * Math.PI / 10),
  8449. y - radius * Math.cos(n * 2 * Math.PI / 10)
  8450. );
  8451. }
  8452. this.closePath();
  8453. };
  8454. /**
  8455. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  8456. */
  8457. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  8458. var r2d = Math.PI/180;
  8459. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  8460. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  8461. this.beginPath();
  8462. this.moveTo(x+r,y);
  8463. this.lineTo(x+w-r,y);
  8464. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  8465. this.lineTo(x+w,y+h-r);
  8466. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  8467. this.lineTo(x+r,y+h);
  8468. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  8469. this.lineTo(x,y+r);
  8470. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  8471. };
  8472. /**
  8473. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8474. */
  8475. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  8476. var kappa = .5522848,
  8477. ox = (w / 2) * kappa, // control point offset horizontal
  8478. oy = (h / 2) * kappa, // control point offset vertical
  8479. xe = x + w, // x-end
  8480. ye = y + h, // y-end
  8481. xm = x + w / 2, // x-middle
  8482. ym = y + h / 2; // y-middle
  8483. this.beginPath();
  8484. this.moveTo(x, ym);
  8485. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8486. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8487. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8488. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8489. };
  8490. /**
  8491. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8492. */
  8493. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  8494. var f = 1/3;
  8495. var wEllipse = w;
  8496. var hEllipse = h * f;
  8497. var kappa = .5522848,
  8498. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  8499. oy = (hEllipse / 2) * kappa, // control point offset vertical
  8500. xe = x + wEllipse, // x-end
  8501. ye = y + hEllipse, // y-end
  8502. xm = x + wEllipse / 2, // x-middle
  8503. ym = y + hEllipse / 2, // y-middle
  8504. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  8505. yeb = y + h; // y-end, bottom ellipse
  8506. this.beginPath();
  8507. this.moveTo(xe, ym);
  8508. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8509. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8510. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8511. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8512. this.lineTo(xe, ymb);
  8513. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  8514. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  8515. this.lineTo(x, ym);
  8516. };
  8517. /**
  8518. * Draw an arrow point (no line)
  8519. */
  8520. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  8521. // tail
  8522. var xt = x - length * Math.cos(angle);
  8523. var yt = y - length * Math.sin(angle);
  8524. // inner tail
  8525. // TODO: allow to customize different shapes
  8526. var xi = x - length * 0.9 * Math.cos(angle);
  8527. var yi = y - length * 0.9 * Math.sin(angle);
  8528. // left
  8529. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  8530. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  8531. // right
  8532. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  8533. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  8534. this.beginPath();
  8535. this.moveTo(x, y);
  8536. this.lineTo(xl, yl);
  8537. this.lineTo(xi, yi);
  8538. this.lineTo(xr, yr);
  8539. this.closePath();
  8540. };
  8541. /**
  8542. * Sets up the dashedLine functionality for drawing
  8543. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  8544. * @author David Jordan
  8545. * @date 2012-08-08
  8546. */
  8547. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  8548. if (!dashArray) dashArray=[10,5];
  8549. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  8550. var dashCount = dashArray.length;
  8551. this.moveTo(x, y);
  8552. var dx = (x2-x), dy = (y2-y);
  8553. var slope = dy/dx;
  8554. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  8555. var dashIndex=0, draw=true;
  8556. while (distRemaining>=0.1){
  8557. var dashLength = dashArray[dashIndex++%dashCount];
  8558. if (dashLength > distRemaining) dashLength = distRemaining;
  8559. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  8560. if (dx<0) xStep = -xStep;
  8561. x += xStep;
  8562. y += slope*xStep;
  8563. this[draw ? 'lineTo' : 'moveTo'](x,y);
  8564. distRemaining -= dashLength;
  8565. draw = !draw;
  8566. }
  8567. };
  8568. // TODO: add diamond shape
  8569. }
  8570. /**
  8571. * @class Node
  8572. * A node. A node can be connected to other nodes via one or multiple edges.
  8573. * @param {object} properties An object containing properties for the node. All
  8574. * properties are optional, except for the id.
  8575. * {number} id Id of the node. Required
  8576. * {string} label Text label for the node
  8577. * {number} x Horizontal position of the node
  8578. * {number} y Vertical position of the node
  8579. * {string} shape Node shape, available:
  8580. * "database", "circle", "ellipse",
  8581. * "box", "image", "text", "dot",
  8582. * "star", "triangle", "triangleDown",
  8583. * "square"
  8584. * {string} image An image url
  8585. * {string} title An title text, can be HTML
  8586. * {anytype} group A group name or number
  8587. * @param {Graph.Images} imagelist A list with images. Only needed
  8588. * when the node has an image
  8589. * @param {Graph.Groups} grouplist A list with groups. Needed for
  8590. * retrieving group properties
  8591. * @param {Object} constants An object with default values for
  8592. * example for the color
  8593. *
  8594. */
  8595. function Node(properties, imagelist, grouplist, constants) {
  8596. this.selected = false;
  8597. this.edges = []; // all edges connected to this node
  8598. this.dynamicEdges = [];
  8599. this.reroutedEdges = {};
  8600. this.group = constants.nodes.group;
  8601. this.fontSize = constants.nodes.fontSize;
  8602. this.fontFace = constants.nodes.fontFace;
  8603. this.fontColor = constants.nodes.fontColor;
  8604. this.fontDrawThreshold = 3;
  8605. this.color = constants.nodes.color;
  8606. // set defaults for the properties
  8607. this.id = undefined;
  8608. this.shape = constants.nodes.shape;
  8609. this.image = constants.nodes.image;
  8610. this.x = 0;
  8611. this.y = 0;
  8612. this.xFixed = false;
  8613. this.yFixed = false;
  8614. this.horizontalAlignLeft = true; // these are for the navigation controls
  8615. this.verticalAlignTop = true; // these are for the navigation controls
  8616. this.radius = constants.nodes.radius;
  8617. this.baseRadiusValue = constants.nodes.radius;
  8618. this.radiusFixed = false;
  8619. this.radiusMin = constants.nodes.radiusMin;
  8620. this.radiusMax = constants.nodes.radiusMax;
  8621. this.level = -1;
  8622. this.imagelist = imagelist;
  8623. this.grouplist = grouplist;
  8624. // physics properties
  8625. this.fx = 0.0; // external force x
  8626. this.fy = 0.0; // external force y
  8627. this.vx = 0.0; // velocity x
  8628. this.vy = 0.0; // velocity y
  8629. this.minForce = constants.minForce;
  8630. this.damping = constants.physics.damping;
  8631. this.mass = 1; // kg
  8632. this.setProperties(properties, constants);
  8633. // creating the variables for clustering
  8634. this.resetCluster();
  8635. this.dynamicEdgesLength = 0;
  8636. this.clusterSession = 0;
  8637. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  8638. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  8639. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  8640. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  8641. this.growthIndicator = 0;
  8642. // variables to tell the node about the graph.
  8643. this.graphScaleInv = 1;
  8644. this.graphScale = 1;
  8645. this.canvasTopLeft = {"x": -300, "y": -300};
  8646. this.canvasBottomRight = {"x": 300, "y": 300};
  8647. this.parentEdgeId = null;
  8648. }
  8649. /**
  8650. * (re)setting the clustering variables and objects
  8651. */
  8652. Node.prototype.resetCluster = function() {
  8653. // clustering variables
  8654. this.formationScale = undefined; // this is used to determine when to open the cluster
  8655. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  8656. this.containedNodes = {};
  8657. this.containedEdges = {};
  8658. this.clusterSessions = [];
  8659. };
  8660. /**
  8661. * Attach a edge to the node
  8662. * @param {Edge} edge
  8663. */
  8664. Node.prototype.attachEdge = function(edge) {
  8665. if (this.edges.indexOf(edge) == -1) {
  8666. this.edges.push(edge);
  8667. }
  8668. if (this.dynamicEdges.indexOf(edge) == -1) {
  8669. this.dynamicEdges.push(edge);
  8670. }
  8671. this.dynamicEdgesLength = this.dynamicEdges.length;
  8672. };
  8673. /**
  8674. * Detach a edge from the node
  8675. * @param {Edge} edge
  8676. */
  8677. Node.prototype.detachEdge = function(edge) {
  8678. var index = this.edges.indexOf(edge);
  8679. if (index != -1) {
  8680. this.edges.splice(index, 1);
  8681. this.dynamicEdges.splice(index, 1);
  8682. }
  8683. this.dynamicEdgesLength = this.dynamicEdges.length;
  8684. };
  8685. /**
  8686. * Set or overwrite properties for the node
  8687. * @param {Object} properties an object with properties
  8688. * @param {Object} constants and object with default, global properties
  8689. */
  8690. Node.prototype.setProperties = function(properties, constants) {
  8691. if (!properties) {
  8692. return;
  8693. }
  8694. this.originalLabel = undefined;
  8695. // basic properties
  8696. if (properties.id !== undefined) {this.id = properties.id;}
  8697. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  8698. if (properties.title !== undefined) {this.title = properties.title;}
  8699. if (properties.group !== undefined) {this.group = properties.group;}
  8700. if (properties.x !== undefined) {this.x = properties.x;}
  8701. if (properties.y !== undefined) {this.y = properties.y;}
  8702. if (properties.value !== undefined) {this.value = properties.value;}
  8703. if (properties.level !== undefined) {this.level = properties.level;}
  8704. // physics
  8705. if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;}
  8706. if (properties.damping !== undefined) {this.dampingBase = properties.damping;}
  8707. if (properties.mass !== undefined) {this.mass = properties.mass;}
  8708. // navigation controls properties
  8709. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  8710. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  8711. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  8712. if (this.id === undefined) {
  8713. throw "Node must have an id";
  8714. }
  8715. // copy group properties
  8716. if (this.group) {
  8717. var groupObj = this.grouplist.get(this.group);
  8718. for (var prop in groupObj) {
  8719. if (groupObj.hasOwnProperty(prop)) {
  8720. this[prop] = groupObj[prop];
  8721. }
  8722. }
  8723. }
  8724. // individual shape properties
  8725. if (properties.shape !== undefined) {this.shape = properties.shape;}
  8726. if (properties.image !== undefined) {this.image = properties.image;}
  8727. if (properties.radius !== undefined) {this.radius = properties.radius;}
  8728. if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
  8729. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8730. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8731. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8732. if (this.image !== undefined) {
  8733. if (this.imagelist) {
  8734. this.imageObj = this.imagelist.load(this.image);
  8735. }
  8736. else {
  8737. throw "No imagelist provided";
  8738. }
  8739. }
  8740. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMove);
  8741. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMove);
  8742. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  8743. if (this.shape == 'image') {
  8744. this.radiusMin = constants.nodes.widthMin;
  8745. this.radiusMax = constants.nodes.widthMax;
  8746. }
  8747. // choose draw method depending on the shape
  8748. switch (this.shape) {
  8749. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8750. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8751. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8752. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8753. // TODO: add diamond shape
  8754. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8755. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8756. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8757. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8758. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8759. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8760. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8761. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8762. }
  8763. // reset the size of the node, this can be changed
  8764. this._reset();
  8765. };
  8766. /**
  8767. * Parse a color property into an object with border, background, and
  8768. * hightlight colors
  8769. * @param {Object | String} color
  8770. * @return {Object} colorObject
  8771. */
  8772. Node.parseColor = function(color) {
  8773. var c;
  8774. if (util.isString(color)) {
  8775. if (util.isValidHex(color)) {
  8776. var hsv = util.hexToHSV(color);
  8777. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  8778. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  8779. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  8780. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  8781. c = {
  8782. background: color,
  8783. border:darkerColorHex,
  8784. highlight: {
  8785. background:lighterColorHex,
  8786. border:darkerColorHex
  8787. }
  8788. };
  8789. }
  8790. else {
  8791. c = {
  8792. background:color,
  8793. border:color,
  8794. highlight: {
  8795. background:color,
  8796. border:color
  8797. }
  8798. };
  8799. }
  8800. }
  8801. else {
  8802. c = {};
  8803. c.background = color.background || 'white';
  8804. c.border = color.border || c.background;
  8805. if (util.isString(color.highlight)) {
  8806. c.highlight = {
  8807. border: color.highlight,
  8808. background: color.highlight
  8809. }
  8810. }
  8811. else {
  8812. c.highlight = {};
  8813. c.highlight.background = color.highlight && color.highlight.background || c.background;
  8814. c.highlight.border = color.highlight && color.highlight.border || c.border;
  8815. }
  8816. }
  8817. return c;
  8818. };
  8819. /**
  8820. * select this node
  8821. */
  8822. Node.prototype.select = function() {
  8823. this.selected = true;
  8824. this._reset();
  8825. };
  8826. /**
  8827. * unselect this node
  8828. */
  8829. Node.prototype.unselect = function() {
  8830. this.selected = false;
  8831. this._reset();
  8832. };
  8833. /**
  8834. * Reset the calculated size of the node, forces it to recalculate its size
  8835. */
  8836. Node.prototype.clearSizeCache = function() {
  8837. this._reset();
  8838. };
  8839. /**
  8840. * Reset the calculated size of the node, forces it to recalculate its size
  8841. * @private
  8842. */
  8843. Node.prototype._reset = function() {
  8844. this.width = undefined;
  8845. this.height = undefined;
  8846. };
  8847. /**
  8848. * get the title of this node.
  8849. * @return {string} title The title of the node, or undefined when no title
  8850. * has been set.
  8851. */
  8852. Node.prototype.getTitle = function() {
  8853. return this.title;
  8854. };
  8855. /**
  8856. * Calculate the distance to the border of the Node
  8857. * @param {CanvasRenderingContext2D} ctx
  8858. * @param {Number} angle Angle in radians
  8859. * @returns {number} distance Distance to the border in pixels
  8860. */
  8861. Node.prototype.distanceToBorder = function (ctx, angle) {
  8862. var borderWidth = 1;
  8863. if (!this.width) {
  8864. this.resize(ctx);
  8865. }
  8866. switch (this.shape) {
  8867. case 'circle':
  8868. case 'dot':
  8869. return this.radius + borderWidth;
  8870. case 'ellipse':
  8871. var a = this.width / 2;
  8872. var b = this.height / 2;
  8873. var w = (Math.sin(angle) * a);
  8874. var h = (Math.cos(angle) * b);
  8875. return a * b / Math.sqrt(w * w + h * h);
  8876. // TODO: implement distanceToBorder for database
  8877. // TODO: implement distanceToBorder for triangle
  8878. // TODO: implement distanceToBorder for triangleDown
  8879. case 'box':
  8880. case 'image':
  8881. case 'text':
  8882. default:
  8883. if (this.width) {
  8884. return Math.min(
  8885. Math.abs(this.width / 2 / Math.cos(angle)),
  8886. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8887. // TODO: reckon with border radius too in case of box
  8888. }
  8889. else {
  8890. return 0;
  8891. }
  8892. }
  8893. // TODO: implement calculation of distance to border for all shapes
  8894. };
  8895. /**
  8896. * Set forces acting on the node
  8897. * @param {number} fx Force in horizontal direction
  8898. * @param {number} fy Force in vertical direction
  8899. */
  8900. Node.prototype._setForce = function(fx, fy) {
  8901. this.fx = fx;
  8902. this.fy = fy;
  8903. };
  8904. /**
  8905. * Add forces acting on the node
  8906. * @param {number} fx Force in horizontal direction
  8907. * @param {number} fy Force in vertical direction
  8908. * @private
  8909. */
  8910. Node.prototype._addForce = function(fx, fy) {
  8911. this.fx += fx;
  8912. this.fy += fy;
  8913. };
  8914. /**
  8915. * Perform one discrete step for the node
  8916. * @param {number} interval Time interval in seconds
  8917. */
  8918. Node.prototype.discreteStep = function(interval) {
  8919. if (!this.xFixed) {
  8920. var dx = this.damping * this.vx; // damping force
  8921. var ax = (this.fx - dx) / this.mass; // acceleration
  8922. this.vx += ax * interval; // velocity
  8923. this.x += this.vx * interval; // position
  8924. }
  8925. if (!this.yFixed) {
  8926. var dy = this.damping * this.vy; // damping force
  8927. var ay = (this.fy - dy) / this.mass; // acceleration
  8928. this.vy += ay * interval; // velocity
  8929. this.y += this.vy * interval; // position
  8930. }
  8931. };
  8932. /**
  8933. * Perform one discrete step for the node
  8934. * @param {number} interval Time interval in seconds
  8935. */
  8936. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8937. if (!this.xFixed) {
  8938. var dx = this.damping * this.vx; // damping force
  8939. var ax = (this.fx - dx) / this.mass; // acceleration
  8940. this.vx += ax * interval; // velocity
  8941. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8942. this.x += this.vx * interval; // position
  8943. }
  8944. if (!this.yFixed) {
  8945. var dy = this.damping * this.vy; // damping force
  8946. var ay = (this.fy - dy) / this.mass; // acceleration
  8947. this.vy += ay * interval; // velocity
  8948. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8949. this.y += this.vy * interval; // position
  8950. }
  8951. };
  8952. /**
  8953. * Check if this node has a fixed x and y position
  8954. * @return {boolean} true if fixed, false if not
  8955. */
  8956. Node.prototype.isFixed = function() {
  8957. return (this.xFixed && this.yFixed);
  8958. };
  8959. /**
  8960. * Check if this node is moving
  8961. * @param {number} vmin the minimum velocity considered as "moving"
  8962. * @return {boolean} true if moving, false if it has no velocity
  8963. */
  8964. // TODO: replace this method with calculating the kinetic energy
  8965. Node.prototype.isMoving = function(vmin) {
  8966. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8967. };
  8968. /**
  8969. * check if this node is selecte
  8970. * @return {boolean} selected True if node is selected, else false
  8971. */
  8972. Node.prototype.isSelected = function() {
  8973. return this.selected;
  8974. };
  8975. /**
  8976. * Retrieve the value of the node. Can be undefined
  8977. * @return {Number} value
  8978. */
  8979. Node.prototype.getValue = function() {
  8980. return this.value;
  8981. };
  8982. /**
  8983. * Calculate the distance from the nodes location to the given location (x,y)
  8984. * @param {Number} x
  8985. * @param {Number} y
  8986. * @return {Number} value
  8987. */
  8988. Node.prototype.getDistance = function(x, y) {
  8989. var dx = this.x - x,
  8990. dy = this.y - y;
  8991. return Math.sqrt(dx * dx + dy * dy);
  8992. };
  8993. /**
  8994. * Adjust the value range of the node. The node will adjust it's radius
  8995. * based on its value.
  8996. * @param {Number} min
  8997. * @param {Number} max
  8998. */
  8999. Node.prototype.setValueRange = function(min, max) {
  9000. if (!this.radiusFixed && this.value !== undefined) {
  9001. if (max == min) {
  9002. this.radius = (this.radiusMin + this.radiusMax) / 2;
  9003. }
  9004. else {
  9005. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  9006. this.radius = (this.value - min) * scale + this.radiusMin;
  9007. }
  9008. }
  9009. this.baseRadiusValue = this.radius;
  9010. };
  9011. /**
  9012. * Draw this node in the given canvas
  9013. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9014. * @param {CanvasRenderingContext2D} ctx
  9015. */
  9016. Node.prototype.draw = function(ctx) {
  9017. throw "Draw method not initialized for node";
  9018. };
  9019. /**
  9020. * Recalculate the size of this node in the given canvas
  9021. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9022. * @param {CanvasRenderingContext2D} ctx
  9023. */
  9024. Node.prototype.resize = function(ctx) {
  9025. throw "Resize method not initialized for node";
  9026. };
  9027. /**
  9028. * Check if this object is overlapping with the provided object
  9029. * @param {Object} obj an object with parameters left, top, right, bottom
  9030. * @return {boolean} True if location is located on node
  9031. */
  9032. Node.prototype.isOverlappingWith = function(obj) {
  9033. return (this.left < obj.right &&
  9034. this.left + this.width > obj.left &&
  9035. this.top < obj.bottom &&
  9036. this.top + this.height > obj.top);
  9037. };
  9038. Node.prototype._resizeImage = function (ctx) {
  9039. // TODO: pre calculate the image size
  9040. if (!this.width || !this.height) { // undefined or 0
  9041. var width, height;
  9042. if (this.value) {
  9043. this.radius = this.baseRadiusValue;
  9044. var scale = this.imageObj.height / this.imageObj.width;
  9045. if (scale !== undefined) {
  9046. width = this.radius || this.imageObj.width;
  9047. height = this.radius * scale || this.imageObj.height;
  9048. }
  9049. else {
  9050. width = 0;
  9051. height = 0;
  9052. }
  9053. }
  9054. else {
  9055. width = this.imageObj.width;
  9056. height = this.imageObj.height;
  9057. }
  9058. this.width = width;
  9059. this.height = height;
  9060. this.growthIndicator = 0;
  9061. if (this.width > 0 && this.height > 0) {
  9062. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9063. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9064. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9065. this.growthIndicator = this.width - width;
  9066. }
  9067. }
  9068. };
  9069. Node.prototype._drawImage = function (ctx) {
  9070. this._resizeImage(ctx);
  9071. this.left = this.x - this.width / 2;
  9072. this.top = this.y - this.height / 2;
  9073. var yLabel;
  9074. if (this.imageObj.width != 0 ) {
  9075. // draw the shade
  9076. if (this.clusterSize > 1) {
  9077. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  9078. lineWidth *= this.graphScaleInv;
  9079. lineWidth = Math.min(0.2 * this.width,lineWidth);
  9080. ctx.globalAlpha = 0.5;
  9081. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  9082. }
  9083. // draw the image
  9084. ctx.globalAlpha = 1.0;
  9085. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  9086. yLabel = this.y + this.height / 2;
  9087. }
  9088. else {
  9089. // image still loading... just draw the label for now
  9090. yLabel = this.y;
  9091. }
  9092. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  9093. };
  9094. Node.prototype._resizeBox = function (ctx) {
  9095. if (!this.width) {
  9096. var margin = 5;
  9097. var textSize = this.getTextSize(ctx);
  9098. this.width = textSize.width + 2 * margin;
  9099. this.height = textSize.height + 2 * margin;
  9100. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  9101. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  9102. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  9103. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9104. }
  9105. };
  9106. Node.prototype._drawBox = function (ctx) {
  9107. this._resizeBox(ctx);
  9108. this.left = this.x - this.width / 2;
  9109. this.top = this.y - this.height / 2;
  9110. var clusterLineWidth = 2.5;
  9111. var selectionLineWidth = 2;
  9112. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9113. // draw the outer border
  9114. if (this.clusterSize > 1) {
  9115. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9116. ctx.lineWidth *= this.graphScaleInv;
  9117. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9118. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  9119. ctx.stroke();
  9120. }
  9121. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9122. ctx.lineWidth *= this.graphScaleInv;
  9123. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9124. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9125. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  9126. ctx.fill();
  9127. ctx.stroke();
  9128. this._label(ctx, this.label, this.x, this.y);
  9129. };
  9130. Node.prototype._resizeDatabase = function (ctx) {
  9131. if (!this.width) {
  9132. var margin = 5;
  9133. var textSize = this.getTextSize(ctx);
  9134. var size = textSize.width + 2 * margin;
  9135. this.width = size;
  9136. this.height = size;
  9137. // scaling used for clustering
  9138. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9139. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9140. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9141. this.growthIndicator = this.width - size;
  9142. }
  9143. };
  9144. Node.prototype._drawDatabase = function (ctx) {
  9145. this._resizeDatabase(ctx);
  9146. this.left = this.x - this.width / 2;
  9147. this.top = this.y - this.height / 2;
  9148. var clusterLineWidth = 2.5;
  9149. var selectionLineWidth = 2;
  9150. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9151. // draw the outer border
  9152. if (this.clusterSize > 1) {
  9153. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9154. ctx.lineWidth *= this.graphScaleInv;
  9155. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9156. 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);
  9157. ctx.stroke();
  9158. }
  9159. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9160. ctx.lineWidth *= this.graphScaleInv;
  9161. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9162. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9163. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  9164. ctx.fill();
  9165. ctx.stroke();
  9166. this._label(ctx, this.label, this.x, this.y);
  9167. };
  9168. Node.prototype._resizeCircle = function (ctx) {
  9169. if (!this.width) {
  9170. var margin = 5;
  9171. var textSize = this.getTextSize(ctx);
  9172. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  9173. this.radius = diameter / 2;
  9174. this.width = diameter;
  9175. this.height = diameter;
  9176. // scaling used for clustering
  9177. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  9178. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  9179. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9180. this.growthIndicator = this.radius - 0.5*diameter;
  9181. }
  9182. };
  9183. Node.prototype._drawCircle = function (ctx) {
  9184. this._resizeCircle(ctx);
  9185. this.left = this.x - this.width / 2;
  9186. this.top = this.y - this.height / 2;
  9187. var clusterLineWidth = 2.5;
  9188. var selectionLineWidth = 2;
  9189. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9190. // draw the outer border
  9191. if (this.clusterSize > 1) {
  9192. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9193. ctx.lineWidth *= this.graphScaleInv;
  9194. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9195. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  9196. ctx.stroke();
  9197. }
  9198. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9199. ctx.lineWidth *= this.graphScaleInv;
  9200. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9201. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9202. ctx.circle(this.x, this.y, this.radius);
  9203. ctx.fill();
  9204. ctx.stroke();
  9205. this._label(ctx, this.label, this.x, this.y);
  9206. };
  9207. Node.prototype._resizeEllipse = function (ctx) {
  9208. if (!this.width) {
  9209. var textSize = this.getTextSize(ctx);
  9210. this.width = textSize.width * 1.5;
  9211. this.height = textSize.height * 2;
  9212. if (this.width < this.height) {
  9213. this.width = this.height;
  9214. }
  9215. var defaultSize = this.width;
  9216. // scaling used for clustering
  9217. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9218. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9219. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9220. this.growthIndicator = this.width - defaultSize;
  9221. }
  9222. };
  9223. Node.prototype._drawEllipse = function (ctx) {
  9224. this._resizeEllipse(ctx);
  9225. this.left = this.x - this.width / 2;
  9226. this.top = this.y - this.height / 2;
  9227. var clusterLineWidth = 2.5;
  9228. var selectionLineWidth = 2;
  9229. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9230. // draw the outer border
  9231. if (this.clusterSize > 1) {
  9232. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9233. ctx.lineWidth *= this.graphScaleInv;
  9234. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9235. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  9236. ctx.stroke();
  9237. }
  9238. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9239. ctx.lineWidth *= this.graphScaleInv;
  9240. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9241. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9242. ctx.ellipse(this.left, this.top, this.width, this.height);
  9243. ctx.fill();
  9244. ctx.stroke();
  9245. this._label(ctx, this.label, this.x, this.y);
  9246. };
  9247. Node.prototype._drawDot = function (ctx) {
  9248. this._drawShape(ctx, 'circle');
  9249. };
  9250. Node.prototype._drawTriangle = function (ctx) {
  9251. this._drawShape(ctx, 'triangle');
  9252. };
  9253. Node.prototype._drawTriangleDown = function (ctx) {
  9254. this._drawShape(ctx, 'triangleDown');
  9255. };
  9256. Node.prototype._drawSquare = function (ctx) {
  9257. this._drawShape(ctx, 'square');
  9258. };
  9259. Node.prototype._drawStar = function (ctx) {
  9260. this._drawShape(ctx, 'star');
  9261. };
  9262. Node.prototype._resizeShape = function (ctx) {
  9263. if (!this.width) {
  9264. this.radius = this.baseRadiusValue;
  9265. var size = 2 * this.radius;
  9266. this.width = size;
  9267. this.height = size;
  9268. // scaling used for clustering
  9269. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9270. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9271. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  9272. this.growthIndicator = this.width - size;
  9273. }
  9274. };
  9275. Node.prototype._drawShape = function (ctx, shape) {
  9276. this._resizeShape(ctx);
  9277. this.left = this.x - this.width / 2;
  9278. this.top = this.y - this.height / 2;
  9279. var clusterLineWidth = 2.5;
  9280. var selectionLineWidth = 2;
  9281. var radiusMultiplier = 2;
  9282. // choose draw method depending on the shape
  9283. switch (shape) {
  9284. case 'dot': radiusMultiplier = 2; break;
  9285. case 'square': radiusMultiplier = 2; break;
  9286. case 'triangle': radiusMultiplier = 3; break;
  9287. case 'triangleDown': radiusMultiplier = 3; break;
  9288. case 'star': radiusMultiplier = 4; break;
  9289. }
  9290. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  9291. // draw the outer border
  9292. if (this.clusterSize > 1) {
  9293. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9294. ctx.lineWidth *= this.graphScaleInv;
  9295. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9296. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  9297. ctx.stroke();
  9298. }
  9299. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  9300. ctx.lineWidth *= this.graphScaleInv;
  9301. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  9302. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  9303. ctx[shape](this.x, this.y, this.radius);
  9304. ctx.fill();
  9305. ctx.stroke();
  9306. if (this.label) {
  9307. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  9308. }
  9309. };
  9310. Node.prototype._resizeText = function (ctx) {
  9311. if (!this.width) {
  9312. var margin = 5;
  9313. var textSize = this.getTextSize(ctx);
  9314. this.width = textSize.width + 2 * margin;
  9315. this.height = textSize.height + 2 * margin;
  9316. // scaling used for clustering
  9317. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9318. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9319. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9320. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  9321. }
  9322. };
  9323. Node.prototype._drawText = function (ctx) {
  9324. this._resizeText(ctx);
  9325. this.left = this.x - this.width / 2;
  9326. this.top = this.y - this.height / 2;
  9327. this._label(ctx, this.label, this.x, this.y);
  9328. };
  9329. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  9330. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  9331. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9332. ctx.fillStyle = this.fontColor || "black";
  9333. ctx.textAlign = align || "center";
  9334. ctx.textBaseline = baseline || "middle";
  9335. var lines = text.split('\n'),
  9336. lineCount = lines.length,
  9337. fontSize = (this.fontSize + 4),
  9338. yLine = y + (1 - lineCount) / 2 * fontSize;
  9339. for (var i = 0; i < lineCount; i++) {
  9340. ctx.fillText(lines[i], x, yLine);
  9341. yLine += fontSize;
  9342. }
  9343. }
  9344. };
  9345. Node.prototype.getTextSize = function(ctx) {
  9346. if (this.label !== undefined) {
  9347. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9348. var lines = this.label.split('\n'),
  9349. height = (this.fontSize + 4) * lines.length,
  9350. width = 0;
  9351. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  9352. width = Math.max(width, ctx.measureText(lines[i]).width);
  9353. }
  9354. return {"width": width, "height": height};
  9355. }
  9356. else {
  9357. return {"width": 0, "height": 0};
  9358. }
  9359. };
  9360. /**
  9361. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  9362. * there is a safety margin of 0.3 * width;
  9363. *
  9364. * @returns {boolean}
  9365. */
  9366. Node.prototype.inArea = function() {
  9367. if (this.width !== undefined) {
  9368. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  9369. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  9370. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  9371. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  9372. }
  9373. else {
  9374. return true;
  9375. }
  9376. };
  9377. /**
  9378. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  9379. * @returns {boolean}
  9380. */
  9381. Node.prototype.inView = function() {
  9382. return (this.x >= this.canvasTopLeft.x &&
  9383. this.x < this.canvasBottomRight.x &&
  9384. this.y >= this.canvasTopLeft.y &&
  9385. this.y < this.canvasBottomRight.y);
  9386. };
  9387. /**
  9388. * This allows the zoom level of the graph to influence the rendering
  9389. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  9390. *
  9391. * @param scale
  9392. * @param canvasTopLeft
  9393. * @param canvasBottomRight
  9394. */
  9395. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  9396. this.graphScaleInv = 1.0/scale;
  9397. this.graphScale = scale;
  9398. this.canvasTopLeft = canvasTopLeft;
  9399. this.canvasBottomRight = canvasBottomRight;
  9400. };
  9401. /**
  9402. * This allows the zoom level of the graph to influence the rendering
  9403. *
  9404. * @param scale
  9405. */
  9406. Node.prototype.setScale = function(scale) {
  9407. this.graphScaleInv = 1.0/scale;
  9408. this.graphScale = scale;
  9409. };
  9410. /**
  9411. * set the velocity at 0. Is called when this node is contained in another during clustering
  9412. */
  9413. Node.prototype.clearVelocity = function() {
  9414. this.vx = 0;
  9415. this.vy = 0;
  9416. };
  9417. /**
  9418. * Basic preservation of (kinectic) energy
  9419. *
  9420. * @param massBeforeClustering
  9421. */
  9422. Node.prototype.updateVelocity = function(massBeforeClustering) {
  9423. var energyBefore = this.vx * this.vx * massBeforeClustering;
  9424. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9425. this.vx = Math.sqrt(energyBefore/this.mass);
  9426. energyBefore = this.vy * this.vy * massBeforeClustering;
  9427. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9428. this.vy = Math.sqrt(energyBefore/this.mass);
  9429. };
  9430. /**
  9431. * @class Edge
  9432. *
  9433. * A edge connects two nodes
  9434. * @param {Object} properties Object with properties. Must contain
  9435. * At least properties from and to.
  9436. * Available properties: from (number),
  9437. * to (number), label (string, color (string),
  9438. * width (number), style (string),
  9439. * length (number), title (string)
  9440. * @param {Graph} graph A graph object, used to find and edge to
  9441. * nodes.
  9442. * @param {Object} constants An object with default values for
  9443. * example for the color
  9444. */
  9445. function Edge (properties, graph, constants) {
  9446. if (!graph) {
  9447. throw "No graph provided";
  9448. }
  9449. this.graph = graph;
  9450. // initialize constants
  9451. this.widthMin = constants.edges.widthMin;
  9452. this.widthMax = constants.edges.widthMax;
  9453. // initialize variables
  9454. this.id = undefined;
  9455. this.fromId = undefined;
  9456. this.toId = undefined;
  9457. this.style = constants.edges.style;
  9458. this.title = undefined;
  9459. this.width = constants.edges.width;
  9460. this.value = undefined;
  9461. this.length = constants.physics.springLength;
  9462. this.customLength = false;
  9463. this.selected = false;
  9464. this.smooth = constants.smoothCurves;
  9465. this.from = null; // a node
  9466. this.to = null; // a node
  9467. this.via = null; // a temp node
  9468. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  9469. // by storing the original information we can revert to the original connection when the cluser is opened.
  9470. this.originalFromId = [];
  9471. this.originalToId = [];
  9472. this.connected = false;
  9473. // Added to support dashed lines
  9474. // David Jordan
  9475. // 2012-08-08
  9476. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  9477. this.color = constants.edges.color;
  9478. this.widthFixed = false;
  9479. this.lengthFixed = false;
  9480. this.setProperties(properties, constants);
  9481. }
  9482. /**
  9483. * Set or overwrite properties for the edge
  9484. * @param {Object} properties an object with properties
  9485. * @param {Object} constants and object with default, global properties
  9486. */
  9487. Edge.prototype.setProperties = function(properties, constants) {
  9488. if (!properties) {
  9489. return;
  9490. }
  9491. if (properties.from !== undefined) {this.fromId = properties.from;}
  9492. if (properties.to !== undefined) {this.toId = properties.to;}
  9493. if (properties.id !== undefined) {this.id = properties.id;}
  9494. if (properties.style !== undefined) {this.style = properties.style;}
  9495. if (properties.label !== undefined) {this.label = properties.label;}
  9496. if (this.label) {
  9497. this.fontSize = constants.edges.fontSize;
  9498. this.fontFace = constants.edges.fontFace;
  9499. this.fontColor = constants.edges.fontColor;
  9500. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  9501. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  9502. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  9503. }
  9504. if (properties.title !== undefined) {this.title = properties.title;}
  9505. if (properties.width !== undefined) {this.width = properties.width;}
  9506. if (properties.value !== undefined) {this.value = properties.value;}
  9507. if (properties.length !== undefined) {this.length = properties.length;
  9508. this.customLength = true;}
  9509. // Added to support dashed lines
  9510. // David Jordan
  9511. // 2012-08-08
  9512. if (properties.dash) {
  9513. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  9514. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  9515. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  9516. }
  9517. if (properties.color !== undefined) {this.color = properties.color;}
  9518. // A node is connected when it has a from and to node.
  9519. this.connect();
  9520. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  9521. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  9522. // set draw method based on style
  9523. switch (this.style) {
  9524. case 'line': this.draw = this._drawLine; break;
  9525. case 'arrow': this.draw = this._drawArrow; break;
  9526. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  9527. case 'dash-line': this.draw = this._drawDashLine; break;
  9528. default: this.draw = this._drawLine; break;
  9529. }
  9530. };
  9531. /**
  9532. * Connect an edge to its nodes
  9533. */
  9534. Edge.prototype.connect = function () {
  9535. this.disconnect();
  9536. this.from = this.graph.nodes[this.fromId] || null;
  9537. this.to = this.graph.nodes[this.toId] || null;
  9538. this.connected = (this.from && this.to);
  9539. if (this.connected) {
  9540. this.from.attachEdge(this);
  9541. this.to.attachEdge(this);
  9542. }
  9543. else {
  9544. if (this.from) {
  9545. this.from.detachEdge(this);
  9546. }
  9547. if (this.to) {
  9548. this.to.detachEdge(this);
  9549. }
  9550. }
  9551. };
  9552. /**
  9553. * Disconnect an edge from its nodes
  9554. */
  9555. Edge.prototype.disconnect = function () {
  9556. if (this.from) {
  9557. this.from.detachEdge(this);
  9558. this.from = null;
  9559. }
  9560. if (this.to) {
  9561. this.to.detachEdge(this);
  9562. this.to = null;
  9563. }
  9564. this.connected = false;
  9565. };
  9566. /**
  9567. * get the title of this edge.
  9568. * @return {string} title The title of the edge, or undefined when no title
  9569. * has been set.
  9570. */
  9571. Edge.prototype.getTitle = function() {
  9572. return this.title;
  9573. };
  9574. /**
  9575. * Retrieve the value of the edge. Can be undefined
  9576. * @return {Number} value
  9577. */
  9578. Edge.prototype.getValue = function() {
  9579. return this.value;
  9580. };
  9581. /**
  9582. * Adjust the value range of the edge. The edge will adjust it's width
  9583. * based on its value.
  9584. * @param {Number} min
  9585. * @param {Number} max
  9586. */
  9587. Edge.prototype.setValueRange = function(min, max) {
  9588. if (!this.widthFixed && this.value !== undefined) {
  9589. var scale = (this.widthMax - this.widthMin) / (max - min);
  9590. this.width = (this.value - min) * scale + this.widthMin;
  9591. }
  9592. };
  9593. /**
  9594. * Redraw a edge
  9595. * Draw this edge in the given canvas
  9596. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9597. * @param {CanvasRenderingContext2D} ctx
  9598. */
  9599. Edge.prototype.draw = function(ctx) {
  9600. throw "Method draw not initialized in edge";
  9601. };
  9602. /**
  9603. * Check if this object is overlapping with the provided object
  9604. * @param {Object} obj an object with parameters left, top
  9605. * @return {boolean} True if location is located on the edge
  9606. */
  9607. Edge.prototype.isOverlappingWith = function(obj) {
  9608. var distMax = 10;
  9609. var xFrom = this.from.x;
  9610. var yFrom = this.from.y;
  9611. var xTo = this.to.x;
  9612. var yTo = this.to.y;
  9613. var xObj = obj.left;
  9614. var yObj = obj.top;
  9615. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  9616. return (dist < distMax);
  9617. };
  9618. /**
  9619. * Redraw a edge as a line
  9620. * Draw this edge in the given canvas
  9621. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9622. * @param {CanvasRenderingContext2D} ctx
  9623. * @private
  9624. */
  9625. Edge.prototype._drawLine = function(ctx) {
  9626. // set style
  9627. ctx.strokeStyle = this.color;
  9628. ctx.lineWidth = this._getLineWidth();
  9629. var point;
  9630. if (this.from != this.to) {
  9631. // draw line
  9632. this._line(ctx);
  9633. // draw label
  9634. if (this.label) {
  9635. point = this._pointOnLine(0.5);
  9636. this._label(ctx, this.label, point.x, point.y);
  9637. }
  9638. }
  9639. else {
  9640. var x, y;
  9641. var radius = this.length / 4;
  9642. var node = this.from;
  9643. if (!node.width) {
  9644. node.resize(ctx);
  9645. }
  9646. if (node.width > node.height) {
  9647. x = node.x + node.width / 2;
  9648. y = node.y - radius;
  9649. }
  9650. else {
  9651. x = node.x + radius;
  9652. y = node.y - node.height / 2;
  9653. }
  9654. this._circle(ctx, x, y, radius);
  9655. point = this._pointOnCircle(x, y, radius, 0.5);
  9656. this._label(ctx, this.label, point.x, point.y);
  9657. }
  9658. };
  9659. /**
  9660. * Get the line width of the edge. Depends on width and whether one of the
  9661. * connected nodes is selected.
  9662. * @return {Number} width
  9663. * @private
  9664. */
  9665. Edge.prototype._getLineWidth = function() {
  9666. if (this.selected == true) {
  9667. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  9668. }
  9669. else {
  9670. return this.width*this.graphScaleInv;
  9671. }
  9672. };
  9673. /**
  9674. * Draw a line between two nodes
  9675. * @param {CanvasRenderingContext2D} ctx
  9676. * @private
  9677. */
  9678. Edge.prototype._line = function (ctx) {
  9679. // draw a straight line
  9680. ctx.beginPath();
  9681. ctx.moveTo(this.from.x, this.from.y);
  9682. if (this.smooth == true) {
  9683. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9684. }
  9685. else {
  9686. ctx.lineTo(this.to.x, this.to.y);
  9687. }
  9688. ctx.stroke();
  9689. };
  9690. /**
  9691. * Draw a line from a node to itself, a circle
  9692. * @param {CanvasRenderingContext2D} ctx
  9693. * @param {Number} x
  9694. * @param {Number} y
  9695. * @param {Number} radius
  9696. * @private
  9697. */
  9698. Edge.prototype._circle = function (ctx, x, y, radius) {
  9699. // draw a circle
  9700. ctx.beginPath();
  9701. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9702. ctx.stroke();
  9703. };
  9704. /**
  9705. * Draw label with white background and with the middle at (x, y)
  9706. * @param {CanvasRenderingContext2D} ctx
  9707. * @param {String} text
  9708. * @param {Number} x
  9709. * @param {Number} y
  9710. * @private
  9711. */
  9712. Edge.prototype._label = function (ctx, text, x, y) {
  9713. if (text) {
  9714. // TODO: cache the calculated size
  9715. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  9716. this.fontSize + "px " + this.fontFace;
  9717. ctx.fillStyle = 'white';
  9718. var width = ctx.measureText(text).width;
  9719. var height = this.fontSize;
  9720. var left = x - width / 2;
  9721. var top = y - height / 2;
  9722. ctx.fillRect(left, top, width, height);
  9723. // draw text
  9724. ctx.fillStyle = this.fontColor || "black";
  9725. ctx.textAlign = "left";
  9726. ctx.textBaseline = "top";
  9727. ctx.fillText(text, left, top);
  9728. }
  9729. };
  9730. /**
  9731. * Redraw a edge as a dashed line
  9732. * Draw this edge in the given canvas
  9733. * @author David Jordan
  9734. * @date 2012-08-08
  9735. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9736. * @param {CanvasRenderingContext2D} ctx
  9737. * @private
  9738. */
  9739. Edge.prototype._drawDashLine = function(ctx) {
  9740. // set style
  9741. ctx.strokeStyle = this.color;
  9742. ctx.lineWidth = this._getLineWidth();
  9743. // only firefox and chrome support this method, else we use the legacy one.
  9744. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  9745. ctx.beginPath();
  9746. ctx.moveTo(this.from.x, this.from.y);
  9747. // configure the dash pattern
  9748. var pattern = [0];
  9749. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  9750. pattern = [this.dash.length,this.dash.gap];
  9751. }
  9752. else {
  9753. pattern = [5,5];
  9754. }
  9755. // set dash settings for chrome or firefox
  9756. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9757. ctx.setLineDash(pattern);
  9758. ctx.lineDashOffset = 0;
  9759. } else { //Firefox
  9760. ctx.mozDash = pattern;
  9761. ctx.mozDashOffset = 0;
  9762. }
  9763. // draw the line
  9764. if (this.smooth == true) {
  9765. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9766. }
  9767. else {
  9768. ctx.lineTo(this.to.x, this.to.y);
  9769. }
  9770. ctx.stroke();
  9771. // restore the dash settings.
  9772. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9773. ctx.setLineDash([0]);
  9774. ctx.lineDashOffset = 0;
  9775. } else { //Firefox
  9776. ctx.mozDash = [0];
  9777. ctx.mozDashOffset = 0;
  9778. }
  9779. }
  9780. else { // unsupporting smooth lines
  9781. // draw dashed line
  9782. ctx.beginPath();
  9783. ctx.lineCap = 'round';
  9784. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9785. {
  9786. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9787. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  9788. }
  9789. 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
  9790. {
  9791. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9792. [this.dash.length,this.dash.gap]);
  9793. }
  9794. else //If all else fails draw a line
  9795. {
  9796. ctx.moveTo(this.from.x, this.from.y);
  9797. ctx.lineTo(this.to.x, this.to.y);
  9798. }
  9799. ctx.stroke();
  9800. }
  9801. // draw label
  9802. if (this.label) {
  9803. var point = this._pointOnLine(0.5);
  9804. this._label(ctx, this.label, point.x, point.y);
  9805. }
  9806. };
  9807. /**
  9808. * Get a point on a line
  9809. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9810. * @return {Object} point
  9811. * @private
  9812. */
  9813. Edge.prototype._pointOnLine = function (percentage) {
  9814. return {
  9815. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9816. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9817. }
  9818. };
  9819. /**
  9820. * Get a point on a circle
  9821. * @param {Number} x
  9822. * @param {Number} y
  9823. * @param {Number} radius
  9824. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9825. * @return {Object} point
  9826. * @private
  9827. */
  9828. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9829. var angle = (percentage - 3/8) * 2 * Math.PI;
  9830. return {
  9831. x: x + radius * Math.cos(angle),
  9832. y: y - radius * Math.sin(angle)
  9833. }
  9834. };
  9835. /**
  9836. * Redraw a edge as a line with an arrow halfway the line
  9837. * Draw this edge in the given canvas
  9838. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9839. * @param {CanvasRenderingContext2D} ctx
  9840. * @private
  9841. */
  9842. Edge.prototype._drawArrowCenter = function(ctx) {
  9843. var point;
  9844. // set style
  9845. ctx.strokeStyle = this.color;
  9846. ctx.fillStyle = this.color;
  9847. ctx.lineWidth = this._getLineWidth();
  9848. if (this.from != this.to) {
  9849. // draw line
  9850. this._line(ctx);
  9851. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9852. var length = 10 + 5 * this.width; // TODO: make customizable?
  9853. // draw an arrow halfway the line
  9854. if (this.smooth == true) {
  9855. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9856. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9857. point = {x:midpointX, y:midpointY};
  9858. }
  9859. else {
  9860. point = this._pointOnLine(0.5);
  9861. }
  9862. ctx.arrow(point.x, point.y, angle, length);
  9863. ctx.fill();
  9864. ctx.stroke();
  9865. // draw label
  9866. if (this.label) {
  9867. point = this._pointOnLine(0.5);
  9868. this._label(ctx, this.label, point.x, point.y);
  9869. }
  9870. }
  9871. else {
  9872. // draw circle
  9873. var x, y;
  9874. var radius = 0.25 * Math.max(100,this.length);
  9875. var node = this.from;
  9876. if (!node.width) {
  9877. node.resize(ctx);
  9878. }
  9879. if (node.width > node.height) {
  9880. x = node.x + node.width * 0.5;
  9881. y = node.y - radius;
  9882. }
  9883. else {
  9884. x = node.x + radius;
  9885. y = node.y - node.height * 0.5;
  9886. }
  9887. this._circle(ctx, x, y, radius);
  9888. // draw all arrows
  9889. var angle = 0.2 * Math.PI;
  9890. var length = 10 + 5 * this.width; // TODO: make customizable?
  9891. point = this._pointOnCircle(x, y, radius, 0.5);
  9892. ctx.arrow(point.x, point.y, angle, length);
  9893. ctx.fill();
  9894. ctx.stroke();
  9895. // draw label
  9896. if (this.label) {
  9897. point = this._pointOnCircle(x, y, radius, 0.5);
  9898. this._label(ctx, this.label, point.x, point.y);
  9899. }
  9900. }
  9901. };
  9902. /**
  9903. * Redraw a edge as a line with an arrow
  9904. * Draw this edge in the given canvas
  9905. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9906. * @param {CanvasRenderingContext2D} ctx
  9907. * @private
  9908. */
  9909. Edge.prototype._drawArrow = function(ctx) {
  9910. // set style
  9911. ctx.strokeStyle = this.color;
  9912. ctx.fillStyle = this.color;
  9913. ctx.lineWidth = this._getLineWidth();
  9914. var angle, length;
  9915. //draw a line
  9916. if (this.from != this.to) {
  9917. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9918. var dx = (this.to.x - this.from.x);
  9919. var dy = (this.to.y - this.from.y);
  9920. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9921. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9922. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9923. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9924. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9925. if (this.smooth == true) {
  9926. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9927. dx = (this.to.x - this.via.x);
  9928. dy = (this.to.y - this.via.y);
  9929. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9930. }
  9931. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9932. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9933. var xTo,yTo;
  9934. if (this.smooth == true) {
  9935. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9936. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9937. }
  9938. else {
  9939. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9940. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9941. }
  9942. ctx.beginPath();
  9943. ctx.moveTo(xFrom,yFrom);
  9944. if (this.smooth == true) {
  9945. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9946. }
  9947. else {
  9948. ctx.lineTo(xTo, yTo);
  9949. }
  9950. ctx.stroke();
  9951. // draw arrow at the end of the line
  9952. length = 10 + 5 * this.width;
  9953. ctx.arrow(xTo, yTo, angle, length);
  9954. ctx.fill();
  9955. ctx.stroke();
  9956. // draw label
  9957. if (this.label) {
  9958. var point = this._pointOnLine(0.5);
  9959. this._label(ctx, this.label, point.x, point.y);
  9960. }
  9961. }
  9962. else {
  9963. // draw circle
  9964. var node = this.from;
  9965. var x, y, arrow;
  9966. var radius = 0.25 * Math.max(100,this.length);
  9967. if (!node.width) {
  9968. node.resize(ctx);
  9969. }
  9970. if (node.width > node.height) {
  9971. x = node.x + node.width * 0.5;
  9972. y = node.y - radius;
  9973. arrow = {
  9974. x: x,
  9975. y: node.y,
  9976. angle: 0.9 * Math.PI
  9977. };
  9978. }
  9979. else {
  9980. x = node.x + radius;
  9981. y = node.y - node.height * 0.5;
  9982. arrow = {
  9983. x: node.x,
  9984. y: y,
  9985. angle: 0.6 * Math.PI
  9986. };
  9987. }
  9988. ctx.beginPath();
  9989. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9990. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9991. ctx.stroke();
  9992. // draw all arrows
  9993. length = 10 + 5 * this.width; // TODO: make customizable?
  9994. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9995. ctx.fill();
  9996. ctx.stroke();
  9997. // draw label
  9998. if (this.label) {
  9999. point = this._pointOnCircle(x, y, radius, 0.5);
  10000. this._label(ctx, this.label, point.x, point.y);
  10001. }
  10002. }
  10003. };
  10004. /**
  10005. * Calculate the distance between a point (x3,y3) and a line segment from
  10006. * (x1,y1) to (x2,y2).
  10007. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  10008. * @param {number} x1
  10009. * @param {number} y1
  10010. * @param {number} x2
  10011. * @param {number} y2
  10012. * @param {number} x3
  10013. * @param {number} y3
  10014. * @private
  10015. */
  10016. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  10017. if (this.smooth == true) {
  10018. var minDistance = 1e9;
  10019. var i,t,x,y,dx,dy;
  10020. for (i = 0; i < 10; i++) {
  10021. t = 0.1*i;
  10022. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  10023. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  10024. dx = Math.abs(x3-x);
  10025. dy = Math.abs(y3-y);
  10026. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  10027. }
  10028. return minDistance
  10029. }
  10030. else {
  10031. var px = x2-x1,
  10032. py = y2-y1,
  10033. something = px*px + py*py,
  10034. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  10035. if (u > 1) {
  10036. u = 1;
  10037. }
  10038. else if (u < 0) {
  10039. u = 0;
  10040. }
  10041. var x = x1 + u * px,
  10042. y = y1 + u * py,
  10043. dx = x - x3,
  10044. dy = y - y3;
  10045. //# Note: If the actual distance does not matter,
  10046. //# if you only want to compare what this function
  10047. //# returns to other results of this function, you
  10048. //# can just return the squared distance instead
  10049. //# (i.e. remove the sqrt) to gain a little performance
  10050. return Math.sqrt(dx*dx + dy*dy);
  10051. }
  10052. };
  10053. /**
  10054. * This allows the zoom level of the graph to influence the rendering
  10055. *
  10056. * @param scale
  10057. */
  10058. Edge.prototype.setScale = function(scale) {
  10059. this.graphScaleInv = 1.0/scale;
  10060. };
  10061. Edge.prototype.select = function() {
  10062. this.selected = true;
  10063. };
  10064. Edge.prototype.unselect = function() {
  10065. this.selected = false;
  10066. };
  10067. Edge.prototype.positionBezierNode = function() {
  10068. if (this.via !== null) {
  10069. this.via.x = 0.5 * (this.from.x + this.to.x);
  10070. this.via.y = 0.5 * (this.from.y + this.to.y);
  10071. }
  10072. };
  10073. /**
  10074. * Popup is a class to create a popup window with some text
  10075. * @param {Element} container The container object.
  10076. * @param {Number} [x]
  10077. * @param {Number} [y]
  10078. * @param {String} [text]
  10079. */
  10080. function Popup(container, x, y, text) {
  10081. if (container) {
  10082. this.container = container;
  10083. }
  10084. else {
  10085. this.container = document.body;
  10086. }
  10087. this.x = 0;
  10088. this.y = 0;
  10089. this.padding = 5;
  10090. if (x !== undefined && y !== undefined ) {
  10091. this.setPosition(x, y);
  10092. }
  10093. if (text !== undefined) {
  10094. this.setText(text);
  10095. }
  10096. // create the frame
  10097. this.frame = document.createElement("div");
  10098. var style = this.frame.style;
  10099. style.position = "absolute";
  10100. style.visibility = "hidden";
  10101. style.border = "1px solid #666";
  10102. style.color = "black";
  10103. style.padding = this.padding + "px";
  10104. style.backgroundColor = "#FFFFC6";
  10105. style.borderRadius = "3px";
  10106. style.MozBorderRadius = "3px";
  10107. style.WebkitBorderRadius = "3px";
  10108. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  10109. style.whiteSpace = "nowrap";
  10110. this.container.appendChild(this.frame);
  10111. }
  10112. /**
  10113. * @param {number} x Horizontal position of the popup window
  10114. * @param {number} y Vertical position of the popup window
  10115. */
  10116. Popup.prototype.setPosition = function(x, y) {
  10117. this.x = parseInt(x);
  10118. this.y = parseInt(y);
  10119. };
  10120. /**
  10121. * Set the text for the popup window. This can be HTML code
  10122. * @param {string} text
  10123. */
  10124. Popup.prototype.setText = function(text) {
  10125. this.frame.innerHTML = text;
  10126. };
  10127. /**
  10128. * Show the popup window
  10129. * @param {boolean} show Optional. Show or hide the window
  10130. */
  10131. Popup.prototype.show = function (show) {
  10132. if (show === undefined) {
  10133. show = true;
  10134. }
  10135. if (show) {
  10136. var height = this.frame.clientHeight;
  10137. var width = this.frame.clientWidth;
  10138. var maxHeight = this.frame.parentNode.clientHeight;
  10139. var maxWidth = this.frame.parentNode.clientWidth;
  10140. var top = (this.y - height);
  10141. if (top + height + this.padding > maxHeight) {
  10142. top = maxHeight - height - this.padding;
  10143. }
  10144. if (top < this.padding) {
  10145. top = this.padding;
  10146. }
  10147. var left = this.x;
  10148. if (left + width + this.padding > maxWidth) {
  10149. left = maxWidth - width - this.padding;
  10150. }
  10151. if (left < this.padding) {
  10152. left = this.padding;
  10153. }
  10154. this.frame.style.left = left + "px";
  10155. this.frame.style.top = top + "px";
  10156. this.frame.style.visibility = "visible";
  10157. }
  10158. else {
  10159. this.hide();
  10160. }
  10161. };
  10162. /**
  10163. * Hide the popup window
  10164. */
  10165. Popup.prototype.hide = function () {
  10166. this.frame.style.visibility = "hidden";
  10167. };
  10168. /**
  10169. * @class Groups
  10170. * This class can store groups and properties specific for groups.
  10171. */
  10172. Groups = function () {
  10173. this.clear();
  10174. this.defaultIndex = 0;
  10175. };
  10176. /**
  10177. * default constants for group colors
  10178. */
  10179. Groups.DEFAULT = [
  10180. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  10181. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  10182. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  10183. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  10184. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  10185. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  10186. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  10187. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  10188. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  10189. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  10190. ];
  10191. /**
  10192. * Clear all groups
  10193. */
  10194. Groups.prototype.clear = function () {
  10195. this.groups = {};
  10196. this.groups.length = function()
  10197. {
  10198. var i = 0;
  10199. for ( var p in this ) {
  10200. if (this.hasOwnProperty(p)) {
  10201. i++;
  10202. }
  10203. }
  10204. return i;
  10205. }
  10206. };
  10207. /**
  10208. * get group properties of a groupname. If groupname is not found, a new group
  10209. * is added.
  10210. * @param {*} groupname Can be a number, string, Date, etc.
  10211. * @return {Object} group The created group, containing all group properties
  10212. */
  10213. Groups.prototype.get = function (groupname) {
  10214. var group = this.groups[groupname];
  10215. if (group == undefined) {
  10216. // create new group
  10217. var index = this.defaultIndex % Groups.DEFAULT.length;
  10218. this.defaultIndex++;
  10219. group = {};
  10220. group.color = Groups.DEFAULT[index];
  10221. this.groups[groupname] = group;
  10222. }
  10223. return group;
  10224. };
  10225. /**
  10226. * Add a custom group style
  10227. * @param {String} groupname
  10228. * @param {Object} style An object containing borderColor,
  10229. * backgroundColor, etc.
  10230. * @return {Object} group The created group object
  10231. */
  10232. Groups.prototype.add = function (groupname, style) {
  10233. this.groups[groupname] = style;
  10234. if (style.color) {
  10235. style.color = Node.parseColor(style.color);
  10236. }
  10237. return style;
  10238. };
  10239. /**
  10240. * @class Images
  10241. * This class loads images and keeps them stored.
  10242. */
  10243. Images = function () {
  10244. this.images = {};
  10245. this.callback = undefined;
  10246. };
  10247. /**
  10248. * Set an onload callback function. This will be called each time an image
  10249. * is loaded
  10250. * @param {function} callback
  10251. */
  10252. Images.prototype.setOnloadCallback = function(callback) {
  10253. this.callback = callback;
  10254. };
  10255. /**
  10256. *
  10257. * @param {string} url Url of the image
  10258. * @return {Image} img The image object
  10259. */
  10260. Images.prototype.load = function(url) {
  10261. var img = this.images[url];
  10262. if (img == undefined) {
  10263. // create the image
  10264. var images = this;
  10265. img = new Image();
  10266. this.images[url] = img;
  10267. img.onload = function() {
  10268. if (images.callback) {
  10269. images.callback(this);
  10270. }
  10271. };
  10272. img.src = url;
  10273. }
  10274. return img;
  10275. };
  10276. /**
  10277. * Created by Alex on 2/6/14.
  10278. */
  10279. var physicsMixin = {
  10280. /**
  10281. * Toggling barnes Hut calculation on and off.
  10282. *
  10283. * @private
  10284. */
  10285. _toggleBarnesHut : function() {
  10286. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  10287. this._loadSelectedForceSolver();
  10288. this.moving = true;
  10289. this.start();
  10290. },
  10291. /**
  10292. * This loads the node force solver based on the barnes hut or repulsion algorithm
  10293. *
  10294. * @private
  10295. */
  10296. _loadSelectedForceSolver : function() {
  10297. // this overloads the this._calculateNodeForces
  10298. if (this.constants.physics.barnesHut.enabled == true) {
  10299. this._clearMixin(repulsionMixin);
  10300. this._clearMixin(hierarchalRepulsionMixin);
  10301. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  10302. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  10303. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  10304. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  10305. this._loadMixin(barnesHutMixin);
  10306. }
  10307. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  10308. this._clearMixin(barnesHutMixin);
  10309. this._clearMixin(repulsionMixin);
  10310. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  10311. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  10312. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  10313. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  10314. this._loadMixin(hierarchalRepulsionMixin);
  10315. }
  10316. else {
  10317. this._clearMixin(barnesHutMixin);
  10318. this._clearMixin(hierarchalRepulsionMixin);
  10319. this.barnesHutTree = undefined;
  10320. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  10321. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  10322. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  10323. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  10324. this._loadMixin(repulsionMixin);
  10325. }
  10326. },
  10327. /**
  10328. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  10329. * if there is more than one node. If it is just one node, we dont calculate anything.
  10330. *
  10331. * @private
  10332. */
  10333. _initializeForceCalculation : function() {
  10334. // stop calculation if there is only one node
  10335. if (this.nodeIndices.length == 1) {
  10336. this.nodes[this.nodeIndices[0]]._setForce(0,0);
  10337. }
  10338. else {
  10339. // if there are too many nodes on screen, we cluster without repositioning
  10340. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  10341. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  10342. }
  10343. // we now start the force calculation
  10344. this._calculateForces();
  10345. }
  10346. },
  10347. /**
  10348. * Calculate the external forces acting on the nodes
  10349. * Forces are caused by: edges, repulsing forces between nodes, gravity
  10350. * @private
  10351. */
  10352. _calculateForces : function() {
  10353. // Gravity is required to keep separated groups from floating off
  10354. // the forces are reset to zero in this loop by using _setForce instead
  10355. // of _addForce
  10356. this._calculateGravitationalForces();
  10357. this._calculateNodeForces();
  10358. if (this.constants.smoothCurves == true) {
  10359. this._calculateSpringForcesWithSupport();
  10360. }
  10361. else {
  10362. this._calculateSpringForces();
  10363. }
  10364. },
  10365. /**
  10366. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  10367. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  10368. * This function joins the datanodes and invisible (called support) nodes into one object.
  10369. * We do this so we do not contaminate this.nodes with the support nodes.
  10370. *
  10371. * @private
  10372. */
  10373. _updateCalculationNodes : function() {
  10374. if (this.constants.smoothCurves == true) {
  10375. this.calculationNodes = {};
  10376. this.calculationNodeIndices = [];
  10377. for (var nodeId in this.nodes) {
  10378. if (this.nodes.hasOwnProperty(nodeId)) {
  10379. this.calculationNodes[nodeId] = this.nodes[nodeId];
  10380. }
  10381. }
  10382. var supportNodes = this.sectors['support']['nodes'];
  10383. for (var supportNodeId in supportNodes) {
  10384. if (supportNodes.hasOwnProperty(supportNodeId)) {
  10385. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  10386. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  10387. }
  10388. else {
  10389. supportNodes[supportNodeId]._setForce(0,0);
  10390. }
  10391. }
  10392. }
  10393. for (var idx in this.calculationNodes) {
  10394. if (this.calculationNodes.hasOwnProperty(idx)) {
  10395. this.calculationNodeIndices.push(idx);
  10396. }
  10397. }
  10398. }
  10399. else {
  10400. this.calculationNodes = this.nodes;
  10401. this.calculationNodeIndices = this.nodeIndices;
  10402. }
  10403. },
  10404. /**
  10405. * this function applies the central gravity effect to keep groups from floating off
  10406. *
  10407. * @private
  10408. */
  10409. _calculateGravitationalForces : function() {
  10410. var dx, dy, distance, node, i;
  10411. var nodes = this.calculationNodes;
  10412. var gravity = this.constants.physics.centralGravity;
  10413. var gravityForce = 0;
  10414. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  10415. node = nodes[this.calculationNodeIndices[i]];
  10416. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  10417. // gravity does not apply when we are in a pocket sector
  10418. if (this._sector() == "default" && gravity != 0) {
  10419. dx = -node.x;
  10420. dy = -node.y;
  10421. distance = Math.sqrt(dx*dx + dy*dy);
  10422. gravityForce = gravity / distance;
  10423. node.fx = dx * gravityForce;
  10424. node.fy = dy * gravityForce;
  10425. }
  10426. else {
  10427. node.fx = 0;
  10428. node.fy = 0;
  10429. }
  10430. }
  10431. },
  10432. /**
  10433. * this function calculates the effects of the springs in the case of unsmooth curves.
  10434. *
  10435. * @private
  10436. */
  10437. _calculateSpringForces : function() {
  10438. var edgeLength, edge, edgeId;
  10439. var dx, dy, fx, fy, springForce, length;
  10440. var edges = this.edges;
  10441. // forces caused by the edges, modelled as springs
  10442. for (edgeId in edges) {
  10443. if (edges.hasOwnProperty(edgeId)) {
  10444. edge = edges[edgeId];
  10445. if (edge.connected) {
  10446. // only calculate forces if nodes are in the same sector
  10447. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10448. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10449. // this implies that the edges between big clusters are longer
  10450. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  10451. dx = (edge.from.x - edge.to.x);
  10452. dy = (edge.from.y - edge.to.y);
  10453. length = Math.sqrt(dx * dx + dy * dy);
  10454. if (length == 0) {
  10455. length = 0.01;
  10456. }
  10457. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  10458. fx = dx * springForce;
  10459. fy = dy * springForce;
  10460. edge.from.fx += fx;
  10461. edge.from.fy += fy;
  10462. edge.to.fx -= fx;
  10463. edge.to.fy -= fy;
  10464. }
  10465. }
  10466. }
  10467. }
  10468. },
  10469. /**
  10470. * This function calculates the springforces on the nodes, accounting for the support nodes.
  10471. *
  10472. * @private
  10473. */
  10474. _calculateSpringForcesWithSupport : function() {
  10475. var edgeLength, edge, edgeId, combinedClusterSize;
  10476. var edges = this.edges;
  10477. // forces caused by the edges, modelled as springs
  10478. for (edgeId in edges) {
  10479. if (edges.hasOwnProperty(edgeId)) {
  10480. edge = edges[edgeId];
  10481. if (edge.connected) {
  10482. // only calculate forces if nodes are in the same sector
  10483. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10484. if (edge.via != null) {
  10485. var node1 = edge.to;
  10486. var node2 = edge.via;
  10487. var node3 = edge.from;
  10488. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10489. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  10490. // this implies that the edges between big clusters are longer
  10491. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  10492. this._calculateSpringForce(node1,node2,0.5*edgeLength);
  10493. this._calculateSpringForce(node2,node3,0.5*edgeLength);
  10494. }
  10495. }
  10496. }
  10497. }
  10498. }
  10499. },
  10500. /**
  10501. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  10502. *
  10503. * @param node1
  10504. * @param node2
  10505. * @param edgeLength
  10506. * @private
  10507. */
  10508. _calculateSpringForce : function(node1,node2,edgeLength) {
  10509. var dx, dy, fx, fy, springForce, length;
  10510. dx = (node1.x - node2.x);
  10511. dy = (node1.y - node2.y);
  10512. length = Math.sqrt(dx * dx + dy * dy);
  10513. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  10514. if (length == 0) {
  10515. length = 0.01;
  10516. }
  10517. fx = dx * springForce;
  10518. fy = dy * springForce;
  10519. node1.fx += fx;
  10520. node1.fy += fy;
  10521. node2.fx -= fx;
  10522. node2.fy -= fy;
  10523. },
  10524. /**
  10525. * Load the HTML for the physics config and bind it
  10526. * @private
  10527. */
  10528. _loadPhysicsConfiguration : function() {
  10529. if (this.physicsConfiguration === undefined) {
  10530. this.physicsConfiguration = document.createElement('div');
  10531. this.physicsConfiguration.className = "PhysicsConfiguration";
  10532. this.physicsConfiguration.innerHTML = '' +
  10533. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  10534. '<tr>' +
  10535. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  10536. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>'+
  10537. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  10538. '</tr>'+
  10539. '</table>' +
  10540. '<table id="graph_BH_table" style="display:none">'+
  10541. '<tr><td><b>Barnes Hut</b></td></tr>'+
  10542. '<tr>'+
  10543. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" max="20000" value="2000" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="-2000" id="graph_BH_gc_value" style="width:60px"></td>'+
  10544. '</tr>'+
  10545. '<tr>'+
  10546. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="0.3" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="0.03" id="graph_BH_cg_value" style="width:60px"></td>'+
  10547. '</tr>'+
  10548. '<tr>'+
  10549. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="100" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="100" id="graph_BH_sl_value" style="width:60px"></td>'+
  10550. '</tr>'+
  10551. '<tr>'+
  10552. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="0.05" step="0.005" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="0.05" id="graph_BH_sc_value" style="width:60px"></td>'+
  10553. '</tr>'+
  10554. '<tr>'+
  10555. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="0.09" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="0.09" id="graph_BH_damp_value" style="width:60px"></td>'+
  10556. '</tr>'+
  10557. '</table>'+
  10558. '<table id="graph_R_table" style="display:none">'+
  10559. '<tr><td><b>Repulsion</b></td></tr>'+
  10560. '<tr>'+
  10561. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="100" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="100" id="graph_R_nd_value" style="width:60px"></td>'+
  10562. '</tr>'+
  10563. '<tr>'+
  10564. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="0.1" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="0.01" id="graph_R_cg_value" style="width:60px"></td>'+
  10565. '</tr>'+
  10566. '<tr>'+
  10567. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="200" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="200" id="graph_R_sl_value" style="width:60px"></td>'+
  10568. '</tr>'+
  10569. '<tr>'+
  10570. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="0.05" step="0.005" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="0.05" id="graph_R_sc_value" style="width:60px"></td>'+
  10571. '</tr>'+
  10572. '<tr>'+
  10573. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="0.09" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="0.09" id="graph_R_damp_value" style="width:60px"></td>'+
  10574. '</tr>'+
  10575. '</table>'+
  10576. '<table id="graph_H_table" style="display:none">'+
  10577. '<tr><td width="150"><b>Hierarchical</b></td></tr>'+
  10578. '<tr>'+
  10579. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="60" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="60" id="graph_H_nd_value" style="width:60px"></td>'+
  10580. '</tr>'+
  10581. '<tr>'+
  10582. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="0" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="0" id="graph_H_cg_value" style="width:60px"></td>'+
  10583. '</tr>'+
  10584. '<tr>'+
  10585. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="100" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="100" id="graph_H_sl_value" style="width:60px"></td>'+
  10586. '</tr>'+
  10587. '<tr>'+
  10588. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="0.01" step="0.005" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="0.01" id="graph_H_sc_value" style="width:60px"></td>'+
  10589. '</tr>'+
  10590. '<tr>'+
  10591. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="0.09" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="0.09" id="graph_H_damp_value" style="width:60px"></td>'+
  10592. '</tr>'+
  10593. '<tr>'+
  10594. '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="0" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="LR" id="graph_H_direction_value" style="width:60px"></td>'+
  10595. '</tr>'+
  10596. '<tr>'+
  10597. '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="150" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="150" id="graph_H_levsep_value" style="width:60px"></td>'+
  10598. '</tr>'+
  10599. '<tr>'+
  10600. '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="100" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="100" id="graph_H_nspac_value" style="width:60px"></td>'+
  10601. '</tr>'+
  10602. '</table>'
  10603. this.containerElement.parentElement.insertBefore(this.physicsConfiguration,this.containerElement);
  10604. var hierarchicalLayoutDirections = ["LR","RL","UD","DU"];
  10605. var rangeElement;
  10606. rangeElement = document.getElementById('graph_BH_gc');
  10607. rangeElement.innerHTML = this.constants.physics.barnesHut.gravitationalConstant;
  10608. rangeElement.onchange = showValueOfRange.bind(this,'graph_BH_gc',-1,"physics_barnesHut_gravitationalConstant");
  10609. rangeElement = document.getElementById('graph_BH_cg');
  10610. rangeElement.innerHTML = this.constants.physics.barnesHut.centralGravity;
  10611. rangeElement.onchange = showValueOfRange.bind(this,'graph_BH_cg',1,"physics_centralGravity");
  10612. rangeElement = document.getElementById('graph_BH_sc');
  10613. rangeElement.innerHTML = this.constants.physics.barnesHut.springConstant;
  10614. rangeElement.onchange = showValueOfRange.bind(this,'graph_BH_sc',1,"physics_springConstant");
  10615. rangeElement = document.getElementById('graph_BH_sl');
  10616. rangeElement.innerHTML = this.constants.physics.barnesHut.springLength;
  10617. rangeElement.onchange = showValueOfRange.bind(this,'graph_BH_sl',1,"physics_springLength");
  10618. rangeElement = document.getElementById('graph_BH_damp');
  10619. rangeElement.innerHTML = this.constants.physics.barnesHut.damping;
  10620. rangeElement.onchange = showValueOfRange.bind(this,'graph_BH_damp',1,"physics_damping");
  10621. rangeElement = document.getElementById('graph_R_nd');
  10622. rangeElement.innerHTML = this.constants.physics.repulsion.nodeDistance;
  10623. rangeElement.onchange = showValueOfRange.bind(this,'graph_R_nd',1,"physics_repulsion_nodeDistance");
  10624. rangeElement = document.getElementById('graph_R_cg');
  10625. rangeElement.innerHTML = this.constants.physics.repulsion.centralGravity;
  10626. rangeElement.onchange = showValueOfRange.bind(this,'graph_R_cg',1,"physics_centralGravity");
  10627. rangeElement = document.getElementById('graph_R_sc');
  10628. rangeElement.innerHTML = this.constants.physics.repulsion.springConstant;
  10629. rangeElement.onchange = showValueOfRange.bind(this,'graph_R_sc',1,"physics_springConstant");
  10630. rangeElement = document.getElementById('graph_R_sl');
  10631. rangeElement.innerHTML = this.constants.physics.repulsion.springLength;
  10632. rangeElement.onchange = showValueOfRange.bind(this,'graph_R_sl',1,"physics_springLength");
  10633. rangeElement = document.getElementById('graph_R_damp');
  10634. rangeElement.innerHTML = this.constants.physics.repulsion.damping;
  10635. rangeElement.onchange = showValueOfRange.bind(this,'graph_R_damp',1,"physics_damping");
  10636. rangeElement = document.getElementById('graph_H_nd');
  10637. rangeElement.innerHTML = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10638. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_nd',1,"physics_hierarchicalRepulsion_nodeDistance");
  10639. rangeElement = document.getElementById('graph_H_cg');
  10640. rangeElement.innerHTML = this.constants.physics.hierarchicalRepulsion.centralGravity;
  10641. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_cg',1,"physics_centralGravity");
  10642. rangeElement = document.getElementById('graph_H_sc');
  10643. rangeElement.innerHTML = this.constants.physics.hierarchicalRepulsion.springConstant;
  10644. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_sc',1,"physics_springConstant");
  10645. rangeElement = document.getElementById('graph_H_sl');
  10646. rangeElement.innerHTML = this.constants.physics.hierarchicalRepulsion.springLength;
  10647. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_sl',1,"physics_springLength");
  10648. rangeElement = document.getElementById('graph_H_damp');
  10649. rangeElement.innerHTML = this.constants.physics.hierarchicalRepulsion.damping;
  10650. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_damp',1,"physics_damping");
  10651. rangeElement = document.getElementById('graph_H_direction');
  10652. rangeElement.innerHTML = hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction);
  10653. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_direction',hierarchicalLayoutDirections,"hierarchicalLayout_direction");
  10654. rangeElement = document.getElementById('graph_H_levsep');
  10655. rangeElement.innerHTML = this.constants.hierarchicalLayout.levelSeparation;
  10656. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_levsep',1,"hierarchicalLayout_levelSeparation");
  10657. rangeElement = document.getElementById('graph_H_nspac');
  10658. rangeElement.innerHTML = this.constants.hierarchicalLayout.nodeSpacing;
  10659. rangeElement.onchange = showValueOfRange.bind(this,'graph_H_nspac',1,"hierarchicalLayout_nodeSpacing");
  10660. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10661. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10662. var radioButton3 = document.getElementById("graph_physicsMethod3");
  10663. radioButton2.checked = true;
  10664. if (this.constants.physics.barnesHut.enabled) {
  10665. radioButton1.checked = true;
  10666. }
  10667. if (this.constants.hierarchicalLayout.enabled) {
  10668. radioButton3.checked = true;
  10669. }
  10670. switchConfigurations.apply(this);
  10671. radioButton1.onchange = switchConfigurations.bind(this);
  10672. radioButton2.onchange = switchConfigurations.bind(this);
  10673. radioButton3.onchange = switchConfigurations.bind(this);
  10674. }
  10675. },
  10676. _overWriteGraphConstants : function(constantsVariableName, value) {
  10677. var nameArray = constantsVariableName.split("_");
  10678. if (nameArray.length == 1) {
  10679. this.constants[nameArray[0]] = value;
  10680. }
  10681. else if (nameArray.length == 2) {
  10682. this.constants[nameArray[0]][nameArray[1]] = value;
  10683. }
  10684. else if (nameArray.length == 3) {
  10685. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  10686. }
  10687. }
  10688. }
  10689. function switchConfigurations () {
  10690. var ids = ["graph_BH_table","graph_R_table","graph_H_table"]
  10691. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  10692. var tableId = "graph_" + radioButton + "_table";
  10693. var table = document.getElementById(tableId);
  10694. table.style.display = "block";
  10695. for (var i = 0; i < ids.length; i++) {
  10696. if (ids[i] != tableId) {
  10697. table = document.getElementById(ids[i]);
  10698. table.style.display = "none";
  10699. }
  10700. }
  10701. this._restoreNodes();
  10702. if (radioButton == "R") {
  10703. this.constants.hierarchicalLayout.enabled = false;
  10704. this.constants.physics.hierarchicalRepulsion.enabeled = false;
  10705. this.constants.physics.barnesHut.enabled = false;
  10706. }
  10707. else if (radioButton == "H") {
  10708. this.constants.hierarchicalLayout.enabled = true;
  10709. this.constants.physics.hierarchicalRepulsion.enabeled = true;
  10710. this.constants.physics.barnesHut.enabled = false;
  10711. this._setupHierarchicalLayout();
  10712. }
  10713. else {
  10714. this.constants.hierarchicalLayout.enabled = false;
  10715. this.constants.physics.hierarchicalRepulsion.enabeled = false;
  10716. this.constants.physics.barnesHut.enabled = true;
  10717. }
  10718. this._loadSelectedForceSolver();
  10719. this.moving = true;
  10720. this.start();
  10721. }
  10722. function showValueOfRange (id,map,constantsVariableName) {
  10723. var valueId = id + "_value";
  10724. var rangeValue = document.getElementById(id).value;
  10725. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10726. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10727. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10728. this._setupHierarchicalLayout();
  10729. }
  10730. if (map instanceof Array) {
  10731. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10732. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10733. }
  10734. else {
  10735. document.getElementById(valueId).value = map * parseFloat(rangeValue);
  10736. this._overWriteGraphConstants(constantsVariableName,map * parseFloat(rangeValue));
  10737. }
  10738. this.moving = true;
  10739. this.start();
  10740. };
  10741. /**
  10742. * Created by Alex on 2/10/14.
  10743. */
  10744. var hierarchalRepulsionMixin = {
  10745. /**
  10746. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10747. * This field is linearly approximated.
  10748. *
  10749. * @private
  10750. */
  10751. _calculateNodeForces : function() {
  10752. var dx, dy, distance, fx, fy, combinedClusterSize,
  10753. repulsingForce, node1, node2, i, j;
  10754. var nodes = this.calculationNodes;
  10755. var nodeIndices = this.calculationNodeIndices;
  10756. // approximation constants
  10757. var b = 5;
  10758. var a_base = 0.5*-b;
  10759. // repulsing forces between nodes
  10760. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10761. var minimumDistance = nodeDistance;
  10762. // we loop from i over all but the last entree in the array
  10763. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10764. for (i = 0; i < nodeIndices.length-1; i++) {
  10765. node1 = nodes[nodeIndices[i]];
  10766. for (j = i+1; j < nodeIndices.length; j++) {
  10767. node2 = nodes[nodeIndices[j]];
  10768. dx = node2.x - node1.x;
  10769. dy = node2.y - node1.y;
  10770. distance = Math.sqrt(dx * dx + dy * dy);
  10771. var a = a_base / minimumDistance;
  10772. if (distance < 2*minimumDistance) {
  10773. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10774. // normalize force with
  10775. if (distance == 0) {
  10776. distance = 0.01;
  10777. }
  10778. else {
  10779. repulsingForce = repulsingForce/distance;
  10780. }
  10781. fx = dx * repulsingForce;
  10782. fy = dy * repulsingForce;
  10783. node1.fx -= fx;
  10784. node1.fy -= fy;
  10785. node2.fx += fx;
  10786. node2.fy += fy;
  10787. }
  10788. }
  10789. }
  10790. }
  10791. }
  10792. /**
  10793. * Created by Alex on 2/10/14.
  10794. */
  10795. var barnesHutMixin = {
  10796. /**
  10797. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10798. * The Barnes Hut method is used to speed up this N-body simulation.
  10799. *
  10800. * @private
  10801. */
  10802. _calculateNodeForces : function() {
  10803. var node;
  10804. var nodes = this.calculationNodes;
  10805. var nodeIndices = this.calculationNodeIndices;
  10806. var nodeCount = nodeIndices.length;
  10807. this._formBarnesHutTree(nodes,nodeIndices);
  10808. var barnesHutTree = this.barnesHutTree;
  10809. // place the nodes one by one recursively
  10810. for (var i = 0; i < nodeCount; i++) {
  10811. node = nodes[nodeIndices[i]];
  10812. // starting with root is irrelevant, it never passes the BarnesHut condition
  10813. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10814. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10815. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10816. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10817. }
  10818. },
  10819. /**
  10820. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10821. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10822. *
  10823. * @param parentBranch
  10824. * @param node
  10825. * @private
  10826. */
  10827. _getForceContribution : function(parentBranch,node) {
  10828. // we get no force contribution from an empty region
  10829. if (parentBranch.childrenCount > 0) {
  10830. var dx,dy,distance;
  10831. // get the distance from the center of mass to the node.
  10832. dx = parentBranch.centerOfMass.x - node.x;
  10833. dy = parentBranch.centerOfMass.y - node.y;
  10834. distance = Math.sqrt(dx * dx + dy * dy);
  10835. // BarnesHut condition
  10836. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10837. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10838. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10839. // duplicate code to reduce function calls to speed up program
  10840. if (distance == 0) {
  10841. distance = 0.1*Math.random();
  10842. dx = distance;
  10843. }
  10844. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10845. var fx = dx * gravityForce;
  10846. var fy = dy * gravityForce;
  10847. node.fx += fx;
  10848. node.fy += fy;
  10849. }
  10850. else {
  10851. // Did not pass the condition, go into children if available
  10852. if (parentBranch.childrenCount == 4) {
  10853. this._getForceContribution(parentBranch.children.NW,node);
  10854. this._getForceContribution(parentBranch.children.NE,node);
  10855. this._getForceContribution(parentBranch.children.SW,node);
  10856. this._getForceContribution(parentBranch.children.SE,node);
  10857. }
  10858. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10859. if (parentBranch.children.data.id != node.id) { // if it is not self
  10860. // duplicate code to reduce function calls to speed up program
  10861. if (distance == 0) {
  10862. distance = 0.5*Math.random();
  10863. dx = distance;
  10864. }
  10865. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10866. var fx = dx * gravityForce;
  10867. var fy = dy * gravityForce;
  10868. node.fx += fx;
  10869. node.fy += fy;
  10870. }
  10871. }
  10872. }
  10873. }
  10874. },
  10875. /**
  10876. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10877. *
  10878. * @param nodes
  10879. * @param nodeIndices
  10880. * @private
  10881. */
  10882. _formBarnesHutTree : function(nodes,nodeIndices) {
  10883. var node;
  10884. var nodeCount = nodeIndices.length;
  10885. var minX = Number.MAX_VALUE,
  10886. minY = Number.MAX_VALUE,
  10887. maxX =-Number.MAX_VALUE,
  10888. maxY =-Number.MAX_VALUE;
  10889. // get the range of the nodes
  10890. for (var i = 0; i < nodeCount; i++) {
  10891. var x = nodes[nodeIndices[i]].x;
  10892. var y = nodes[nodeIndices[i]].y;
  10893. if (x < minX) { minX = x; }
  10894. if (x > maxX) { maxX = x; }
  10895. if (y < minY) { minY = y; }
  10896. if (y > maxY) { maxY = y; }
  10897. }
  10898. // make the range a square
  10899. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10900. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10901. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10902. var minimumTreeSize = 1e-5;
  10903. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10904. var halfRootSize = 0.5 * rootSize;
  10905. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10906. // construct the barnesHutTree
  10907. var barnesHutTree = {root:{
  10908. centerOfMass:{x:0,y:0}, // Center of Mass
  10909. mass:0,
  10910. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10911. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10912. size: rootSize,
  10913. calcSize: 1 / rootSize,
  10914. children: {data:null},
  10915. maxWidth: 0,
  10916. level: 0,
  10917. childrenCount: 4
  10918. }};
  10919. this._splitBranch(barnesHutTree.root);
  10920. // place the nodes one by one recursively
  10921. for (i = 0; i < nodeCount; i++) {
  10922. node = nodes[nodeIndices[i]];
  10923. this._placeInTree(barnesHutTree.root,node);
  10924. }
  10925. // make global
  10926. this.barnesHutTree = barnesHutTree
  10927. },
  10928. _updateBranchMass : function(parentBranch, node) {
  10929. var totalMass = parentBranch.mass + node.mass;
  10930. var totalMassInv = 1/totalMass;
  10931. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10932. parentBranch.centerOfMass.x *= totalMassInv;
  10933. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10934. parentBranch.centerOfMass.y *= totalMassInv;
  10935. parentBranch.mass = totalMass;
  10936. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10937. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10938. },
  10939. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10940. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10941. // update the mass of the branch.
  10942. this._updateBranchMass(parentBranch,node);
  10943. }
  10944. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10945. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10946. this._placeInRegion(parentBranch,node,"NW");
  10947. }
  10948. else { // in SW
  10949. this._placeInRegion(parentBranch,node,"SW");
  10950. }
  10951. }
  10952. else { // in NE or SE
  10953. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10954. this._placeInRegion(parentBranch,node,"NE");
  10955. }
  10956. else { // in SE
  10957. this._placeInRegion(parentBranch,node,"SE");
  10958. }
  10959. }
  10960. },
  10961. _placeInRegion : function(parentBranch,node,region) {
  10962. switch (parentBranch.children[region].childrenCount) {
  10963. case 0: // place node here
  10964. parentBranch.children[region].children.data = node;
  10965. parentBranch.children[region].childrenCount = 1;
  10966. this._updateBranchMass(parentBranch.children[region],node);
  10967. break;
  10968. case 1: // convert into children
  10969. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10970. // we move one node a pixel and we do not put it in the tree.
  10971. if (parentBranch.children[region].children.data.x == node.x &&
  10972. parentBranch.children[region].children.data.y == node.y) {
  10973. node.x += Math.random();
  10974. node.y += Math.random();
  10975. this._placeInTree(parentBranch,node, true);
  10976. }
  10977. else {
  10978. this._splitBranch(parentBranch.children[region]);
  10979. this._placeInTree(parentBranch.children[region],node);
  10980. }
  10981. break;
  10982. case 4: // place in branch
  10983. this._placeInTree(parentBranch.children[region],node);
  10984. break;
  10985. }
  10986. },
  10987. /**
  10988. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10989. * after the split is complete.
  10990. *
  10991. * @param parentBranch
  10992. * @private
  10993. */
  10994. _splitBranch : function(parentBranch) {
  10995. // if the branch is filled with a node, replace the node in the new subset.
  10996. var containedNode = null;
  10997. if (parentBranch.childrenCount == 1) {
  10998. containedNode = parentBranch.children.data;
  10999. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  11000. }
  11001. parentBranch.childrenCount = 4;
  11002. parentBranch.children.data = null;
  11003. this._insertRegion(parentBranch,"NW");
  11004. this._insertRegion(parentBranch,"NE");
  11005. this._insertRegion(parentBranch,"SW");
  11006. this._insertRegion(parentBranch,"SE");
  11007. if (containedNode != null) {
  11008. this._placeInTree(parentBranch,containedNode);
  11009. }
  11010. },
  11011. /**
  11012. * This function subdivides the region into four new segments.
  11013. * Specifically, this inserts a single new segment.
  11014. * It fills the children section of the parentBranch
  11015. *
  11016. * @param parentBranch
  11017. * @param region
  11018. * @param parentRange
  11019. * @private
  11020. */
  11021. _insertRegion : function(parentBranch, region) {
  11022. var minX,maxX,minY,maxY;
  11023. var childSize = 0.5 * parentBranch.size;
  11024. switch (region) {
  11025. case "NW":
  11026. minX = parentBranch.range.minX;
  11027. maxX = parentBranch.range.minX + childSize;
  11028. minY = parentBranch.range.minY;
  11029. maxY = parentBranch.range.minY + childSize;
  11030. break;
  11031. case "NE":
  11032. minX = parentBranch.range.minX + childSize;
  11033. maxX = parentBranch.range.maxX;
  11034. minY = parentBranch.range.minY;
  11035. maxY = parentBranch.range.minY + childSize;
  11036. break;
  11037. case "SW":
  11038. minX = parentBranch.range.minX;
  11039. maxX = parentBranch.range.minX + childSize;
  11040. minY = parentBranch.range.minY + childSize;
  11041. maxY = parentBranch.range.maxY;
  11042. break;
  11043. case "SE":
  11044. minX = parentBranch.range.minX + childSize;
  11045. maxX = parentBranch.range.maxX;
  11046. minY = parentBranch.range.minY + childSize;
  11047. maxY = parentBranch.range.maxY;
  11048. break;
  11049. }
  11050. parentBranch.children[region] = {
  11051. centerOfMass:{x:0,y:0},
  11052. mass:0,
  11053. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  11054. size: 0.5 * parentBranch.size,
  11055. calcSize: 2 * parentBranch.calcSize,
  11056. children: {data:null},
  11057. maxWidth: 0,
  11058. level: parentBranch.level+1,
  11059. childrenCount: 0
  11060. };
  11061. },
  11062. /**
  11063. * This function is for debugging purposed, it draws the tree.
  11064. *
  11065. * @param ctx
  11066. * @param color
  11067. * @private
  11068. */
  11069. _drawTree : function(ctx,color) {
  11070. if (this.barnesHutTree !== undefined) {
  11071. ctx.lineWidth = 1;
  11072. this._drawBranch(this.barnesHutTree.root,ctx,color);
  11073. }
  11074. },
  11075. /**
  11076. * This function is for debugging purposes. It draws the branches recursively.
  11077. *
  11078. * @param branch
  11079. * @param ctx
  11080. * @param color
  11081. * @private
  11082. */
  11083. _drawBranch : function(branch,ctx,color) {
  11084. if (color === undefined) {
  11085. color = "#FF0000";
  11086. }
  11087. if (branch.childrenCount == 4) {
  11088. this._drawBranch(branch.children.NW,ctx);
  11089. this._drawBranch(branch.children.NE,ctx);
  11090. this._drawBranch(branch.children.SE,ctx);
  11091. this._drawBranch(branch.children.SW,ctx);
  11092. }
  11093. ctx.strokeStyle = color;
  11094. ctx.beginPath();
  11095. ctx.moveTo(branch.range.minX,branch.range.minY);
  11096. ctx.lineTo(branch.range.maxX,branch.range.minY);
  11097. ctx.stroke();
  11098. ctx.beginPath();
  11099. ctx.moveTo(branch.range.maxX,branch.range.minY);
  11100. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  11101. ctx.stroke();
  11102. ctx.beginPath();
  11103. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  11104. ctx.lineTo(branch.range.minX,branch.range.maxY);
  11105. ctx.stroke();
  11106. ctx.beginPath();
  11107. ctx.moveTo(branch.range.minX,branch.range.maxY);
  11108. ctx.lineTo(branch.range.minX,branch.range.minY);
  11109. ctx.stroke();
  11110. /*
  11111. if (branch.mass > 0) {
  11112. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  11113. ctx.stroke();
  11114. }
  11115. */
  11116. }
  11117. };
  11118. /**
  11119. * Created by Alex on 2/10/14.
  11120. */
  11121. var repulsionMixin = {
  11122. /**
  11123. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  11124. * This field is linearly approximated.
  11125. *
  11126. * @private
  11127. */
  11128. _calculateNodeForces : function() {
  11129. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  11130. repulsingForce, node1, node2, i, j;
  11131. var nodes = this.calculationNodes;
  11132. var nodeIndices = this.calculationNodeIndices;
  11133. // approximation constants
  11134. var a_base = -2/3;
  11135. var b = 4/3;
  11136. // repulsing forces between nodes
  11137. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  11138. var minimumDistance = nodeDistance;
  11139. // we loop from i over all but the last entree in the array
  11140. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  11141. for (i = 0; i < nodeIndices.length-1; i++) {
  11142. node1 = nodes[nodeIndices[i]];
  11143. for (j = i+1; j < nodeIndices.length; j++) {
  11144. node2 = nodes[nodeIndices[j]];
  11145. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  11146. dx = node2.x - node1.x;
  11147. dy = node2.y - node1.y;
  11148. distance = Math.sqrt(dx * dx + dy * dy);
  11149. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  11150. var a = a_base / minimumDistance;
  11151. if (distance < 2*minimumDistance) {
  11152. if (distance < 0.5*minimumDistance) {
  11153. repulsingForce = 1.0;
  11154. }
  11155. else {
  11156. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  11157. }
  11158. // amplify the repulsion for clusters.
  11159. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  11160. repulsingForce = repulsingForce/distance;
  11161. fx = dx * repulsingForce;
  11162. fy = dy * repulsingForce;
  11163. node1.fx -= fx;
  11164. node1.fy -= fy;
  11165. node2.fx += fx;
  11166. node2.fy += fy;
  11167. }
  11168. }
  11169. }
  11170. }
  11171. }
  11172. var HierarchicalLayoutMixin = {
  11173. /**
  11174. * This is the main function to layout the nodes in a hierarchical way.
  11175. * It checks if the node details are supplied correctly
  11176. *
  11177. * @private
  11178. */
  11179. _setupHierarchicalLayout : function() {
  11180. if (this.constants.hierarchicalLayout.enabled == true) {
  11181. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  11182. this.constants.hierarchicalLayout.levelSeparation *= -1;
  11183. }
  11184. // get the size of the largest hubs and check if the user has defined a level for a node.
  11185. var hubsize = 0;
  11186. var node, nodeId;
  11187. var definedLevel = false;
  11188. var undefinedLevel = false;
  11189. for (nodeId in this.nodes) {
  11190. if (this.nodes.hasOwnProperty(nodeId)) {
  11191. node = this.nodes[nodeId];
  11192. if (node.level != -1) {
  11193. definedLevel = true;
  11194. }
  11195. else {
  11196. undefinedLevel = true;
  11197. }
  11198. if (hubsize < node.edges.length) {
  11199. hubsize = node.edges.length;
  11200. }
  11201. }
  11202. }
  11203. // if the user defined some levels but not all, alert and run without hierarchical layout
  11204. if (undefinedLevel == true && definedLevel == true) {
  11205. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.")
  11206. this.zoomExtent(true,this.constants.clustering.enabled);
  11207. if (!this.constants.clustering.enabled) {
  11208. this.start();
  11209. }
  11210. }
  11211. else {
  11212. // setup the system to use hierarchical method.
  11213. this._changeConstants();
  11214. // define levels if undefined by the users. Based on hubsize
  11215. if (undefinedLevel == true) {
  11216. this._determineLevels(hubsize);
  11217. }
  11218. // check the distribution of the nodes per level.
  11219. var distribution = this._getDistribution();
  11220. // place the nodes on the canvas. This also stablilizes the system.
  11221. this._placeNodesByHierarchy(distribution);
  11222. // start the simulation.
  11223. this.start();
  11224. }
  11225. }
  11226. },
  11227. /**
  11228. * This function places the nodes on the canvas based on the hierarchial distribution.
  11229. *
  11230. * @param {Object} distribution | obtained by the function this._getDistribution()
  11231. * @private
  11232. */
  11233. _placeNodesByHierarchy : function(distribution) {
  11234. var nodeId, node;
  11235. // start placing all the level 0 nodes first. Then recursively position their branches.
  11236. for (nodeId in distribution[0].nodes) {
  11237. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  11238. node = distribution[0].nodes[nodeId];
  11239. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11240. if (node.xFixed) {
  11241. node.x = distribution[0].minPos;
  11242. node.xFixed = false;
  11243. distribution[0].minPos += distribution[0].nodeSpacing;
  11244. }
  11245. }
  11246. else {
  11247. if (node.yFixed) {
  11248. node.y = distribution[0].minPos;
  11249. node.yFixed = false;
  11250. distribution[0].minPos += distribution[0].nodeSpacing;
  11251. }
  11252. }
  11253. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  11254. }
  11255. }
  11256. // stabilize the system after positioning. This function calls zoomExtent.
  11257. this._doStabilize();
  11258. },
  11259. /**
  11260. * This function get the distribution of levels based on hubsize
  11261. *
  11262. * @returns {Object}
  11263. * @private
  11264. */
  11265. _getDistribution : function() {
  11266. var distribution = {};
  11267. var nodeId, node;
  11268. // 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.
  11269. // the fix of X is removed after the x value has been set.
  11270. for (nodeId in this.nodes) {
  11271. if (this.nodes.hasOwnProperty(nodeId)) {
  11272. node = this.nodes[nodeId];
  11273. node.xFixed = true;
  11274. node.yFixed = true;
  11275. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11276. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11277. }
  11278. else {
  11279. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11280. }
  11281. if (!distribution.hasOwnProperty(node.level)) {
  11282. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  11283. }
  11284. distribution[node.level].amount += 1;
  11285. distribution[node.level].nodes[node.id] = node;
  11286. }
  11287. }
  11288. // determine the largest amount of nodes of all levels
  11289. var maxCount = 0;
  11290. for (var level in distribution) {
  11291. if (distribution.hasOwnProperty(level)) {
  11292. if (maxCount < distribution[level].amount) {
  11293. maxCount = distribution[level].amount;
  11294. }
  11295. }
  11296. }
  11297. // set the initial position and spacing of each nodes accordingly
  11298. for (var level in distribution) {
  11299. if (distribution.hasOwnProperty(level)) {
  11300. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  11301. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  11302. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  11303. }
  11304. }
  11305. return distribution;
  11306. },
  11307. /**
  11308. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  11309. *
  11310. * @param hubsize
  11311. * @private
  11312. */
  11313. _determineLevels : function(hubsize) {
  11314. var nodeId, node;
  11315. // determine hubs
  11316. for (nodeId in this.nodes) {
  11317. if (this.nodes.hasOwnProperty(nodeId)) {
  11318. node = this.nodes[nodeId];
  11319. if (node.edges.length == hubsize) {
  11320. node.level = 0;
  11321. }
  11322. }
  11323. }
  11324. // branch from hubs
  11325. for (nodeId in this.nodes) {
  11326. if (this.nodes.hasOwnProperty(nodeId)) {
  11327. node = this.nodes[nodeId];
  11328. if (node.level == 0) {
  11329. this._setLevel(1,node.edges,node.id);
  11330. }
  11331. }
  11332. }
  11333. },
  11334. /**
  11335. * Since hierarchical layout does not support:
  11336. * - smooth curves (based on the physics),
  11337. * - clustering (based on dynamic node counts)
  11338. *
  11339. * We disable both features so there will be no problems.
  11340. *
  11341. * @private
  11342. */
  11343. _changeConstants : function() {
  11344. this.constants.clustering.enabled = false;
  11345. this.constants.physics.barnesHut.enabled = false;
  11346. this.constants.physics.hierarchicalRepulsion.enabled = true;
  11347. this._loadSelectedForceSolver();
  11348. this.constants.smoothCurves = false;
  11349. this._configureSmoothCurves();
  11350. },
  11351. /**
  11352. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  11353. * on a X position that ensures there will be no overlap.
  11354. *
  11355. * @param edges
  11356. * @param parentId
  11357. * @param distribution
  11358. * @param parentLevel
  11359. * @private
  11360. */
  11361. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  11362. for (var i = 0; i < edges.length; i++) {
  11363. var childNode = null;
  11364. if (edges[i].toId == parentId) {
  11365. childNode = edges[i].from;
  11366. }
  11367. else {
  11368. childNode = edges[i].to;
  11369. }
  11370. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  11371. var nodeMoved = false;
  11372. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11373. if (childNode.xFixed && childNode.level > parentLevel) {
  11374. childNode.xFixed = false;
  11375. childNode.x = distribution[childNode.level].minPos;
  11376. nodeMoved = true;
  11377. }
  11378. }
  11379. else {
  11380. if (childNode.yFixed && childNode.level > parentLevel) {
  11381. childNode.yFixed = false;
  11382. childNode.y = distribution[childNode.level].minPos;
  11383. nodeMoved = true;
  11384. }
  11385. }
  11386. if (nodeMoved == true) {
  11387. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  11388. if (childNode.edges.length > 1) {
  11389. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  11390. }
  11391. }
  11392. }
  11393. },
  11394. /**
  11395. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  11396. *
  11397. * @param level
  11398. * @param edges
  11399. * @param parentId
  11400. * @private
  11401. */
  11402. _setLevel : function(level, edges, parentId) {
  11403. for (var i = 0; i < edges.length; i++) {
  11404. var childNode = null;
  11405. if (edges[i].toId == parentId) {
  11406. childNode = edges[i].from;
  11407. }
  11408. else {
  11409. childNode = edges[i].to;
  11410. }
  11411. if (childNode.level == -1 || childNode.level > level) {
  11412. childNode.level = level;
  11413. if (edges.length > 1) {
  11414. this._setLevel(level+1, childNode.edges, childNode.id);
  11415. }
  11416. }
  11417. }
  11418. },
  11419. /**
  11420. * Unfix nodes
  11421. *
  11422. * @private
  11423. */
  11424. _restoreNodes : function() {
  11425. for (nodeId in this.nodes) {
  11426. if (this.nodes.hasOwnProperty(nodeId)) {
  11427. this.nodes[nodeId].xFixed = false;
  11428. this.nodes[nodeId].yFixed = false;
  11429. }
  11430. }
  11431. }
  11432. };
  11433. /**
  11434. * Created by Alex on 2/4/14.
  11435. */
  11436. var manipulationMixin = {
  11437. /**
  11438. * clears the toolbar div element of children
  11439. *
  11440. * @private
  11441. */
  11442. _clearManipulatorBar : function() {
  11443. while (this.manipulationDiv.hasChildNodes()) {
  11444. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11445. }
  11446. },
  11447. /**
  11448. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  11449. * these functions to their original functionality, we saved them in this.cachedFunctions.
  11450. * This function restores these functions to their original function.
  11451. *
  11452. * @private
  11453. */
  11454. _restoreOverloadedFunctions : function() {
  11455. for (var functionName in this.cachedFunctions) {
  11456. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  11457. this[functionName] = this.cachedFunctions[functionName];
  11458. }
  11459. }
  11460. },
  11461. /**
  11462. * Enable or disable edit-mode.
  11463. *
  11464. * @private
  11465. */
  11466. _toggleEditMode : function() {
  11467. this.editMode = !this.editMode;
  11468. var toolbar = document.getElementById("graph-manipulationDiv");
  11469. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11470. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  11471. if (this.editMode == true) {
  11472. toolbar.style.display="block";
  11473. closeDiv.style.display="block";
  11474. editModeDiv.style.display="none";
  11475. closeDiv.onclick = this._toggleEditMode.bind(this);
  11476. }
  11477. else {
  11478. toolbar.style.display="none";
  11479. closeDiv.style.display="none";
  11480. editModeDiv.style.display="block";
  11481. closeDiv.onclick = null;
  11482. }
  11483. this._createManipulatorBar()
  11484. },
  11485. /**
  11486. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  11487. *
  11488. * @private
  11489. */
  11490. _createManipulatorBar : function() {
  11491. // remove bound functions
  11492. this.off('select', this.boundFunction);
  11493. // restore overloaded functions
  11494. this._restoreOverloadedFunctions();
  11495. // resume calculation
  11496. this.freezeSimulation = false;
  11497. // reset global variables
  11498. this.blockConnectingEdgeSelection = false;
  11499. this.forceAppendSelection = false
  11500. if (this.editMode == true) {
  11501. while (this.manipulationDiv.hasChildNodes()) {
  11502. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11503. }
  11504. // add the icons to the manipulator div
  11505. this.manipulationDiv.innerHTML = "" +
  11506. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  11507. "<span class='graph-manipulationLabel'>Add Node</span></span>" +
  11508. "<div class='graph-seperatorLine'></div>" +
  11509. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  11510. "<span class='graph-manipulationLabel'>Add Link</span></span>";
  11511. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11512. this.manipulationDiv.innerHTML += "" +
  11513. "<div class='graph-seperatorLine'></div>" +
  11514. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  11515. "<span class='graph-manipulationLabel'>Edit Node</span></span>";
  11516. }
  11517. if (this._selectionIsEmpty() == false) {
  11518. this.manipulationDiv.innerHTML += "" +
  11519. "<div class='graph-seperatorLine'></div>" +
  11520. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  11521. "<span class='graph-manipulationLabel'>Delete selected</span></span>";
  11522. }
  11523. // bind the icons
  11524. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  11525. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  11526. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  11527. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  11528. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11529. var editButton = document.getElementById("graph-manipulate-editNode");
  11530. editButton.onclick = this._editNode.bind(this);
  11531. }
  11532. if (this._selectionIsEmpty() == false) {
  11533. var deleteButton = document.getElementById("graph-manipulate-delete");
  11534. deleteButton.onclick = this._deleteSelected.bind(this);
  11535. }
  11536. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11537. closeDiv.onclick = this._toggleEditMode.bind(this);
  11538. this.boundFunction = this._createManipulatorBar.bind(this);
  11539. this.on('select', this.boundFunction);
  11540. }
  11541. else {
  11542. this.editModeDiv.innerHTML = "" +
  11543. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  11544. "<span class='graph-manipulationLabel'>Edit</span></span>"
  11545. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  11546. editModeButton.onclick = this._toggleEditMode.bind(this);
  11547. }
  11548. },
  11549. /**
  11550. * Create the toolbar for adding Nodes
  11551. *
  11552. * @private
  11553. */
  11554. _createAddNodeToolbar : function() {
  11555. // clear the toolbar
  11556. this._clearManipulatorBar();
  11557. this.off('select', this.boundFunction);
  11558. // create the toolbar contents
  11559. this.manipulationDiv.innerHTML = "" +
  11560. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11561. "<span class='graph-manipulationLabel'>Back</span></span>" +
  11562. "<div class='graph-seperatorLine'></div>" +
  11563. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11564. "<span class='graph-manipulationLabel'>Click in an empty space to place a new node</span></span>";
  11565. // bind the icon
  11566. var backButton = document.getElementById("graph-manipulate-back");
  11567. backButton.onclick = this._createManipulatorBar.bind(this);
  11568. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11569. this.boundFunction = this._addNode.bind(this);
  11570. this.on('select', this.boundFunction);
  11571. },
  11572. /**
  11573. * create the toolbar to connect nodes
  11574. *
  11575. * @private
  11576. */
  11577. _createAddEdgeToolbar : function() {
  11578. // clear the toolbar
  11579. this._clearManipulatorBar();
  11580. this._unselectAll(true);
  11581. this.freezeSimulation = true;
  11582. this.off('select', this.boundFunction);
  11583. this._unselectAll();
  11584. this.forceAppendSelection = false;
  11585. this.blockConnectingEdgeSelection = true;
  11586. this.manipulationDiv.innerHTML = "" +
  11587. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11588. "<span class='graph-manipulationLabel'>Back</span></span>" +
  11589. "<div class='graph-seperatorLine'></div>" +
  11590. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11591. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>Click on a node and drag the edge to another node to connect them.</span></span>";
  11592. // bind the icon
  11593. var backButton = document.getElementById("graph-manipulate-back");
  11594. backButton.onclick = this._createManipulatorBar.bind(this);
  11595. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11596. this.boundFunction = this._handleConnect.bind(this);
  11597. this.on('select', this.boundFunction);
  11598. // temporarily overload functions
  11599. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  11600. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  11601. this._handleTouch = this._handleConnect;
  11602. this._handleOnRelease = this._finishConnect;
  11603. // redraw to show the unselect
  11604. this._redraw();
  11605. },
  11606. /**
  11607. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  11608. * to walk the user through the process.
  11609. *
  11610. * @private
  11611. */
  11612. _handleConnect : function(pointer) {
  11613. if (this._getSelectedNodeCount() == 0) {
  11614. var node = this._getNodeAt(pointer);
  11615. if (node != null) {
  11616. if (node.clusterSize > 1) {
  11617. alert("Cannot create edges to a cluster.")
  11618. }
  11619. else {
  11620. this._selectObject(node,false);
  11621. // create a node the temporary line can look at
  11622. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  11623. this.sectors['support']['nodes']['targetNode'].x = node.x;
  11624. this.sectors['support']['nodes']['targetNode'].y = node.y;
  11625. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  11626. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  11627. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  11628. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  11629. // create a temporary edge
  11630. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  11631. this.edges['connectionEdge'].from = node;
  11632. this.edges['connectionEdge'].connected = true;
  11633. this.edges['connectionEdge'].smooth = true;
  11634. this.edges['connectionEdge'].selected = true;
  11635. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  11636. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  11637. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  11638. this._handleOnDrag = function(event) {
  11639. var pointer = this._getPointer(event.gesture.center);
  11640. this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
  11641. this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
  11642. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
  11643. this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
  11644. };
  11645. this.moving = true;
  11646. this.start();
  11647. }
  11648. }
  11649. }
  11650. },
  11651. _finishConnect : function(pointer) {
  11652. if (this._getSelectedNodeCount() == 1) {
  11653. // restore the drag function
  11654. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  11655. delete this.cachedFunctions["_handleOnDrag"];
  11656. // remember the edge id
  11657. var connectFromId = this.edges['connectionEdge'].fromId;
  11658. // remove the temporary nodes and edge
  11659. delete this.edges['connectionEdge']
  11660. delete this.sectors['support']['nodes']['targetNode'];
  11661. delete this.sectors['support']['nodes']['targetViaNode'];
  11662. var node = this._getNodeAt(pointer);
  11663. if (node != null) {
  11664. if (node.clusterSize > 1) {
  11665. alert("Cannot create edges to a cluster.")
  11666. }
  11667. else {
  11668. this._createEdge(connectFromId,node.id);
  11669. this._createManipulatorBar();
  11670. }
  11671. }
  11672. this._unselectAll();
  11673. }
  11674. },
  11675. /**
  11676. * Adds a node on the specified location
  11677. *
  11678. * @param {Object} pointer
  11679. */
  11680. _addNode : function() {
  11681. if (this._selectionIsEmpty() && this.editMode == true) {
  11682. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  11683. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMove:true};
  11684. if (this.triggerFunctions.add) {
  11685. if (this.triggerFunctions.add.length == 2) {
  11686. var me = this;
  11687. this.triggerFunctions.add(defaultData, function(finalizedData) {
  11688. me.createNodeOnClick = true;
  11689. me.nodesData.add(finalizedData);
  11690. me.createNodeOnClick = false;
  11691. me._createManipulatorBar();
  11692. me.moving = true;
  11693. me.start();
  11694. });
  11695. }
  11696. else {
  11697. alert("The function for add does not support two arguments (data,callback).");
  11698. this._createManipulatorBar();
  11699. this.moving = true;
  11700. this.start();
  11701. }
  11702. }
  11703. else {
  11704. this.createNodeOnClick = true;
  11705. this.nodesData.add(defaultData);
  11706. this.createNodeOnClick = false;
  11707. this._createManipulatorBar();
  11708. this.moving = true;
  11709. this.start();
  11710. }
  11711. }
  11712. },
  11713. /**
  11714. * connect two nodes with a new edge.
  11715. *
  11716. * @private
  11717. */
  11718. _createEdge : function(sourceNodeId,targetNodeId) {
  11719. if (this.editMode == true) {
  11720. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11721. if (this.triggerFunctions.connect) {
  11722. if (this.triggerFunctions.connect.length == 2) {
  11723. var me = this;
  11724. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11725. me.edgesData.add(finalizedData)
  11726. me.moving = true;
  11727. me.start();
  11728. });
  11729. }
  11730. else {
  11731. alert("The function for connect does not support two arguments (data,callback).");
  11732. this.moving = true;
  11733. this.start();
  11734. }
  11735. }
  11736. else {
  11737. this.edgesData.add(defaultData)
  11738. this.moving = true;
  11739. this.start();
  11740. }
  11741. }
  11742. },
  11743. /**
  11744. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11745. *
  11746. * @private
  11747. */
  11748. _editNode : function() {
  11749. if (this.triggerFunctions.edit && this.editMode == true) {
  11750. var node = this._getSelectedNode();
  11751. var data = {id:node.id,
  11752. label: node.label,
  11753. group: node.group,
  11754. shape: node.shape,
  11755. color: {
  11756. background:node.color.background,
  11757. border:node.color.border,
  11758. highlight: {
  11759. background:node.color.highlight.background,
  11760. border:node.color.highlight.border
  11761. }
  11762. }};
  11763. if (this.triggerFunctions.edit.length == 2) {
  11764. var me = this;
  11765. this.triggerFunctions.edit(data, function (finalizedData) {
  11766. me.nodesData.update(finalizedData);
  11767. me._createManipulatorBar();
  11768. me.moving = true;
  11769. me.start();
  11770. });
  11771. }
  11772. else {
  11773. alert("The function for edit does not support two arguments (data, callback).")
  11774. }
  11775. }
  11776. else {
  11777. alert("No edit function has been bound to this button.")
  11778. }
  11779. },
  11780. /**
  11781. * delete everything in the selection
  11782. *
  11783. * @private
  11784. */
  11785. _deleteSelected : function() {
  11786. if (!this._selectionIsEmpty() && this.editMode == true) {
  11787. if (!this._clusterInSelection()) {
  11788. var selectedNodes = this.getSelectedNodes();
  11789. var selectedEdges = this.getSelectedEdges();
  11790. if (this.triggerFunctions.delete) {
  11791. var me = this;
  11792. var data = {nodes: selectedNodes, edges: selectedEdges};
  11793. if (this.triggerFunctions.delete.length = 2) {
  11794. this.triggerFunctions.delete(data, function (finalizedData) {
  11795. me.edgesData.remove(finalizedData.edges);
  11796. me.nodesData.remove(finalizedData.nodes);
  11797. this._unselectAll();
  11798. me.moving = true;
  11799. me.start();
  11800. });
  11801. }
  11802. else {
  11803. alert("The function for edit does not support two arguments (data, callback).")
  11804. }
  11805. }
  11806. else {
  11807. this.edgesData.remove(selectedEdges);
  11808. this.nodesData.remove(selectedNodes);
  11809. this._unselectAll();
  11810. this.moving = true;
  11811. this.start();
  11812. }
  11813. }
  11814. else {
  11815. alert("Clusters cannot be deleted.");
  11816. }
  11817. }
  11818. }
  11819. };
  11820. /**
  11821. * Creation of the SectorMixin var.
  11822. *
  11823. * This contains all the functions the Graph object can use to employ the sector system.
  11824. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11825. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11826. *
  11827. * Alex de Mulder
  11828. * 21-01-2013
  11829. */
  11830. var SectorMixin = {
  11831. /**
  11832. * This function is only called by the setData function of the Graph object.
  11833. * This loads the global references into the active sector. This initializes the sector.
  11834. *
  11835. * @private
  11836. */
  11837. _putDataInSector : function() {
  11838. this.sectors["active"][this._sector()].nodes = this.nodes;
  11839. this.sectors["active"][this._sector()].edges = this.edges;
  11840. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11841. },
  11842. /**
  11843. * /**
  11844. * This function sets the global references to nodes, edges and nodeIndices back to
  11845. * those of the supplied (active) sector. If a type is defined, do the specific type
  11846. *
  11847. * @param {String} sectorId
  11848. * @param {String} [sectorType] | "active" or "frozen"
  11849. * @private
  11850. */
  11851. _switchToSector : function(sectorId, sectorType) {
  11852. if (sectorType === undefined || sectorType == "active") {
  11853. this._switchToActiveSector(sectorId);
  11854. }
  11855. else {
  11856. this._switchToFrozenSector(sectorId);
  11857. }
  11858. },
  11859. /**
  11860. * This function sets the global references to nodes, edges and nodeIndices back to
  11861. * those of the supplied active sector.
  11862. *
  11863. * @param sectorId
  11864. * @private
  11865. */
  11866. _switchToActiveSector : function(sectorId) {
  11867. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11868. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11869. this.edges = this.sectors["active"][sectorId]["edges"];
  11870. },
  11871. /**
  11872. * This function sets the global references to nodes, edges and nodeIndices back to
  11873. * those of the supplied active sector.
  11874. *
  11875. * @param sectorId
  11876. * @private
  11877. */
  11878. _switchToSupportSector : function() {
  11879. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11880. this.nodes = this.sectors["support"]["nodes"];
  11881. this.edges = this.sectors["support"]["edges"];
  11882. },
  11883. /**
  11884. * This function sets the global references to nodes, edges and nodeIndices back to
  11885. * those of the supplied frozen sector.
  11886. *
  11887. * @param sectorId
  11888. * @private
  11889. */
  11890. _switchToFrozenSector : function(sectorId) {
  11891. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11892. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11893. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11894. },
  11895. /**
  11896. * This function sets the global references to nodes, edges and nodeIndices back to
  11897. * those of the currently active sector.
  11898. *
  11899. * @private
  11900. */
  11901. _loadLatestSector : function() {
  11902. this._switchToSector(this._sector());
  11903. },
  11904. /**
  11905. * This function returns the currently active sector Id
  11906. *
  11907. * @returns {String}
  11908. * @private
  11909. */
  11910. _sector : function() {
  11911. return this.activeSector[this.activeSector.length-1];
  11912. },
  11913. /**
  11914. * This function returns the previously active sector Id
  11915. *
  11916. * @returns {String}
  11917. * @private
  11918. */
  11919. _previousSector : function() {
  11920. if (this.activeSector.length > 1) {
  11921. return this.activeSector[this.activeSector.length-2];
  11922. }
  11923. else {
  11924. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11925. }
  11926. },
  11927. /**
  11928. * We add the active sector at the end of the this.activeSector array
  11929. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11930. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11931. *
  11932. * @param newId
  11933. * @private
  11934. */
  11935. _setActiveSector : function(newId) {
  11936. this.activeSector.push(newId);
  11937. },
  11938. /**
  11939. * We remove the currently active sector id from the active sector stack. This happens when
  11940. * we reactivate the previously active sector
  11941. *
  11942. * @private
  11943. */
  11944. _forgetLastSector : function() {
  11945. this.activeSector.pop();
  11946. },
  11947. /**
  11948. * This function creates a new active sector with the supplied newId. This newId
  11949. * is the expanding node id.
  11950. *
  11951. * @param {String} newId | Id of the new active sector
  11952. * @private
  11953. */
  11954. _createNewSector : function(newId) {
  11955. // create the new sector
  11956. this.sectors["active"][newId] = {"nodes":{},
  11957. "edges":{},
  11958. "nodeIndices":[],
  11959. "formationScale": this.scale,
  11960. "drawingNode": undefined};
  11961. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11962. this.sectors["active"][newId]['drawingNode'] = new Node(
  11963. {id:newId,
  11964. color: {
  11965. background: "#eaefef",
  11966. border: "495c5e"
  11967. }
  11968. },{},{},this.constants);
  11969. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11970. },
  11971. /**
  11972. * This function removes the currently active sector. This is called when we create a new
  11973. * active sector.
  11974. *
  11975. * @param {String} sectorId | Id of the active sector that will be removed
  11976. * @private
  11977. */
  11978. _deleteActiveSector : function(sectorId) {
  11979. delete this.sectors["active"][sectorId];
  11980. },
  11981. /**
  11982. * This function removes the currently active sector. This is called when we reactivate
  11983. * the previously active sector.
  11984. *
  11985. * @param {String} sectorId | Id of the active sector that will be removed
  11986. * @private
  11987. */
  11988. _deleteFrozenSector : function(sectorId) {
  11989. delete this.sectors["frozen"][sectorId];
  11990. },
  11991. /**
  11992. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11993. * We copy the references, then delete the active entree.
  11994. *
  11995. * @param sectorId
  11996. * @private
  11997. */
  11998. _freezeSector : function(sectorId) {
  11999. // we move the set references from the active to the frozen stack.
  12000. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  12001. // we have moved the sector data into the frozen set, we now remove it from the active set
  12002. this._deleteActiveSector(sectorId);
  12003. },
  12004. /**
  12005. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  12006. * object to the "active" object.
  12007. *
  12008. * @param sectorId
  12009. * @private
  12010. */
  12011. _activateSector : function(sectorId) {
  12012. // we move the set references from the frozen to the active stack.
  12013. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  12014. // we have moved the sector data into the active set, we now remove it from the frozen stack
  12015. this._deleteFrozenSector(sectorId);
  12016. },
  12017. /**
  12018. * This function merges the data from the currently active sector with a frozen sector. This is used
  12019. * in the process of reverting back to the previously active sector.
  12020. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  12021. * upon the creation of a new active sector.
  12022. *
  12023. * @param sectorId
  12024. * @private
  12025. */
  12026. _mergeThisWithFrozen : function(sectorId) {
  12027. // copy all nodes
  12028. for (var nodeId in this.nodes) {
  12029. if (this.nodes.hasOwnProperty(nodeId)) {
  12030. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  12031. }
  12032. }
  12033. // copy all edges (if not fully clustered, else there are no edges)
  12034. for (var edgeId in this.edges) {
  12035. if (this.edges.hasOwnProperty(edgeId)) {
  12036. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  12037. }
  12038. }
  12039. // merge the nodeIndices
  12040. for (var i = 0; i < this.nodeIndices.length; i++) {
  12041. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  12042. }
  12043. },
  12044. /**
  12045. * This clusters the sector to one cluster. It was a single cluster before this process started so
  12046. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  12047. *
  12048. * @private
  12049. */
  12050. _collapseThisToSingleCluster : function() {
  12051. this.clusterToFit(1,false);
  12052. },
  12053. /**
  12054. * We create a new active sector from the node that we want to open.
  12055. *
  12056. * @param node
  12057. * @private
  12058. */
  12059. _addSector : function(node) {
  12060. // this is the currently active sector
  12061. var sector = this._sector();
  12062. // // this should allow me to select nodes from a frozen set.
  12063. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  12064. // console.log("the node is part of the active sector");
  12065. // }
  12066. // else {
  12067. // console.log("I dont know what the fuck happened!!");
  12068. // }
  12069. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  12070. delete this.nodes[node.id];
  12071. var unqiueIdentifier = util.randomUUID();
  12072. // we fully freeze the currently active sector
  12073. this._freezeSector(sector);
  12074. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  12075. this._createNewSector(unqiueIdentifier);
  12076. // we add the active sector to the sectors array to be able to revert these steps later on
  12077. this._setActiveSector(unqiueIdentifier);
  12078. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  12079. this._switchToSector(this._sector());
  12080. // finally we add the node we removed from our previous active sector to the new active sector
  12081. this.nodes[node.id] = node;
  12082. },
  12083. /**
  12084. * We close the sector that is currently open and revert back to the one before.
  12085. * If the active sector is the "default" sector, nothing happens.
  12086. *
  12087. * @private
  12088. */
  12089. _collapseSector : function() {
  12090. // the currently active sector
  12091. var sector = this._sector();
  12092. // we cannot collapse the default sector
  12093. if (sector != "default") {
  12094. if ((this.nodeIndices.length == 1) ||
  12095. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12096. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12097. var previousSector = this._previousSector();
  12098. // we collapse the sector back to a single cluster
  12099. this._collapseThisToSingleCluster();
  12100. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  12101. // This previous sector is the one we will reactivate
  12102. this._mergeThisWithFrozen(previousSector);
  12103. // the previously active (frozen) sector now has all the data from the currently active sector.
  12104. // we can now delete the active sector.
  12105. this._deleteActiveSector(sector);
  12106. // we activate the previously active (and currently frozen) sector.
  12107. this._activateSector(previousSector);
  12108. // we load the references from the newly active sector into the global references
  12109. this._switchToSector(previousSector);
  12110. // we forget the previously active sector because we reverted to the one before
  12111. this._forgetLastSector();
  12112. // finally, we update the node index list.
  12113. this._updateNodeIndexList();
  12114. // we refresh the list with calulation nodes and calculation node indices.
  12115. this._updateCalculationNodes();
  12116. }
  12117. }
  12118. },
  12119. /**
  12120. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12121. *
  12122. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12123. * | we dont pass the function itself because then the "this" is the window object
  12124. * | instead of the Graph object
  12125. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12126. * @private
  12127. */
  12128. _doInAllActiveSectors : function(runFunction,argument) {
  12129. if (argument === undefined) {
  12130. for (var sector in this.sectors["active"]) {
  12131. if (this.sectors["active"].hasOwnProperty(sector)) {
  12132. // switch the global references to those of this sector
  12133. this._switchToActiveSector(sector);
  12134. this[runFunction]();
  12135. }
  12136. }
  12137. }
  12138. else {
  12139. for (var sector in this.sectors["active"]) {
  12140. if (this.sectors["active"].hasOwnProperty(sector)) {
  12141. // switch the global references to those of this sector
  12142. this._switchToActiveSector(sector);
  12143. var args = Array.prototype.splice.call(arguments, 1);
  12144. if (args.length > 1) {
  12145. this[runFunction](args[0],args[1]);
  12146. }
  12147. else {
  12148. this[runFunction](argument);
  12149. }
  12150. }
  12151. }
  12152. }
  12153. // we revert the global references back to our active sector
  12154. this._loadLatestSector();
  12155. },
  12156. /**
  12157. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12158. *
  12159. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12160. * | we dont pass the function itself because then the "this" is the window object
  12161. * | instead of the Graph object
  12162. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12163. * @private
  12164. */
  12165. _doInSupportSector : function(runFunction,argument) {
  12166. if (argument === undefined) {
  12167. this._switchToSupportSector();
  12168. this[runFunction]();
  12169. }
  12170. else {
  12171. this._switchToSupportSector();
  12172. var args = Array.prototype.splice.call(arguments, 1);
  12173. if (args.length > 1) {
  12174. this[runFunction](args[0],args[1]);
  12175. }
  12176. else {
  12177. this[runFunction](argument);
  12178. }
  12179. }
  12180. // we revert the global references back to our active sector
  12181. this._loadLatestSector();
  12182. },
  12183. /**
  12184. * This runs a function in all frozen sectors. This is used in the _redraw().
  12185. *
  12186. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12187. * | we don't pass the function itself because then the "this" is the window object
  12188. * | instead of the Graph object
  12189. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12190. * @private
  12191. */
  12192. _doInAllFrozenSectors : function(runFunction,argument) {
  12193. if (argument === undefined) {
  12194. for (var sector in this.sectors["frozen"]) {
  12195. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12196. // switch the global references to those of this sector
  12197. this._switchToFrozenSector(sector);
  12198. this[runFunction]();
  12199. }
  12200. }
  12201. }
  12202. else {
  12203. for (var sector in this.sectors["frozen"]) {
  12204. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12205. // switch the global references to those of this sector
  12206. this._switchToFrozenSector(sector);
  12207. var args = Array.prototype.splice.call(arguments, 1);
  12208. if (args.length > 1) {
  12209. this[runFunction](args[0],args[1]);
  12210. }
  12211. else {
  12212. this[runFunction](argument);
  12213. }
  12214. }
  12215. }
  12216. }
  12217. this._loadLatestSector();
  12218. },
  12219. /**
  12220. * This runs a function in all sectors. This is used in the _redraw().
  12221. *
  12222. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12223. * | we don't pass the function itself because then the "this" is the window object
  12224. * | instead of the Graph object
  12225. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12226. * @private
  12227. */
  12228. _doInAllSectors : function(runFunction,argument) {
  12229. var args = Array.prototype.splice.call(arguments, 1);
  12230. if (argument === undefined) {
  12231. this._doInAllActiveSectors(runFunction);
  12232. this._doInAllFrozenSectors(runFunction);
  12233. }
  12234. else {
  12235. if (args.length > 1) {
  12236. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  12237. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  12238. }
  12239. else {
  12240. this._doInAllActiveSectors(runFunction,argument);
  12241. this._doInAllFrozenSectors(runFunction,argument);
  12242. }
  12243. }
  12244. },
  12245. /**
  12246. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  12247. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  12248. *
  12249. * @private
  12250. */
  12251. _clearNodeIndexList : function() {
  12252. var sector = this._sector();
  12253. this.sectors["active"][sector]["nodeIndices"] = [];
  12254. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  12255. },
  12256. /**
  12257. * Draw the encompassing sector node
  12258. *
  12259. * @param ctx
  12260. * @param sectorType
  12261. * @private
  12262. */
  12263. _drawSectorNodes : function(ctx,sectorType) {
  12264. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  12265. for (var sector in this.sectors[sectorType]) {
  12266. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  12267. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  12268. this._switchToSector(sector,sectorType);
  12269. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  12270. for (var nodeId in this.nodes) {
  12271. if (this.nodes.hasOwnProperty(nodeId)) {
  12272. node = this.nodes[nodeId];
  12273. node.resize(ctx);
  12274. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  12275. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  12276. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  12277. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  12278. }
  12279. }
  12280. node = this.sectors[sectorType][sector]["drawingNode"];
  12281. node.x = 0.5 * (maxX + minX);
  12282. node.y = 0.5 * (maxY + minY);
  12283. node.width = 2 * (node.x - minX);
  12284. node.height = 2 * (node.y - minY);
  12285. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  12286. node.setScale(this.scale);
  12287. node._drawCircle(ctx);
  12288. }
  12289. }
  12290. }
  12291. },
  12292. _drawAllSectorNodes : function(ctx) {
  12293. this._drawSectorNodes(ctx,"frozen");
  12294. this._drawSectorNodes(ctx,"active");
  12295. this._loadLatestSector();
  12296. }
  12297. };
  12298. /**
  12299. * Creation of the ClusterMixin var.
  12300. *
  12301. * This contains all the functions the Graph object can use to employ clustering
  12302. *
  12303. * Alex de Mulder
  12304. * 21-01-2013
  12305. */
  12306. var ClusterMixin = {
  12307. /**
  12308. * This is only called in the constructor of the graph object
  12309. *
  12310. */
  12311. startWithClustering : function() {
  12312. // cluster if the data set is big
  12313. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  12314. // updates the lables after clustering
  12315. this.updateLabels();
  12316. // this is called here because if clusterin is disabled, the start and stabilize are called in
  12317. // the setData function.
  12318. if (this.stabilize) {
  12319. this._doStabilize();
  12320. }
  12321. this.start();
  12322. },
  12323. /**
  12324. * This function clusters until the initialMaxNodes has been reached
  12325. *
  12326. * @param {Number} maxNumberOfNodes
  12327. * @param {Boolean} reposition
  12328. */
  12329. clusterToFit : function(maxNumberOfNodes, reposition) {
  12330. var numberOfNodes = this.nodeIndices.length;
  12331. var maxLevels = 50;
  12332. var level = 0;
  12333. // we first cluster the hubs, then we pull in the outliers, repeat
  12334. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  12335. if (level % 3 == 0) {
  12336. this.forceAggregateHubs(true);
  12337. this.normalizeClusterLevels();
  12338. }
  12339. else {
  12340. this.increaseClusterLevel(); // this also includes a cluster normalization
  12341. }
  12342. numberOfNodes = this.nodeIndices.length;
  12343. level += 1;
  12344. }
  12345. // after the clustering we reposition the nodes to reduce the initial chaos
  12346. if (level > 0 && reposition == true) {
  12347. this.repositionNodes();
  12348. }
  12349. this._updateCalculationNodes();
  12350. },
  12351. /**
  12352. * This function can be called to open up a specific cluster. It is only called by
  12353. * It will unpack the cluster back one level.
  12354. *
  12355. * @param node | Node object: cluster to open.
  12356. */
  12357. openCluster : function(node) {
  12358. var isMovingBeforeClustering = this.moving;
  12359. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  12360. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  12361. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  12362. this._addSector(node);
  12363. var level = 0;
  12364. // we decluster until we reach a decent number of nodes
  12365. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  12366. this.decreaseClusterLevel();
  12367. level += 1;
  12368. }
  12369. }
  12370. else {
  12371. this._expandClusterNode(node,false,true);
  12372. // update the index list, dynamic edges and labels
  12373. this._updateNodeIndexList();
  12374. this._updateDynamicEdges();
  12375. this._updateCalculationNodes();
  12376. this.updateLabels();
  12377. }
  12378. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12379. if (this.moving != isMovingBeforeClustering) {
  12380. this.start();
  12381. }
  12382. },
  12383. /**
  12384. * This calls the updateClustes with default arguments
  12385. */
  12386. updateClustersDefault : function() {
  12387. if (this.constants.clustering.enabled == true) {
  12388. this.updateClusters(0,false,false);
  12389. }
  12390. },
  12391. /**
  12392. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  12393. * be clustered with their connected node. This can be repeated as many times as needed.
  12394. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  12395. */
  12396. increaseClusterLevel : function() {
  12397. this.updateClusters(-1,false,true);
  12398. },
  12399. /**
  12400. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  12401. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  12402. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  12403. */
  12404. decreaseClusterLevel : function() {
  12405. this.updateClusters(1,false,true);
  12406. },
  12407. /**
  12408. * This is the main clustering function. It clusters and declusters on zoom or forced
  12409. * This function clusters on zoom, it can be called with a predefined zoom direction
  12410. * If out, check if we can form clusters, if in, check if we can open clusters.
  12411. * This function is only called from _zoom()
  12412. *
  12413. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  12414. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  12415. * @param {Boolean} force | enabled or disable forcing
  12416. *
  12417. */
  12418. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  12419. var isMovingBeforeClustering = this.moving;
  12420. var amountOfNodes = this.nodeIndices.length;
  12421. // on zoom out collapse the sector if the scale is at the level the sector was made
  12422. if (this.previousScale > this.scale && zoomDirection == 0) {
  12423. this._collapseSector();
  12424. }
  12425. // check if we zoom in or out
  12426. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12427. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  12428. // outer nodes determines if it is being clustered
  12429. this._formClusters(force);
  12430. }
  12431. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  12432. if (force == true) {
  12433. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  12434. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  12435. this._openClusters(recursive,force);
  12436. }
  12437. else {
  12438. // if a cluster takes up a set percentage of the active window
  12439. this._openClustersBySize();
  12440. }
  12441. }
  12442. this._updateNodeIndexList();
  12443. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  12444. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  12445. this._aggregateHubs(force);
  12446. this._updateNodeIndexList();
  12447. }
  12448. // we now reduce chains.
  12449. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12450. this.handleChains();
  12451. this._updateNodeIndexList();
  12452. }
  12453. this.previousScale = this.scale;
  12454. // rest of the update the index list, dynamic edges and labels
  12455. this._updateDynamicEdges();
  12456. this.updateLabels();
  12457. // if a cluster was formed, we increase the clusterSession
  12458. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  12459. this.clusterSession += 1;
  12460. // if clusters have been made, we normalize the cluster level
  12461. this.normalizeClusterLevels();
  12462. }
  12463. if (doNotStart == false || doNotStart === undefined) {
  12464. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12465. if (this.moving != isMovingBeforeClustering) {
  12466. this.start();
  12467. }
  12468. }
  12469. this._updateCalculationNodes();
  12470. },
  12471. /**
  12472. * This function handles the chains. It is called on every updateClusters().
  12473. */
  12474. handleChains : function() {
  12475. // after clustering we check how many chains there are
  12476. var chainPercentage = this._getChainFraction();
  12477. if (chainPercentage > this.constants.clustering.chainThreshold) {
  12478. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  12479. }
  12480. },
  12481. /**
  12482. * this functions starts clustering by hubs
  12483. * The minimum hub threshold is set globally
  12484. *
  12485. * @private
  12486. */
  12487. _aggregateHubs : function(force) {
  12488. this._getHubSize();
  12489. this._formClustersByHub(force,false);
  12490. },
  12491. /**
  12492. * This function is fired by keypress. It forces hubs to form.
  12493. *
  12494. */
  12495. forceAggregateHubs : function(doNotStart) {
  12496. var isMovingBeforeClustering = this.moving;
  12497. var amountOfNodes = this.nodeIndices.length;
  12498. this._aggregateHubs(true);
  12499. // update the index list, dynamic edges and labels
  12500. this._updateNodeIndexList();
  12501. this._updateDynamicEdges();
  12502. this.updateLabels();
  12503. // if a cluster was formed, we increase the clusterSession
  12504. if (this.nodeIndices.length != amountOfNodes) {
  12505. this.clusterSession += 1;
  12506. }
  12507. if (doNotStart == false || doNotStart === undefined) {
  12508. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12509. if (this.moving != isMovingBeforeClustering) {
  12510. this.start();
  12511. }
  12512. }
  12513. },
  12514. /**
  12515. * If a cluster takes up more than a set percentage of the screen, open the cluster
  12516. *
  12517. * @private
  12518. */
  12519. _openClustersBySize : function() {
  12520. for (var nodeId in this.nodes) {
  12521. if (this.nodes.hasOwnProperty(nodeId)) {
  12522. var node = this.nodes[nodeId];
  12523. if (node.inView() == true) {
  12524. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12525. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12526. this.openCluster(node);
  12527. }
  12528. }
  12529. }
  12530. }
  12531. },
  12532. /**
  12533. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  12534. * has to be opened based on the current zoom level.
  12535. *
  12536. * @private
  12537. */
  12538. _openClusters : function(recursive,force) {
  12539. for (var i = 0; i < this.nodeIndices.length; i++) {
  12540. var node = this.nodes[this.nodeIndices[i]];
  12541. this._expandClusterNode(node,recursive,force);
  12542. this._updateCalculationNodes();
  12543. }
  12544. },
  12545. /**
  12546. * This function checks if a node has to be opened. This is done by checking the zoom level.
  12547. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  12548. * This recursive behaviour is optional and can be set by the recursive argument.
  12549. *
  12550. * @param {Node} parentNode | to check for cluster and expand
  12551. * @param {Boolean} recursive | enabled or disable recursive calling
  12552. * @param {Boolean} force | enabled or disable forcing
  12553. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  12554. * @private
  12555. */
  12556. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  12557. // first check if node is a cluster
  12558. if (parentNode.clusterSize > 1) {
  12559. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  12560. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  12561. openAll = true;
  12562. }
  12563. recursive = openAll ? true : recursive;
  12564. // if the last child has been added on a smaller scale than current scale decluster
  12565. if (parentNode.formationScale < this.scale || force == true) {
  12566. // we will check if any of the contained child nodes should be removed from the cluster
  12567. for (var containedNodeId in parentNode.containedNodes) {
  12568. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  12569. var childNode = parentNode.containedNodes[containedNodeId];
  12570. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  12571. // the largest cluster is the one that comes from outside
  12572. if (force == true) {
  12573. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  12574. || openAll) {
  12575. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12576. }
  12577. }
  12578. else {
  12579. if (this._nodeInActiveArea(parentNode)) {
  12580. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12581. }
  12582. }
  12583. }
  12584. }
  12585. }
  12586. }
  12587. },
  12588. /**
  12589. * ONLY CALLED FROM _expandClusterNode
  12590. *
  12591. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  12592. * the child node from the parent contained_node object and put it back into the global nodes object.
  12593. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  12594. *
  12595. * @param {Node} parentNode | the parent node
  12596. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  12597. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  12598. * With force and recursive both true, the entire cluster is unpacked
  12599. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  12600. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  12601. * @private
  12602. */
  12603. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  12604. var childNode = parentNode.containedNodes[containedNodeId];
  12605. // if child node has been added on smaller scale than current, kick out
  12606. if (childNode.formationScale < this.scale || force == true) {
  12607. // unselect all selected items
  12608. this._unselectAll();
  12609. // put the child node back in the global nodes object
  12610. this.nodes[containedNodeId] = childNode;
  12611. // release the contained edges from this childNode back into the global edges
  12612. this._releaseContainedEdges(parentNode,childNode);
  12613. // reconnect rerouted edges to the childNode
  12614. this._connectEdgeBackToChild(parentNode,childNode);
  12615. // validate all edges in dynamicEdges
  12616. this._validateEdges(parentNode);
  12617. // undo the changes from the clustering operation on the parent node
  12618. parentNode.mass -= childNode.mass;
  12619. parentNode.clusterSize -= childNode.clusterSize;
  12620. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12621. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  12622. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  12623. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  12624. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  12625. // remove node from the list
  12626. delete parentNode.containedNodes[containedNodeId];
  12627. // check if there are other childs with this clusterSession in the parent.
  12628. var othersPresent = false;
  12629. for (var childNodeId in parentNode.containedNodes) {
  12630. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  12631. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  12632. othersPresent = true;
  12633. break;
  12634. }
  12635. }
  12636. }
  12637. // if there are no others, remove the cluster session from the list
  12638. if (othersPresent == false) {
  12639. parentNode.clusterSessions.pop();
  12640. }
  12641. this._repositionBezierNodes(childNode);
  12642. // this._repositionBezierNodes(parentNode);
  12643. // remove the clusterSession from the child node
  12644. childNode.clusterSession = 0;
  12645. // recalculate the size of the node on the next time the node is rendered
  12646. parentNode.clearSizeCache();
  12647. // restart the simulation to reorganise all nodes
  12648. this.moving = true;
  12649. }
  12650. // check if a further expansion step is possible if recursivity is enabled
  12651. if (recursive == true) {
  12652. this._expandClusterNode(childNode,recursive,force,openAll);
  12653. }
  12654. },
  12655. /**
  12656. * position the bezier nodes at the center of the edges
  12657. *
  12658. * @param node
  12659. * @private
  12660. */
  12661. _repositionBezierNodes : function(node) {
  12662. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12663. node.dynamicEdges[i].positionBezierNode();
  12664. }
  12665. },
  12666. /**
  12667. * This function checks if any nodes at the end of their trees have edges below a threshold length
  12668. * This function is called only from updateClusters()
  12669. * forceLevelCollapse ignores the length of the edge and collapses one level
  12670. * This means that a node with only one edge will be clustered with its connected node
  12671. *
  12672. * @private
  12673. * @param {Boolean} force
  12674. */
  12675. _formClusters : function(force) {
  12676. if (force == false) {
  12677. this._formClustersByZoom();
  12678. }
  12679. else {
  12680. this._forceClustersByZoom();
  12681. }
  12682. },
  12683. /**
  12684. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  12685. *
  12686. * @private
  12687. */
  12688. _formClustersByZoom : function() {
  12689. var dx,dy,length,
  12690. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12691. // check if any edges are shorter than minLength and start the clustering
  12692. // the clustering favours the node with the larger mass
  12693. for (var edgeId in this.edges) {
  12694. if (this.edges.hasOwnProperty(edgeId)) {
  12695. var edge = this.edges[edgeId];
  12696. if (edge.connected) {
  12697. if (edge.toId != edge.fromId) {
  12698. dx = (edge.to.x - edge.from.x);
  12699. dy = (edge.to.y - edge.from.y);
  12700. length = Math.sqrt(dx * dx + dy * dy);
  12701. if (length < minLength) {
  12702. // first check which node is larger
  12703. var parentNode = edge.from;
  12704. var childNode = edge.to;
  12705. if (edge.to.mass > edge.from.mass) {
  12706. parentNode = edge.to;
  12707. childNode = edge.from;
  12708. }
  12709. if (childNode.dynamicEdgesLength == 1) {
  12710. this._addToCluster(parentNode,childNode,false);
  12711. }
  12712. else if (parentNode.dynamicEdgesLength == 1) {
  12713. this._addToCluster(childNode,parentNode,false);
  12714. }
  12715. }
  12716. }
  12717. }
  12718. }
  12719. }
  12720. },
  12721. /**
  12722. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12723. * connected node.
  12724. *
  12725. * @private
  12726. */
  12727. _forceClustersByZoom : function() {
  12728. for (var nodeId in this.nodes) {
  12729. // another node could have absorbed this child.
  12730. if (this.nodes.hasOwnProperty(nodeId)) {
  12731. var childNode = this.nodes[nodeId];
  12732. // the edges can be swallowed by another decrease
  12733. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12734. var edge = childNode.dynamicEdges[0];
  12735. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12736. // group to the largest node
  12737. if (childNode.id != parentNode.id) {
  12738. if (parentNode.mass > childNode.mass) {
  12739. this._addToCluster(parentNode,childNode,true);
  12740. }
  12741. else {
  12742. this._addToCluster(childNode,parentNode,true);
  12743. }
  12744. }
  12745. }
  12746. }
  12747. }
  12748. },
  12749. /**
  12750. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12751. * This function clusters a node to its smallest connected neighbour.
  12752. *
  12753. * @param node
  12754. * @private
  12755. */
  12756. _clusterToSmallestNeighbour : function(node) {
  12757. var smallestNeighbour = -1;
  12758. var smallestNeighbourNode = null;
  12759. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12760. if (node.dynamicEdges[i] !== undefined) {
  12761. var neighbour = null;
  12762. if (node.dynamicEdges[i].fromId != node.id) {
  12763. neighbour = node.dynamicEdges[i].from;
  12764. }
  12765. else if (node.dynamicEdges[i].toId != node.id) {
  12766. neighbour = node.dynamicEdges[i].to;
  12767. }
  12768. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12769. smallestNeighbour = neighbour.clusterSessions.length;
  12770. smallestNeighbourNode = neighbour;
  12771. }
  12772. }
  12773. }
  12774. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12775. this._addToCluster(neighbour, node, true);
  12776. }
  12777. },
  12778. /**
  12779. * This function forms clusters from hubs, it loops over all nodes
  12780. *
  12781. * @param {Boolean} force | Disregard zoom level
  12782. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12783. * @private
  12784. */
  12785. _formClustersByHub : function(force, onlyEqual) {
  12786. // we loop over all nodes in the list
  12787. for (var nodeId in this.nodes) {
  12788. // we check if it is still available since it can be used by the clustering in this loop
  12789. if (this.nodes.hasOwnProperty(nodeId)) {
  12790. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12791. }
  12792. }
  12793. },
  12794. /**
  12795. * This function forms a cluster from a specific preselected hub node
  12796. *
  12797. * @param {Node} hubNode | the node we will cluster as a hub
  12798. * @param {Boolean} force | Disregard zoom level
  12799. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12800. * @param {Number} [absorptionSizeOffset] |
  12801. * @private
  12802. */
  12803. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12804. if (absorptionSizeOffset === undefined) {
  12805. absorptionSizeOffset = 0;
  12806. }
  12807. // we decide if the node is a hub
  12808. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12809. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12810. // initialize variables
  12811. var dx,dy,length;
  12812. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12813. var allowCluster = false;
  12814. // we create a list of edges because the dynamicEdges change over the course of this loop
  12815. var edgesIdarray = [];
  12816. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12817. for (var j = 0; j < amountOfInitialEdges; j++) {
  12818. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12819. }
  12820. // if the hub clustering is not forces, we check if one of the edges connected
  12821. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12822. if (force == false) {
  12823. allowCluster = false;
  12824. for (j = 0; j < amountOfInitialEdges; j++) {
  12825. var edge = this.edges[edgesIdarray[j]];
  12826. if (edge !== undefined) {
  12827. if (edge.connected) {
  12828. if (edge.toId != edge.fromId) {
  12829. dx = (edge.to.x - edge.from.x);
  12830. dy = (edge.to.y - edge.from.y);
  12831. length = Math.sqrt(dx * dx + dy * dy);
  12832. if (length < minLength) {
  12833. allowCluster = true;
  12834. break;
  12835. }
  12836. }
  12837. }
  12838. }
  12839. }
  12840. }
  12841. // start the clustering if allowed
  12842. if ((!force && allowCluster) || force) {
  12843. // we loop over all edges INITIALLY connected to this hub
  12844. for (j = 0; j < amountOfInitialEdges; j++) {
  12845. edge = this.edges[edgesIdarray[j]];
  12846. // the edge can be clustered by this function in a previous loop
  12847. if (edge !== undefined) {
  12848. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12849. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12850. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12851. (childNode.id != hubNode.id)) {
  12852. this._addToCluster(hubNode,childNode,force);
  12853. }
  12854. }
  12855. }
  12856. }
  12857. }
  12858. },
  12859. /**
  12860. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12861. *
  12862. * @param {Node} parentNode | this is the node that will house the child node
  12863. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12864. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12865. * @private
  12866. */
  12867. _addToCluster : function(parentNode, childNode, force) {
  12868. // join child node in the parent node
  12869. parentNode.containedNodes[childNode.id] = childNode;
  12870. // manage all the edges connected to the child and parent nodes
  12871. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12872. var edge = childNode.dynamicEdges[i];
  12873. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12874. this._addToContainedEdges(parentNode,childNode,edge);
  12875. }
  12876. else {
  12877. this._connectEdgeToCluster(parentNode,childNode,edge);
  12878. }
  12879. }
  12880. // a contained node has no dynamic edges.
  12881. childNode.dynamicEdges = [];
  12882. // remove circular edges from clusters
  12883. this._containCircularEdgesFromNode(parentNode,childNode);
  12884. // remove the childNode from the global nodes object
  12885. delete this.nodes[childNode.id];
  12886. // update the properties of the child and parent
  12887. var massBefore = parentNode.mass;
  12888. childNode.clusterSession = this.clusterSession;
  12889. parentNode.mass += childNode.mass;
  12890. parentNode.clusterSize += childNode.clusterSize;
  12891. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12892. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12893. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12894. parentNode.clusterSessions.push(this.clusterSession);
  12895. }
  12896. // forced clusters only open from screen size and double tap
  12897. if (force == true) {
  12898. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12899. parentNode.formationScale = 0;
  12900. }
  12901. else {
  12902. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12903. }
  12904. // recalculate the size of the node on the next time the node is rendered
  12905. parentNode.clearSizeCache();
  12906. // set the pop-out scale for the childnode
  12907. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12908. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12909. childNode.clearVelocity();
  12910. // the mass has altered, preservation of energy dictates the velocity to be updated
  12911. parentNode.updateVelocity(massBefore);
  12912. // restart the simulation to reorganise all nodes
  12913. this.moving = true;
  12914. },
  12915. /**
  12916. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12917. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12918. * It has to be called if a level is collapsed. It is called by _formClusters().
  12919. * @private
  12920. */
  12921. _updateDynamicEdges : function() {
  12922. for (var i = 0; i < this.nodeIndices.length; i++) {
  12923. var node = this.nodes[this.nodeIndices[i]];
  12924. node.dynamicEdgesLength = node.dynamicEdges.length;
  12925. // this corrects for multiple edges pointing at the same other node
  12926. var correction = 0;
  12927. if (node.dynamicEdgesLength > 1) {
  12928. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12929. var edgeToId = node.dynamicEdges[j].toId;
  12930. var edgeFromId = node.dynamicEdges[j].fromId;
  12931. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12932. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12933. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12934. correction += 1;
  12935. }
  12936. }
  12937. }
  12938. }
  12939. node.dynamicEdgesLength -= correction;
  12940. }
  12941. },
  12942. /**
  12943. * This adds an edge from the childNode to the contained edges of the parent node
  12944. *
  12945. * @param parentNode | Node object
  12946. * @param childNode | Node object
  12947. * @param edge | Edge object
  12948. * @private
  12949. */
  12950. _addToContainedEdges : function(parentNode, childNode, edge) {
  12951. // create an array object if it does not yet exist for this childNode
  12952. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12953. parentNode.containedEdges[childNode.id] = []
  12954. }
  12955. // add this edge to the list
  12956. parentNode.containedEdges[childNode.id].push(edge);
  12957. // remove the edge from the global edges object
  12958. delete this.edges[edge.id];
  12959. // remove the edge from the parent object
  12960. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12961. if (parentNode.dynamicEdges[i].id == edge.id) {
  12962. parentNode.dynamicEdges.splice(i,1);
  12963. break;
  12964. }
  12965. }
  12966. },
  12967. /**
  12968. * This function connects an edge that was connected to a child node to the parent node.
  12969. * It keeps track of which nodes it has been connected to with the originalId array.
  12970. *
  12971. * @param {Node} parentNode | Node object
  12972. * @param {Node} childNode | Node object
  12973. * @param {Edge} edge | Edge object
  12974. * @private
  12975. */
  12976. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12977. // handle circular edges
  12978. if (edge.toId == edge.fromId) {
  12979. this._addToContainedEdges(parentNode, childNode, edge);
  12980. }
  12981. else {
  12982. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12983. edge.originalToId.push(childNode.id);
  12984. edge.to = parentNode;
  12985. edge.toId = parentNode.id;
  12986. }
  12987. else { // edge connected to other node with the "from" side
  12988. edge.originalFromId.push(childNode.id);
  12989. edge.from = parentNode;
  12990. edge.fromId = parentNode.id;
  12991. }
  12992. this._addToReroutedEdges(parentNode,childNode,edge);
  12993. }
  12994. },
  12995. /**
  12996. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12997. * these edges inside of the cluster.
  12998. *
  12999. * @param parentNode
  13000. * @param childNode
  13001. * @private
  13002. */
  13003. _containCircularEdgesFromNode : function(parentNode, childNode) {
  13004. // manage all the edges connected to the child and parent nodes
  13005. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13006. var edge = parentNode.dynamicEdges[i];
  13007. // handle circular edges
  13008. if (edge.toId == edge.fromId) {
  13009. this._addToContainedEdges(parentNode, childNode, edge);
  13010. }
  13011. }
  13012. },
  13013. /**
  13014. * This adds an edge from the childNode to the rerouted edges of the parent node
  13015. *
  13016. * @param parentNode | Node object
  13017. * @param childNode | Node object
  13018. * @param edge | Edge object
  13019. * @private
  13020. */
  13021. _addToReroutedEdges : function(parentNode, childNode, edge) {
  13022. // create an array object if it does not yet exist for this childNode
  13023. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  13024. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  13025. parentNode.reroutedEdges[childNode.id] = [];
  13026. }
  13027. parentNode.reroutedEdges[childNode.id].push(edge);
  13028. // this edge becomes part of the dynamicEdges of the cluster node
  13029. parentNode.dynamicEdges.push(edge);
  13030. },
  13031. /**
  13032. * This function connects an edge that was connected to a cluster node back to the child node.
  13033. *
  13034. * @param parentNode | Node object
  13035. * @param childNode | Node object
  13036. * @private
  13037. */
  13038. _connectEdgeBackToChild : function(parentNode, childNode) {
  13039. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  13040. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  13041. var edge = parentNode.reroutedEdges[childNode.id][i];
  13042. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  13043. edge.originalFromId.pop();
  13044. edge.fromId = childNode.id;
  13045. edge.from = childNode;
  13046. }
  13047. else {
  13048. edge.originalToId.pop();
  13049. edge.toId = childNode.id;
  13050. edge.to = childNode;
  13051. }
  13052. // append this edge to the list of edges connecting to the childnode
  13053. childNode.dynamicEdges.push(edge);
  13054. // remove the edge from the parent object
  13055. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  13056. if (parentNode.dynamicEdges[j].id == edge.id) {
  13057. parentNode.dynamicEdges.splice(j,1);
  13058. break;
  13059. }
  13060. }
  13061. }
  13062. // remove the entry from the rerouted edges
  13063. delete parentNode.reroutedEdges[childNode.id];
  13064. }
  13065. },
  13066. /**
  13067. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  13068. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  13069. * parentNode
  13070. *
  13071. * @param parentNode | Node object
  13072. * @private
  13073. */
  13074. _validateEdges : function(parentNode) {
  13075. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13076. var edge = parentNode.dynamicEdges[i];
  13077. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  13078. parentNode.dynamicEdges.splice(i,1);
  13079. }
  13080. }
  13081. },
  13082. /**
  13083. * This function released the contained edges back into the global domain and puts them back into the
  13084. * dynamic edges of both parent and child.
  13085. *
  13086. * @param {Node} parentNode |
  13087. * @param {Node} childNode |
  13088. * @private
  13089. */
  13090. _releaseContainedEdges : function(parentNode, childNode) {
  13091. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  13092. var edge = parentNode.containedEdges[childNode.id][i];
  13093. // put the edge back in the global edges object
  13094. this.edges[edge.id] = edge;
  13095. // put the edge back in the dynamic edges of the child and parent
  13096. childNode.dynamicEdges.push(edge);
  13097. parentNode.dynamicEdges.push(edge);
  13098. }
  13099. // remove the entry from the contained edges
  13100. delete parentNode.containedEdges[childNode.id];
  13101. },
  13102. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  13103. /**
  13104. * This updates the node labels for all nodes (for debugging purposes)
  13105. */
  13106. updateLabels : function() {
  13107. var nodeId;
  13108. // update node labels
  13109. for (nodeId in this.nodes) {
  13110. if (this.nodes.hasOwnProperty(nodeId)) {
  13111. var node = this.nodes[nodeId];
  13112. if (node.clusterSize > 1) {
  13113. node.label = "[".concat(String(node.clusterSize),"]");
  13114. }
  13115. }
  13116. }
  13117. // update node labels
  13118. for (nodeId in this.nodes) {
  13119. if (this.nodes.hasOwnProperty(nodeId)) {
  13120. node = this.nodes[nodeId];
  13121. if (node.clusterSize == 1) {
  13122. if (node.originalLabel !== undefined) {
  13123. node.label = node.originalLabel;
  13124. }
  13125. else {
  13126. node.label = String(node.id);
  13127. }
  13128. }
  13129. }
  13130. }
  13131. // /* Debug Override */
  13132. // for (nodeId in this.nodes) {
  13133. // if (this.nodes.hasOwnProperty(nodeId)) {
  13134. // node = this.nodes[nodeId];
  13135. // node.label = String(node.level);
  13136. // }
  13137. // }
  13138. },
  13139. /**
  13140. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  13141. * if the rest of the nodes are already a few cluster levels in.
  13142. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  13143. * clustered enough to the clusterToSmallestNeighbours function.
  13144. */
  13145. normalizeClusterLevels : function() {
  13146. var maxLevel = 0;
  13147. var minLevel = 1e9;
  13148. var clusterLevel = 0;
  13149. // we loop over all nodes in the list
  13150. for (var nodeId in this.nodes) {
  13151. if (this.nodes.hasOwnProperty(nodeId)) {
  13152. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  13153. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  13154. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  13155. }
  13156. }
  13157. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  13158. var amountOfNodes = this.nodeIndices.length;
  13159. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  13160. // we loop over all nodes in the list
  13161. for (var nodeId in this.nodes) {
  13162. if (this.nodes.hasOwnProperty(nodeId)) {
  13163. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  13164. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  13165. }
  13166. }
  13167. }
  13168. this._updateNodeIndexList();
  13169. this._updateDynamicEdges();
  13170. // if a cluster was formed, we increase the clusterSession
  13171. if (this.nodeIndices.length != amountOfNodes) {
  13172. this.clusterSession += 1;
  13173. }
  13174. }
  13175. },
  13176. /**
  13177. * This function determines if the cluster we want to decluster is in the active area
  13178. * this means around the zoom center
  13179. *
  13180. * @param {Node} node
  13181. * @returns {boolean}
  13182. * @private
  13183. */
  13184. _nodeInActiveArea : function(node) {
  13185. return (
  13186. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13187. &&
  13188. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13189. )
  13190. },
  13191. /**
  13192. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  13193. * It puts large clusters away from the center and randomizes the order.
  13194. *
  13195. */
  13196. repositionNodes : function() {
  13197. for (var i = 0; i < this.nodeIndices.length; i++) {
  13198. var node = this.nodes[this.nodeIndices[i]];
  13199. if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
  13200. var radius = this.constants.physics.springLength * Math.min(100,node.mass);
  13201. var angle = 2 * Math.PI * Math.random();
  13202. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  13203. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  13204. this._repositionBezierNodes(node);
  13205. }
  13206. }
  13207. },
  13208. /**
  13209. * We determine how many connections denote an important hub.
  13210. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  13211. *
  13212. * @private
  13213. */
  13214. _getHubSize : function() {
  13215. var average = 0;
  13216. var averageSquared = 0;
  13217. var hubCounter = 0;
  13218. var largestHub = 0;
  13219. for (var i = 0; i < this.nodeIndices.length; i++) {
  13220. var node = this.nodes[this.nodeIndices[i]];
  13221. if (node.dynamicEdgesLength > largestHub) {
  13222. largestHub = node.dynamicEdgesLength;
  13223. }
  13224. average += node.dynamicEdgesLength;
  13225. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  13226. hubCounter += 1;
  13227. }
  13228. average = average / hubCounter;
  13229. averageSquared = averageSquared / hubCounter;
  13230. var variance = averageSquared - Math.pow(average,2);
  13231. var standardDeviation = Math.sqrt(variance);
  13232. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  13233. // always have at least one to cluster
  13234. if (this.hubThreshold > largestHub) {
  13235. this.hubThreshold = largestHub;
  13236. }
  13237. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  13238. // console.log("hubThreshold:",this.hubThreshold);
  13239. },
  13240. /**
  13241. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13242. * with this amount we can cluster specifically on these chains.
  13243. *
  13244. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  13245. * @private
  13246. */
  13247. _reduceAmountOfChains : function(fraction) {
  13248. this.hubThreshold = 2;
  13249. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  13250. for (var nodeId in this.nodes) {
  13251. if (this.nodes.hasOwnProperty(nodeId)) {
  13252. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13253. if (reduceAmount > 0) {
  13254. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  13255. reduceAmount -= 1;
  13256. }
  13257. }
  13258. }
  13259. }
  13260. },
  13261. /**
  13262. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13263. * with this amount we can cluster specifically on these chains.
  13264. *
  13265. * @private
  13266. */
  13267. _getChainFraction : function() {
  13268. var chains = 0;
  13269. var total = 0;
  13270. for (var nodeId in this.nodes) {
  13271. if (this.nodes.hasOwnProperty(nodeId)) {
  13272. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13273. chains += 1;
  13274. }
  13275. total += 1;
  13276. }
  13277. }
  13278. return chains/total;
  13279. }
  13280. };
  13281. var SelectionMixin = {
  13282. /**
  13283. * This function can be called from the _doInAllSectors function
  13284. *
  13285. * @param object
  13286. * @param overlappingNodes
  13287. * @private
  13288. */
  13289. _getNodesOverlappingWith : function(object, overlappingNodes) {
  13290. var nodes = this.nodes;
  13291. for (var nodeId in nodes) {
  13292. if (nodes.hasOwnProperty(nodeId)) {
  13293. if (nodes[nodeId].isOverlappingWith(object)) {
  13294. overlappingNodes.push(nodeId);
  13295. }
  13296. }
  13297. }
  13298. },
  13299. /**
  13300. * retrieve all nodes overlapping with given object
  13301. * @param {Object} object An object with parameters left, top, right, bottom
  13302. * @return {Number[]} An array with id's of the overlapping nodes
  13303. * @private
  13304. */
  13305. _getAllNodesOverlappingWith : function (object) {
  13306. var overlappingNodes = [];
  13307. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  13308. return overlappingNodes;
  13309. },
  13310. /**
  13311. * Return a position object in canvasspace from a single point in screenspace
  13312. *
  13313. * @param pointer
  13314. * @returns {{left: number, top: number, right: number, bottom: number}}
  13315. * @private
  13316. */
  13317. _pointerToPositionObject : function(pointer) {
  13318. var x = this._canvasToX(pointer.x);
  13319. var y = this._canvasToY(pointer.y);
  13320. return {left: x,
  13321. top: y,
  13322. right: x,
  13323. bottom: y};
  13324. },
  13325. /**
  13326. * Get the top node at the a specific point (like a click)
  13327. *
  13328. * @param {{x: Number, y: Number}} pointer
  13329. * @return {Node | null} node
  13330. * @private
  13331. */
  13332. _getNodeAt : function (pointer) {
  13333. // we first check if this is an navigation controls element
  13334. var positionObject = this._pointerToPositionObject(pointer);
  13335. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  13336. // if there are overlapping nodes, select the last one, this is the
  13337. // one which is drawn on top of the others
  13338. if (overlappingNodes.length > 0) {
  13339. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  13340. }
  13341. else {
  13342. return null;
  13343. }
  13344. },
  13345. /**
  13346. * retrieve all edges overlapping with given object, selector is around center
  13347. * @param {Object} object An object with parameters left, top, right, bottom
  13348. * @return {Number[]} An array with id's of the overlapping nodes
  13349. * @private
  13350. */
  13351. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  13352. var edges = this.edges;
  13353. for (var edgeId in edges) {
  13354. if (edges.hasOwnProperty(edgeId)) {
  13355. if (edges[edgeId].isOverlappingWith(object)) {
  13356. overlappingEdges.push(edgeId);
  13357. }
  13358. }
  13359. }
  13360. },
  13361. /**
  13362. * retrieve all nodes overlapping with given object
  13363. * @param {Object} object An object with parameters left, top, right, bottom
  13364. * @return {Number[]} An array with id's of the overlapping nodes
  13365. * @private
  13366. */
  13367. _getAllEdgesOverlappingWith : function (object) {
  13368. var overlappingEdges = [];
  13369. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  13370. return overlappingEdges;
  13371. },
  13372. /**
  13373. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  13374. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  13375. *
  13376. * @param pointer
  13377. * @returns {null}
  13378. * @private
  13379. */
  13380. _getEdgeAt : function(pointer) {
  13381. var positionObject = this._pointerToPositionObject(pointer);
  13382. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  13383. if (overlappingEdges.length > 0) {
  13384. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  13385. }
  13386. else {
  13387. return null;
  13388. }
  13389. },
  13390. /**
  13391. * Add object to the selection array.
  13392. *
  13393. * @param obj
  13394. * @private
  13395. */
  13396. _addToSelection : function(obj) {
  13397. this.selectionObj[obj.id] = obj;
  13398. },
  13399. /**
  13400. * Remove a single option from selection.
  13401. *
  13402. * @param {Object} obj
  13403. * @private
  13404. */
  13405. _removeFromSelection : function(obj) {
  13406. delete this.selectionObj[obj.id];
  13407. },
  13408. /**
  13409. * Unselect all. The selectionObj is useful for this.
  13410. *
  13411. * @param {Boolean} [doNotTrigger] | ignore trigger
  13412. * @private
  13413. */
  13414. _unselectAll : function(doNotTrigger) {
  13415. if (doNotTrigger === undefined) {
  13416. doNotTrigger = false;
  13417. }
  13418. for (var objectId in this.selectionObj) {
  13419. if (this.selectionObj.hasOwnProperty(objectId)) {
  13420. this.selectionObj[objectId].unselect();
  13421. }
  13422. }
  13423. this.selectionObj = {};
  13424. if (doNotTrigger == false) {
  13425. this.emit('select', this.getSelection());
  13426. }
  13427. },
  13428. /**
  13429. * Unselect all clusters. The selectionObj is useful for this.
  13430. *
  13431. * @param {Boolean} [doNotTrigger] | ignore trigger
  13432. * @private
  13433. */
  13434. _unselectClusters : function(doNotTrigger) {
  13435. if (doNotTrigger === undefined) {
  13436. doNotTrigger = false;
  13437. }
  13438. for (var objectId in this.selectionObj) {
  13439. if (this.selectionObj.hasOwnProperty(objectId)) {
  13440. if (this.selectionObj[objectId] instanceof Node) {
  13441. if (this.selectionObj[objectId].clusterSize > 1) {
  13442. this.selectionObj[objectId].unselect();
  13443. this._removeFromSelection(this.selectionObj[objectId]);
  13444. }
  13445. }
  13446. }
  13447. }
  13448. if (doNotTrigger == false) {
  13449. this.emit('select', this.getSelection());
  13450. }
  13451. },
  13452. /**
  13453. * return the number of selected nodes
  13454. *
  13455. * @returns {number}
  13456. * @private
  13457. */
  13458. _getSelectedNodeCount : function() {
  13459. var count = 0;
  13460. for (var objectId in this.selectionObj) {
  13461. if (this.selectionObj.hasOwnProperty(objectId)) {
  13462. if (this.selectionObj[objectId] instanceof Node) {
  13463. count += 1;
  13464. }
  13465. }
  13466. }
  13467. return count;
  13468. },
  13469. /**
  13470. * return the number of selected nodes
  13471. *
  13472. * @returns {number}
  13473. * @private
  13474. */
  13475. _getSelectedNode : function() {
  13476. for (var objectId in this.selectionObj) {
  13477. if (this.selectionObj.hasOwnProperty(objectId)) {
  13478. if (this.selectionObj[objectId] instanceof Node) {
  13479. return this.selectionObj[objectId];
  13480. }
  13481. }
  13482. }
  13483. return null;
  13484. },
  13485. /**
  13486. * return the number of selected edges
  13487. *
  13488. * @returns {number}
  13489. * @private
  13490. */
  13491. _getSelectedEdgeCount : function() {
  13492. var count = 0;
  13493. for (var objectId in this.selectionObj) {
  13494. if (this.selectionObj.hasOwnProperty(objectId)) {
  13495. if (this.selectionObj[objectId] instanceof Edge) {
  13496. count += 1;
  13497. }
  13498. }
  13499. }
  13500. return count;
  13501. },
  13502. /**
  13503. * return the number of selected objects.
  13504. *
  13505. * @returns {number}
  13506. * @private
  13507. */
  13508. _getSelectedObjectCount : function() {
  13509. var count = 0;
  13510. for (var objectId in this.selectionObj) {
  13511. if (this.selectionObj.hasOwnProperty(objectId)) {
  13512. count += 1;
  13513. }
  13514. }
  13515. return count;
  13516. },
  13517. /**
  13518. * Check if anything is selected
  13519. *
  13520. * @returns {boolean}
  13521. * @private
  13522. */
  13523. _selectionIsEmpty : function() {
  13524. for(var objectId in this.selectionObj) {
  13525. if(this.selectionObj.hasOwnProperty(objectId)) {
  13526. return false;
  13527. }
  13528. }
  13529. return true;
  13530. },
  13531. /**
  13532. * check if one of the selected nodes is a cluster.
  13533. *
  13534. * @returns {boolean}
  13535. * @private
  13536. */
  13537. _clusterInSelection : function() {
  13538. for(var objectId in this.selectionObj) {
  13539. if(this.selectionObj.hasOwnProperty(objectId)) {
  13540. if (this.selectionObj[objectId] instanceof Node) {
  13541. if (this.selectionObj[objectId].clusterSize > 1) {
  13542. return true;
  13543. }
  13544. }
  13545. }
  13546. }
  13547. return false;
  13548. },
  13549. /**
  13550. * select the edges connected to the node that is being selected
  13551. *
  13552. * @param {Node} node
  13553. * @private
  13554. */
  13555. _selectConnectedEdges : function(node) {
  13556. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13557. var edge = node.dynamicEdges[i];
  13558. edge.select();
  13559. this._addToSelection(edge);
  13560. }
  13561. },
  13562. /**
  13563. * unselect the edges connected to the node that is being selected
  13564. *
  13565. * @param {Node} node
  13566. * @private
  13567. */
  13568. _unselectConnectedEdges : function(node) {
  13569. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13570. var edge = node.dynamicEdges[i];
  13571. edge.unselect();
  13572. this._removeFromSelection(edge);
  13573. }
  13574. },
  13575. /**
  13576. * This is called when someone clicks on a node. either select or deselect it.
  13577. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13578. *
  13579. * @param {Node || Edge} object
  13580. * @param {Boolean} append
  13581. * @param {Boolean} [doNotTrigger] | ignore trigger
  13582. * @private
  13583. */
  13584. _selectObject : function(object, append, doNotTrigger) {
  13585. if (doNotTrigger === undefined) {
  13586. doNotTrigger = false;
  13587. }
  13588. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  13589. this._unselectAll(true);
  13590. }
  13591. if (object.selected == false) {
  13592. object.select();
  13593. this._addToSelection(object);
  13594. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  13595. this._selectConnectedEdges(object);
  13596. }
  13597. }
  13598. else {
  13599. object.unselect();
  13600. this._removeFromSelection(object);
  13601. }
  13602. if (doNotTrigger == false) {
  13603. this.emit('select', this.getSelection());
  13604. }
  13605. },
  13606. /**
  13607. * handles the selection part of the touch, only for navigation controls elements;
  13608. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  13609. * This is the most responsive solution
  13610. *
  13611. * @param {Object} pointer
  13612. * @private
  13613. */
  13614. _handleTouch : function(pointer) {
  13615. },
  13616. /**
  13617. * handles the selection part of the tap;
  13618. *
  13619. * @param {Object} pointer
  13620. * @private
  13621. */
  13622. _handleTap : function(pointer) {
  13623. var node = this._getNodeAt(pointer);
  13624. if (node != null) {
  13625. this._selectObject(node,false);
  13626. }
  13627. else {
  13628. var edge = this._getEdgeAt(pointer);
  13629. if (edge != null) {
  13630. this._selectObject(edge,false);
  13631. }
  13632. else {
  13633. this._unselectAll();
  13634. }
  13635. }
  13636. this.emit("click", this.getSelection());
  13637. this._redraw();
  13638. },
  13639. /**
  13640. * handles the selection part of the double tap and opens a cluster if needed
  13641. *
  13642. * @param {Object} pointer
  13643. * @private
  13644. */
  13645. _handleDoubleTap : function(pointer) {
  13646. var node = this._getNodeAt(pointer);
  13647. if (node != null && node !== undefined) {
  13648. // we reset the areaCenter here so the opening of the node will occur
  13649. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  13650. "y" : this._canvasToY(pointer.y)};
  13651. this.openCluster(node);
  13652. }
  13653. this.emit("doubleClick", this.getSelection());
  13654. },
  13655. /**
  13656. * Handle the onHold selection part
  13657. *
  13658. * @param pointer
  13659. * @private
  13660. */
  13661. _handleOnHold : function(pointer) {
  13662. var node = this._getNodeAt(pointer);
  13663. if (node != null) {
  13664. this._selectObject(node,true);
  13665. }
  13666. else {
  13667. var edge = this._getEdgeAt(pointer);
  13668. if (edge != null) {
  13669. this._selectObject(edge,true);
  13670. }
  13671. }
  13672. this._redraw();
  13673. },
  13674. /**
  13675. * handle the onRelease event. These functions are here for the navigation controls module.
  13676. *
  13677. * @private
  13678. */
  13679. _handleOnRelease : function(pointer) {
  13680. },
  13681. /**
  13682. *
  13683. * retrieve the currently selected objects
  13684. * @return {Number[] | String[]} selection An array with the ids of the
  13685. * selected nodes.
  13686. */
  13687. getSelection : function() {
  13688. var nodeIds = this.getSelectedNodes();
  13689. var edgeIds = this.getSelectedEdges();
  13690. return {nodes:nodeIds, edges:edgeIds};
  13691. },
  13692. /**
  13693. *
  13694. * retrieve the currently selected nodes
  13695. * @return {String} selection An array with the ids of the
  13696. * selected nodes.
  13697. */
  13698. getSelectedNodes : function() {
  13699. var idArray = [];
  13700. for(var objectId in this.selectionObj) {
  13701. if(this.selectionObj.hasOwnProperty(objectId)) {
  13702. if (this.selectionObj[objectId] instanceof Node) {
  13703. idArray.push(objectId);
  13704. }
  13705. }
  13706. }
  13707. return idArray
  13708. },
  13709. /**
  13710. *
  13711. * retrieve the currently selected edges
  13712. * @return {Array} selection An array with the ids of the
  13713. * selected nodes.
  13714. */
  13715. getSelectedEdges : function() {
  13716. var idArray = [];
  13717. for(var objectId in this.selectionObj) {
  13718. if(this.selectionObj.hasOwnProperty(objectId)) {
  13719. if (this.selectionObj[objectId] instanceof Edge) {
  13720. idArray.push(objectId);
  13721. }
  13722. }
  13723. }
  13724. return idArray
  13725. },
  13726. /**
  13727. * select zero or more nodes
  13728. * @param {Number[] | String[]} selection An array with the ids of the
  13729. * selected nodes.
  13730. */
  13731. setSelection : function(selection) {
  13732. var i, iMax, id;
  13733. if (!selection || (selection.length == undefined))
  13734. throw 'Selection must be an array with ids';
  13735. // first unselect any selected node
  13736. this._unselectAll(true);
  13737. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13738. id = selection[i];
  13739. var node = this.nodes[id];
  13740. if (!node) {
  13741. throw new RangeError('Node with id "' + id + '" not found');
  13742. }
  13743. this._selectObject(node,true,true);
  13744. }
  13745. this.redraw();
  13746. },
  13747. /**
  13748. * Validate the selection: remove ids of nodes which no longer exist
  13749. * @private
  13750. */
  13751. _updateSelection : function () {
  13752. for(var objectId in this.selectionObj) {
  13753. if(this.selectionObj.hasOwnProperty(objectId)) {
  13754. if (this.selectionObj[objectId] instanceof Node) {
  13755. if (!this.nodes.hasOwnProperty(objectId)) {
  13756. delete this.selectionObj[objectId];
  13757. }
  13758. }
  13759. else { // assuming only edges and nodes are selected
  13760. if (!this.edges.hasOwnProperty(objectId)) {
  13761. delete this.selectionObj[objectId];
  13762. }
  13763. }
  13764. }
  13765. }
  13766. }
  13767. };
  13768. /**
  13769. * Created by Alex on 1/22/14.
  13770. */
  13771. var NavigationMixin = {
  13772. _cleanNavigation : function() {
  13773. // clean up previosu navigation items
  13774. var wrapper = document.getElementById('graph-navigation_wrapper');
  13775. if (wrapper != null) {
  13776. this.containerElement.removeChild(wrapper);
  13777. }
  13778. document.onmouseup = null;
  13779. },
  13780. /**
  13781. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13782. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13783. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13784. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13785. *
  13786. * @private
  13787. */
  13788. _loadNavigationElements : function() {
  13789. this._cleanNavigation();
  13790. this.navigationDivs = {};
  13791. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13792. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13793. this.navigationDivs['wrapper'] = document.createElement('div');
  13794. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13795. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13796. for (var i = 0; i < navigationDivs.length; i++) {
  13797. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13798. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13799. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13800. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13801. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13802. }
  13803. document.onmouseup = this._stopMovement.bind(this);
  13804. },
  13805. /**
  13806. * this stops all movement induced by the navigation buttons
  13807. *
  13808. * @private
  13809. */
  13810. _stopMovement : function() {
  13811. this._xStopMoving();
  13812. this._yStopMoving();
  13813. this._stopZoom();
  13814. },
  13815. /**
  13816. * stops the actions performed by page up and down etc.
  13817. *
  13818. * @param event
  13819. * @private
  13820. */
  13821. _preventDefault : function(event) {
  13822. if (event !== undefined) {
  13823. if (event.preventDefault) {
  13824. event.preventDefault();
  13825. } else {
  13826. event.returnValue = false;
  13827. }
  13828. }
  13829. },
  13830. /**
  13831. * move the screen up
  13832. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13833. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13834. * To avoid this behaviour, we do the translation in the start loop.
  13835. *
  13836. * @private
  13837. */
  13838. _moveUp : function(event) {
  13839. console.log("here")
  13840. this.yIncrement = this.constants.keyboard.speed.y;
  13841. this.start(); // if there is no node movement, the calculation wont be done
  13842. this._preventDefault(event);
  13843. },
  13844. /**
  13845. * move the screen down
  13846. * @private
  13847. */
  13848. _moveDown : function(event) {
  13849. this.yIncrement = -this.constants.keyboard.speed.y;
  13850. this.start(); // if there is no node movement, the calculation wont be done
  13851. this._preventDefault(event);
  13852. },
  13853. /**
  13854. * move the screen left
  13855. * @private
  13856. */
  13857. _moveLeft : function(event) {
  13858. this.xIncrement = this.constants.keyboard.speed.x;
  13859. this.start(); // if there is no node movement, the calculation wont be done
  13860. this._preventDefault(event);
  13861. },
  13862. /**
  13863. * move the screen right
  13864. * @private
  13865. */
  13866. _moveRight : function(event) {
  13867. this.xIncrement = -this.constants.keyboard.speed.y;
  13868. this.start(); // if there is no node movement, the calculation wont be done
  13869. this._preventDefault(event);
  13870. },
  13871. /**
  13872. * Zoom in, using the same method as the movement.
  13873. * @private
  13874. */
  13875. _zoomIn : function(event) {
  13876. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13877. this.start(); // if there is no node movement, the calculation wont be done
  13878. this._preventDefault(event);
  13879. },
  13880. /**
  13881. * Zoom out
  13882. * @private
  13883. */
  13884. _zoomOut : function() {
  13885. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13886. this.start(); // if there is no node movement, the calculation wont be done
  13887. this._preventDefault(event);
  13888. },
  13889. /**
  13890. * Stop zooming and unhighlight the zoom controls
  13891. * @private
  13892. */
  13893. _stopZoom : function() {
  13894. this.zoomIncrement = 0;
  13895. },
  13896. /**
  13897. * Stop moving in the Y direction and unHighlight the up and down
  13898. * @private
  13899. */
  13900. _yStopMoving : function() {
  13901. this.yIncrement = 0;
  13902. },
  13903. /**
  13904. * Stop moving in the X direction and unHighlight left and right.
  13905. * @private
  13906. */
  13907. _xStopMoving : function() {
  13908. this.xIncrement = 0;
  13909. }
  13910. };
  13911. /**
  13912. * Created by Alex on 2/10/14.
  13913. */
  13914. var graphMixinLoaders = {
  13915. /**
  13916. * Load a mixin into the graph object
  13917. *
  13918. * @param {Object} sourceVariable | this object has to contain functions.
  13919. * @private
  13920. */
  13921. _loadMixin : function(sourceVariable) {
  13922. for (var mixinFunction in sourceVariable) {
  13923. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13924. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13925. }
  13926. }
  13927. },
  13928. /**
  13929. * removes a mixin from the graph object.
  13930. *
  13931. * @param {Object} sourceVariable | this object has to contain functions.
  13932. * @private
  13933. */
  13934. _clearMixin : function(sourceVariable) {
  13935. for (var mixinFunction in sourceVariable) {
  13936. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13937. Graph.prototype[mixinFunction] = undefined;
  13938. }
  13939. }
  13940. },
  13941. /**
  13942. * Mixin the physics system and initialize the parameters required.
  13943. *
  13944. * @private
  13945. */
  13946. _loadPhysicsSystem : function() {
  13947. this._loadMixin(physicsMixin);
  13948. this._loadSelectedForceSolver();
  13949. if (this.constants.configurePhysics == true) {
  13950. this._loadPhysicsConfiguration();
  13951. }
  13952. },
  13953. /**
  13954. * Mixin the cluster system and initialize the parameters required.
  13955. *
  13956. * @private
  13957. */
  13958. _loadClusterSystem : function() {
  13959. this.clusterSession = 0;
  13960. this.hubThreshold = 5;
  13961. this._loadMixin(ClusterMixin);
  13962. },
  13963. /**
  13964. * Mixin the sector system and initialize the parameters required
  13965. *
  13966. * @private
  13967. */
  13968. _loadSectorSystem : function() {
  13969. this.sectors = { },
  13970. this.activeSector = ["default"];
  13971. this.sectors["active"] = { },
  13972. this.sectors["active"]["default"] = {"nodes":{},
  13973. "edges":{},
  13974. "nodeIndices":[],
  13975. "formationScale": 1.0,
  13976. "drawingNode": undefined };
  13977. this.sectors["frozen"] = {},
  13978. this.sectors["support"] = {"nodes":{},
  13979. "edges":{},
  13980. "nodeIndices":[],
  13981. "formationScale": 1.0,
  13982. "drawingNode": undefined };
  13983. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13984. this._loadMixin(SectorMixin);
  13985. },
  13986. /**
  13987. * Mixin the selection system and initialize the parameters required
  13988. *
  13989. * @private
  13990. */
  13991. _loadSelectionSystem : function() {
  13992. this.selectionObj = { };
  13993. this._loadMixin(SelectionMixin);
  13994. },
  13995. /**
  13996. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13997. *
  13998. * @private
  13999. */
  14000. _loadManipulationSystem : function() {
  14001. // reset global variables -- these are used by the selection of nodes and edges.
  14002. this.blockConnectingEdgeSelection = false;
  14003. this.forceAppendSelection = false
  14004. if (this.constants.dataManipulation.enabled == true) {
  14005. // load the manipulator HTML elements. All styling done in css.
  14006. if (this.manipulationDiv === undefined) {
  14007. this.manipulationDiv = document.createElement('div');
  14008. this.manipulationDiv.className = 'graph-manipulationDiv';
  14009. this.manipulationDiv.id = 'graph-manipulationDiv';
  14010. if (this.editMode == true) {
  14011. this.manipulationDiv.style.display = "block";
  14012. }
  14013. else {
  14014. this.manipulationDiv.style.display = "none";
  14015. }
  14016. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  14017. }
  14018. if (this.editModeDiv === undefined) {
  14019. this.editModeDiv = document.createElement('div');
  14020. this.editModeDiv.className = 'graph-manipulation-editMode';
  14021. this.editModeDiv.id = 'graph-manipulation-editMode';
  14022. if (this.editMode == true) {
  14023. this.editModeDiv.style.display = "none";
  14024. }
  14025. else {
  14026. this.editModeDiv.style.display = "block";
  14027. }
  14028. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  14029. }
  14030. if (this.closeDiv === undefined) {
  14031. this.closeDiv = document.createElement('div');
  14032. this.closeDiv.className = 'graph-manipulation-closeDiv';
  14033. this.closeDiv.id = 'graph-manipulation-closeDiv';
  14034. this.closeDiv.style.display = this.manipulationDiv.style.display;
  14035. this.containerElement.insertBefore(this.closeDiv, this.frame);
  14036. }
  14037. // load the manipulation functions
  14038. this._loadMixin(manipulationMixin);
  14039. // create the manipulator toolbar
  14040. this._createManipulatorBar();
  14041. }
  14042. else {
  14043. if (this.manipulationDiv !== undefined) {
  14044. // removes all the bindings and overloads
  14045. this._createManipulatorBar();
  14046. // remove the manipulation divs
  14047. this.containerElement.removeChild(this.manipulationDiv);
  14048. this.containerElement.removeChild(this.editModeDiv);
  14049. this.containerElement.removeChild(this.closeDiv);
  14050. this.manipulationDiv = undefined;
  14051. this.editModeDiv = undefined;
  14052. this.closeDiv = undefined;
  14053. // remove the mixin functions
  14054. this._clearMixin(manipulationMixin);
  14055. }
  14056. }
  14057. },
  14058. /**
  14059. * Mixin the navigation (User Interface) system and initialize the parameters required
  14060. *
  14061. * @private
  14062. */
  14063. _loadNavigationControls : function() {
  14064. this._loadMixin(NavigationMixin);
  14065. // the clean function removes the button divs, this is done to remove the bindings.
  14066. this._cleanNavigation();
  14067. if (this.constants.navigation.enabled == true) {
  14068. this._loadNavigationElements();
  14069. }
  14070. },
  14071. /**
  14072. * Mixin the hierarchical layout system.
  14073. *
  14074. * @private
  14075. */
  14076. _loadHierarchySystem : function() {
  14077. this._loadMixin(HierarchicalLayoutMixin);
  14078. }
  14079. }
  14080. /**
  14081. * @constructor Graph
  14082. * Create a graph visualization, displaying nodes and edges.
  14083. *
  14084. * @param {Element} container The DOM element in which the Graph will
  14085. * be created. Normally a div element.
  14086. * @param {Object} data An object containing parameters
  14087. * {Array} nodes
  14088. * {Array} edges
  14089. * @param {Object} options Options
  14090. */
  14091. function Graph (container, data, options) {
  14092. this._initializeMixinLoaders();
  14093. // create variables and set default values
  14094. this.containerElement = container;
  14095. this.width = '100%';
  14096. this.height = '100%';
  14097. // render and calculation settings
  14098. this.renderRefreshRate = 60; // hz (fps)
  14099. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  14100. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  14101. this.maxRenderSteps = 3; // max amount of physics ticks per render step.
  14102. this.stabilize = true; // stabilize before displaying the graph
  14103. this.selectable = true;
  14104. // these functions are triggered when the dataset is edited
  14105. this.triggerFunctions = {add:null,edit:null,connect:null,delete:null};
  14106. // set constant values
  14107. this.constants = {
  14108. nodes: {
  14109. radiusMin: 5,
  14110. radiusMax: 20,
  14111. radius: 5,
  14112. shape: 'ellipse',
  14113. image: undefined,
  14114. widthMin: 16, // px
  14115. widthMax: 64, // px
  14116. fixed: false,
  14117. fontColor: 'black',
  14118. fontSize: 14, // px
  14119. fontFace: 'verdana',
  14120. level: -1,
  14121. color: {
  14122. border: '#2B7CE9',
  14123. background: '#97C2FC',
  14124. highlight: {
  14125. border: '#2B7CE9',
  14126. background: '#D2E5FF'
  14127. }
  14128. },
  14129. borderColor: '#2B7CE9',
  14130. backgroundColor: '#97C2FC',
  14131. highlightColor: '#D2E5FF',
  14132. group: undefined
  14133. },
  14134. edges: {
  14135. widthMin: 1,
  14136. widthMax: 15,
  14137. width: 1,
  14138. style: 'line',
  14139. color: '#848484',
  14140. fontColor: '#343434',
  14141. fontSize: 14, // px
  14142. fontFace: 'arial',
  14143. dash: {
  14144. length: 10,
  14145. gap: 5,
  14146. altLength: undefined
  14147. }
  14148. },
  14149. configurePhysics:false,
  14150. physics: {
  14151. barnesHut: {
  14152. enabled: true,
  14153. theta: 1 / 0.6, // inverted to save time during calculation
  14154. gravitationalConstant: -2000,
  14155. centralGravity: 0.3,
  14156. springLength: 100,
  14157. springConstant: 0.05,
  14158. damping: 0.09
  14159. },
  14160. repulsion: {
  14161. centralGravity: 0.1,
  14162. springLength: 200,
  14163. springConstant: 0.05,
  14164. nodeDistance: 100,
  14165. damping: 0.09
  14166. },
  14167. hierarchicalRepulsion: {
  14168. enabled: false,
  14169. centralGravity: 0.0,
  14170. springLength: 100,
  14171. springConstant: 0.01,
  14172. nodeDistance: 60,
  14173. damping: 0.09
  14174. },
  14175. damping: null,
  14176. centralGravity: null,
  14177. springLength: null,
  14178. springConstant: null
  14179. },
  14180. clustering: { // Per Node in Cluster = PNiC
  14181. enabled: false, // (Boolean) | global on/off switch for clustering.
  14182. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  14183. 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
  14184. 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
  14185. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  14186. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  14187. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  14188. 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.
  14189. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  14190. maxFontSize: 1000,
  14191. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  14192. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  14193. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  14194. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  14195. height: 1, // (px PNiC) | growth of the height per node in cluster.
  14196. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  14197. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  14198. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  14199. clusterLevelDifference: 2
  14200. },
  14201. navigation: {
  14202. enabled: false
  14203. },
  14204. keyboard: {
  14205. enabled: false,
  14206. speed: {x: 10, y: 10, zoom: 0.02}
  14207. },
  14208. dataManipulation: {
  14209. enabled: false,
  14210. initiallyVisible: false
  14211. },
  14212. hierarchicalLayout: {
  14213. enabled:false,
  14214. levelSeparation: 150,
  14215. nodeSpacing: 100,
  14216. direction: "UD" // UD, DU, LR, RL
  14217. },
  14218. smoothCurves: true,
  14219. maxVelocity: 10,
  14220. minVelocity: 0.1, // px/s
  14221. maxIterations: 1000 // maximum number of iteration to stabilize
  14222. };
  14223. this.editMode = this.constants.dataManipulation.initiallyVisible;
  14224. // Node variables
  14225. var graph = this;
  14226. this.groups = new Groups(); // object with groups
  14227. this.images = new Images(); // object with images
  14228. this.images.setOnloadCallback(function () {
  14229. graph._redraw();
  14230. });
  14231. // keyboard navigation variables
  14232. this.xIncrement = 0;
  14233. this.yIncrement = 0;
  14234. this.zoomIncrement = 0;
  14235. // loading all the mixins:
  14236. // load the force calculation functions, grouped under the physics system.
  14237. this._loadPhysicsSystem();
  14238. // create a frame and canvas
  14239. this._create();
  14240. // load the sector system. (mandatory, fully integrated with Graph)
  14241. this._loadSectorSystem();
  14242. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  14243. this._loadClusterSystem();
  14244. // load the selection system. (mandatory, required by Graph)
  14245. this._loadSelectionSystem();
  14246. // load the selection system. (mandatory, required by Graph)
  14247. this._loadHierarchySystem();
  14248. // apply options
  14249. this.setOptions(options);
  14250. // other vars
  14251. this.freezeSimulation = false;// freeze the simulation
  14252. this.cachedFunctions = {};
  14253. // containers for nodes and edges
  14254. this.calculationNodes = {};
  14255. this.calculationNodeIndices = [];
  14256. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  14257. this.nodes = {}; // object with Node objects
  14258. this.edges = {}; // object with Edge objects
  14259. // position and scale variables and objects
  14260. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  14261. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14262. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14263. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  14264. this.scale = 1; // defining the global scale variable in the constructor
  14265. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  14266. // datasets or dataviews
  14267. this.nodesData = null; // A DataSet or DataView
  14268. this.edgesData = null; // A DataSet or DataView
  14269. // create event listeners used to subscribe on the DataSets of the nodes and edges
  14270. this.nodesListeners = {
  14271. 'add': function (event, params) {
  14272. graph._addNodes(params.items);
  14273. graph.start();
  14274. },
  14275. 'update': function (event, params) {
  14276. graph._updateNodes(params.items);
  14277. graph.start();
  14278. },
  14279. 'remove': function (event, params) {
  14280. graph._removeNodes(params.items);
  14281. graph.start();
  14282. }
  14283. };
  14284. this.edgesListeners = {
  14285. 'add': function (event, params) {
  14286. graph._addEdges(params.items);
  14287. graph.start();
  14288. },
  14289. 'update': function (event, params) {
  14290. graph._updateEdges(params.items);
  14291. graph.start();
  14292. },
  14293. 'remove': function (event, params) {
  14294. graph._removeEdges(params.items);
  14295. graph.start();
  14296. }
  14297. };
  14298. // properties for the animation
  14299. this.moving = true;
  14300. this.timer = undefined; // Scheduling function. Is definded in this.start();
  14301. // load data (the disable start variable will be the same as the enabled clustering)
  14302. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  14303. // hierarchical layout
  14304. if (this.constants.hierarchicalLayout.enabled == true) {
  14305. this._setupHierarchicalLayout();
  14306. }
  14307. else {
  14308. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  14309. if (this.stabilize == false) {
  14310. this.zoomExtent(true,this.constants.clustering.enabled);
  14311. }
  14312. }
  14313. // if clustering is disabled, the simulation will have started in the setData function
  14314. if (this.constants.clustering.enabled) {
  14315. this.startWithClustering();
  14316. }
  14317. }
  14318. // Extend Graph with an Emitter mixin
  14319. Emitter(Graph.prototype);
  14320. /**
  14321. * Get the script path where the vis.js library is located
  14322. *
  14323. * @returns {string | null} path Path or null when not found. Path does not
  14324. * end with a slash.
  14325. * @private
  14326. */
  14327. Graph.prototype._getScriptPath = function() {
  14328. var scripts = document.getElementsByTagName( 'script' );
  14329. // find script named vis.js or vis.min.js
  14330. for (var i = 0; i < scripts.length; i++) {
  14331. var src = scripts[i].src;
  14332. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  14333. if (match) {
  14334. // return path without the script name
  14335. return src.substring(0, src.length - match[0].length);
  14336. }
  14337. }
  14338. return null;
  14339. };
  14340. /**
  14341. * Find the center position of the graph
  14342. * @private
  14343. */
  14344. Graph.prototype._getRange = function() {
  14345. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  14346. for (var nodeId in this.nodes) {
  14347. if (this.nodes.hasOwnProperty(nodeId)) {
  14348. node = this.nodes[nodeId];
  14349. if (minX > (node.x)) {minX = node.x;}
  14350. if (maxX < (node.x)) {maxX = node.x;}
  14351. if (minY > (node.y)) {minY = node.y;}
  14352. if (maxY < (node.y)) {maxY = node.y;}
  14353. }
  14354. }
  14355. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14356. };
  14357. /**
  14358. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14359. * @returns {{x: number, y: number}}
  14360. * @private
  14361. */
  14362. Graph.prototype._findCenter = function(range) {
  14363. return {x: (0.5 * (range.maxX + range.minX)),
  14364. y: (0.5 * (range.maxY + range.minY))};
  14365. };
  14366. /**
  14367. * center the graph
  14368. *
  14369. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14370. */
  14371. Graph.prototype._centerGraph = function(range) {
  14372. var center = this._findCenter(range);
  14373. center.x *= this.scale;
  14374. center.y *= this.scale;
  14375. center.x -= 0.5 * this.frame.canvas.clientWidth;
  14376. center.y -= 0.5 * this.frame.canvas.clientHeight;
  14377. this._setTranslation(-center.x,-center.y); // set at 0,0
  14378. };
  14379. /**
  14380. * This function zooms out to fit all data on screen based on amount of nodes
  14381. *
  14382. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  14383. */
  14384. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  14385. if (initialZoom === undefined) {
  14386. initialZoom = false;
  14387. }
  14388. if (disableStart === undefined) {
  14389. disableStart = false;
  14390. }
  14391. var range = this._getRange();
  14392. var zoomLevel;
  14393. if (initialZoom == true) {
  14394. var numberOfNodes = this.nodeIndices.length;
  14395. if (this.constants.smoothCurves == true) {
  14396. if (this.constants.clustering.enabled == true &&
  14397. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14398. 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.
  14399. }
  14400. else {
  14401. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14402. }
  14403. }
  14404. else {
  14405. if (this.constants.clustering.enabled == true &&
  14406. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14407. 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.
  14408. }
  14409. else {
  14410. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14411. }
  14412. }
  14413. // correct for larger canvasses.
  14414. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  14415. zoomLevel *= factor;
  14416. }
  14417. else {
  14418. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  14419. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  14420. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  14421. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  14422. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  14423. }
  14424. if (zoomLevel > 1.0) {
  14425. zoomLevel = 1.0;
  14426. }
  14427. this.pinch.mousewheelScale = zoomLevel;
  14428. this._setScale(zoomLevel);
  14429. this._centerGraph(range);
  14430. if (disableStart == false) {
  14431. this.moving = true;
  14432. this.start();
  14433. }
  14434. };
  14435. /**
  14436. * Update the this.nodeIndices with the most recent node index list
  14437. * @private
  14438. */
  14439. Graph.prototype._updateNodeIndexList = function() {
  14440. this._clearNodeIndexList();
  14441. for (var idx in this.nodes) {
  14442. if (this.nodes.hasOwnProperty(idx)) {
  14443. this.nodeIndices.push(idx);
  14444. }
  14445. }
  14446. };
  14447. /**
  14448. * Set nodes and edges, and optionally options as well.
  14449. *
  14450. * @param {Object} data Object containing parameters:
  14451. * {Array | DataSet | DataView} [nodes] Array with nodes
  14452. * {Array | DataSet | DataView} [edges] Array with edges
  14453. * {String} [dot] String containing data in DOT format
  14454. * {Options} [options] Object with options
  14455. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  14456. */
  14457. Graph.prototype.setData = function(data, disableStart) {
  14458. if (disableStart === undefined) {
  14459. disableStart = false;
  14460. }
  14461. if (data && data.dot && (data.nodes || data.edges)) {
  14462. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14463. ' parameter pair "nodes" and "edges", but not both.');
  14464. }
  14465. // set options
  14466. this.setOptions(data && data.options);
  14467. // set all data
  14468. if (data && data.dot) {
  14469. // parse DOT file
  14470. if(data && data.dot) {
  14471. var dotData = vis.util.DOTToGraph(data.dot);
  14472. this.setData(dotData);
  14473. return;
  14474. }
  14475. }
  14476. else {
  14477. this._setNodes(data && data.nodes);
  14478. this._setEdges(data && data.edges);
  14479. }
  14480. this._putDataInSector();
  14481. if (!disableStart) {
  14482. // find a stable position or start animating to a stable position
  14483. if (this.stabilize) {
  14484. this._doStabilize();
  14485. }
  14486. this.start();
  14487. }
  14488. };
  14489. /**
  14490. * Set options
  14491. * @param {Object} options
  14492. */
  14493. Graph.prototype.setOptions = function (options) {
  14494. if (options) {
  14495. var prop;
  14496. // retrieve parameter values
  14497. if (options.width !== undefined) {this.width = options.width;}
  14498. if (options.height !== undefined) {this.height = options.height;}
  14499. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14500. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14501. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  14502. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  14503. if (options.onAdd) {
  14504. this.triggerFunctions.add = options.onAdd;
  14505. }
  14506. if (options.onEdit) {
  14507. this.triggerFunctions.edit = options.onEdit;
  14508. }
  14509. if (options.onConnect) {
  14510. this.triggerFunctions.connect = options.onConnect;
  14511. }
  14512. if (options.onDelete) {
  14513. this.triggerFunctions.delete = options.onDelete;
  14514. }
  14515. if (options.physics) {
  14516. if (options.physics.barnesHut) {
  14517. this.constants.physics.barnesHut.enabled = true;
  14518. for (prop in options.physics.barnesHut) {
  14519. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14520. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14521. }
  14522. }
  14523. }
  14524. if (options.physics.repulsion) {
  14525. this.constants.physics.barnesHut.enabled = false;
  14526. for (prop in options.physics.repulsion) {
  14527. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14528. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14529. }
  14530. }
  14531. }
  14532. }
  14533. if (options.hierarchicalLayout) {
  14534. this.constants.hierarchicalLayout.enabled = true;
  14535. for (prop in options.hierarchicalLayout) {
  14536. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14537. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14538. }
  14539. }
  14540. }
  14541. else if (options.hierarchicalLayout !== undefined) {
  14542. this.constants.hierarchicalLayout.enabled = false;
  14543. }
  14544. if (options.clustering) {
  14545. this.constants.clustering.enabled = true;
  14546. for (prop in options.clustering) {
  14547. if (options.clustering.hasOwnProperty(prop)) {
  14548. this.constants.clustering[prop] = options.clustering[prop];
  14549. }
  14550. }
  14551. }
  14552. else if (options.clustering !== undefined) {
  14553. this.constants.clustering.enabled = false;
  14554. }
  14555. if (options.navigation) {
  14556. this.constants.navigation.enabled = true;
  14557. for (prop in options.navigation) {
  14558. if (options.navigation.hasOwnProperty(prop)) {
  14559. this.constants.navigation[prop] = options.navigation[prop];
  14560. }
  14561. }
  14562. }
  14563. else if (options.navigation !== undefined) {
  14564. this.constants.navigation.enabled = false;
  14565. }
  14566. if (options.keyboard) {
  14567. this.constants.keyboard.enabled = true;
  14568. for (prop in options.keyboard) {
  14569. if (options.keyboard.hasOwnProperty(prop)) {
  14570. this.constants.keyboard[prop] = options.keyboard[prop];
  14571. }
  14572. }
  14573. }
  14574. else if (options.keyboard !== undefined) {
  14575. this.constants.keyboard.enabled = false;
  14576. }
  14577. if (options.dataManipulation) {
  14578. this.constants.dataManipulation.enabled = true;
  14579. for (prop in options.dataManipulation) {
  14580. if (options.dataManipulation.hasOwnProperty(prop)) {
  14581. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14582. }
  14583. }
  14584. }
  14585. else if (options.dataManipulation !== undefined) {
  14586. this.constants.dataManipulation.enabled = false;
  14587. }
  14588. // TODO: work out these options and document them
  14589. if (options.edges) {
  14590. for (prop in options.edges) {
  14591. if (options.edges.hasOwnProperty(prop)) {
  14592. this.constants.edges[prop] = options.edges[prop];
  14593. }
  14594. }
  14595. if (!options.edges.fontColor) {
  14596. this.constants.edges.fontColor = options.edges.color;
  14597. }
  14598. // Added to support dashed lines
  14599. // David Jordan
  14600. // 2012-08-08
  14601. if (options.edges.dash) {
  14602. if (options.edges.dash.length !== undefined) {
  14603. this.constants.edges.dash.length = options.edges.dash.length;
  14604. }
  14605. if (options.edges.dash.gap !== undefined) {
  14606. this.constants.edges.dash.gap = options.edges.dash.gap;
  14607. }
  14608. if (options.edges.dash.altLength !== undefined) {
  14609. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14610. }
  14611. }
  14612. }
  14613. if (options.nodes) {
  14614. for (prop in options.nodes) {
  14615. if (options.nodes.hasOwnProperty(prop)) {
  14616. this.constants.nodes[prop] = options.nodes[prop];
  14617. }
  14618. }
  14619. if (options.nodes.color) {
  14620. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  14621. }
  14622. /*
  14623. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14624. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14625. */
  14626. }
  14627. if (options.groups) {
  14628. for (var groupname in options.groups) {
  14629. if (options.groups.hasOwnProperty(groupname)) {
  14630. var group = options.groups[groupname];
  14631. this.groups.add(groupname, group);
  14632. }
  14633. }
  14634. }
  14635. }
  14636. // (Re)loading the mixins that can be enabled or disabled in the options.
  14637. // load the force calculation functions, grouped under the physics system.
  14638. this._loadPhysicsSystem();
  14639. // load the navigation system.
  14640. this._loadNavigationControls();
  14641. // load the data manipulation system
  14642. this._loadManipulationSystem();
  14643. // configure the smooth curves
  14644. this._configureSmoothCurves();
  14645. // bind keys. If disabled, this will not do anything;
  14646. this._createKeyBinds();
  14647. this.setSize(this.width, this.height);
  14648. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14649. this._setScale(1);
  14650. this._redraw();
  14651. };
  14652. /**
  14653. * Create the main frame for the Graph.
  14654. * This function is executed once when a Graph object is created. The frame
  14655. * contains a canvas, and this canvas contains all objects like the axis and
  14656. * nodes.
  14657. * @private
  14658. */
  14659. Graph.prototype._create = function () {
  14660. // remove all elements from the container element.
  14661. while (this.containerElement.hasChildNodes()) {
  14662. this.containerElement.removeChild(this.containerElement.firstChild);
  14663. }
  14664. this.frame = document.createElement('div');
  14665. this.frame.className = 'graph-frame';
  14666. this.frame.style.position = 'relative';
  14667. this.frame.style.overflow = 'hidden';
  14668. this.frame.style.zIndex = "1";
  14669. // create the graph canvas (HTML canvas element)
  14670. this.frame.canvas = document.createElement( 'canvas' );
  14671. this.frame.canvas.style.position = 'relative';
  14672. this.frame.appendChild(this.frame.canvas);
  14673. if (!this.frame.canvas.getContext) {
  14674. var noCanvas = document.createElement( 'DIV' );
  14675. noCanvas.style.color = 'red';
  14676. noCanvas.style.fontWeight = 'bold' ;
  14677. noCanvas.style.padding = '10px';
  14678. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14679. this.frame.canvas.appendChild(noCanvas);
  14680. }
  14681. var me = this;
  14682. this.drag = {};
  14683. this.pinch = {};
  14684. this.hammer = Hammer(this.frame.canvas, {
  14685. prevent_default: true
  14686. });
  14687. this.hammer.on('tap', me._onTap.bind(me) );
  14688. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14689. this.hammer.on('hold', me._onHold.bind(me) );
  14690. this.hammer.on('pinch', me._onPinch.bind(me) );
  14691. this.hammer.on('touch', me._onTouch.bind(me) );
  14692. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14693. this.hammer.on('drag', me._onDrag.bind(me) );
  14694. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14695. this.hammer.on('release', me._onRelease.bind(me) );
  14696. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14697. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14698. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14699. // add the frame to the container element
  14700. this.containerElement.appendChild(this.frame);
  14701. };
  14702. /**
  14703. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14704. * @private
  14705. */
  14706. Graph.prototype._createKeyBinds = function() {
  14707. var me = this;
  14708. this.mousetrap = mousetrap;
  14709. this.mousetrap.reset();
  14710. if (this.constants.keyboard.enabled == true) {
  14711. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14712. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14713. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14714. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14715. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14716. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14717. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14718. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14719. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14720. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14721. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14722. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14723. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14724. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14725. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14726. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14727. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14728. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14729. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14730. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14731. }
  14732. if (this.constants.dataManipulation.enabled == true) {
  14733. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14734. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14735. }
  14736. };
  14737. /**
  14738. * Get the pointer location from a touch location
  14739. * @param {{pageX: Number, pageY: Number}} touch
  14740. * @return {{x: Number, y: Number}} pointer
  14741. * @private
  14742. */
  14743. Graph.prototype._getPointer = function (touch) {
  14744. return {
  14745. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14746. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14747. };
  14748. };
  14749. /**
  14750. * On start of a touch gesture, store the pointer
  14751. * @param event
  14752. * @private
  14753. */
  14754. Graph.prototype._onTouch = function (event) {
  14755. this.drag.pointer = this._getPointer(event.gesture.center);
  14756. this.drag.pinched = false;
  14757. this.pinch.scale = this._getScale();
  14758. this._handleTouch(this.drag.pointer);
  14759. };
  14760. /**
  14761. * handle drag start event
  14762. * @private
  14763. */
  14764. Graph.prototype._onDragStart = function () {
  14765. this._handleDragStart();
  14766. };
  14767. /**
  14768. * This function is called by _onDragStart.
  14769. * It is separated out because we can then overload it for the datamanipulation system.
  14770. *
  14771. * @private
  14772. */
  14773. Graph.prototype._handleDragStart = function() {
  14774. var drag = this.drag;
  14775. var node = this._getNodeAt(drag.pointer);
  14776. // note: drag.pointer is set in _onTouch to get the initial touch location
  14777. drag.dragging = true;
  14778. drag.selection = [];
  14779. drag.translation = this._getTranslation();
  14780. drag.nodeId = null;
  14781. if (node != null) {
  14782. drag.nodeId = node.id;
  14783. // select the clicked node if not yet selected
  14784. if (!node.isSelected()) {
  14785. this._selectObject(node,false);
  14786. }
  14787. // create an array with the selected nodes and their original location and status
  14788. for (var objectId in this.selectionObj) {
  14789. if (this.selectionObj.hasOwnProperty(objectId)) {
  14790. var object = this.selectionObj[objectId];
  14791. if (object instanceof Node) {
  14792. var s = {
  14793. id: object.id,
  14794. node: object,
  14795. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14796. x: object.x,
  14797. y: object.y,
  14798. xFixed: object.xFixed,
  14799. yFixed: object.yFixed
  14800. };
  14801. object.xFixed = true;
  14802. object.yFixed = true;
  14803. drag.selection.push(s);
  14804. }
  14805. }
  14806. }
  14807. }
  14808. };
  14809. /**
  14810. * handle drag event
  14811. * @private
  14812. */
  14813. Graph.prototype._onDrag = function (event) {
  14814. this._handleOnDrag(event)
  14815. };
  14816. /**
  14817. * This function is called by _onDrag.
  14818. * It is separated out because we can then overload it for the datamanipulation system.
  14819. *
  14820. * @private
  14821. */
  14822. Graph.prototype._handleOnDrag = function(event) {
  14823. if (this.drag.pinched) {
  14824. return;
  14825. }
  14826. var pointer = this._getPointer(event.gesture.center);
  14827. var me = this,
  14828. drag = this.drag,
  14829. selection = drag.selection;
  14830. if (selection && selection.length) {
  14831. // calculate delta's and new location
  14832. var deltaX = pointer.x - drag.pointer.x,
  14833. deltaY = pointer.y - drag.pointer.y;
  14834. // update position of all selected nodes
  14835. selection.forEach(function (s) {
  14836. var node = s.node;
  14837. if (!s.xFixed) {
  14838. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14839. }
  14840. if (!s.yFixed) {
  14841. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14842. }
  14843. });
  14844. // start _animationStep if not yet running
  14845. if (!this.moving) {
  14846. this.moving = true;
  14847. this.start();
  14848. }
  14849. }
  14850. else {
  14851. // move the graph
  14852. var diffX = pointer.x - this.drag.pointer.x;
  14853. var diffY = pointer.y - this.drag.pointer.y;
  14854. this._setTranslation(
  14855. this.drag.translation.x + diffX,
  14856. this.drag.translation.y + diffY);
  14857. this._redraw();
  14858. this.moved = true;
  14859. }
  14860. };
  14861. /**
  14862. * handle drag start event
  14863. * @private
  14864. */
  14865. Graph.prototype._onDragEnd = function () {
  14866. this.drag.dragging = false;
  14867. var selection = this.drag.selection;
  14868. if (selection) {
  14869. selection.forEach(function (s) {
  14870. // restore original xFixed and yFixed
  14871. s.node.xFixed = s.xFixed;
  14872. s.node.yFixed = s.yFixed;
  14873. });
  14874. }
  14875. };
  14876. /**
  14877. * handle tap/click event: select/unselect a node
  14878. * @private
  14879. */
  14880. Graph.prototype._onTap = function (event) {
  14881. var pointer = this._getPointer(event.gesture.center);
  14882. this.pointerPosition = pointer;
  14883. this._handleTap(pointer);
  14884. };
  14885. /**
  14886. * handle doubletap event
  14887. * @private
  14888. */
  14889. Graph.prototype._onDoubleTap = function (event) {
  14890. var pointer = this._getPointer(event.gesture.center);
  14891. this._handleDoubleTap(pointer);
  14892. };
  14893. /**
  14894. * handle long tap event: multi select nodes
  14895. * @private
  14896. */
  14897. Graph.prototype._onHold = function (event) {
  14898. var pointer = this._getPointer(event.gesture.center);
  14899. this.pointerPosition = pointer;
  14900. this._handleOnHold(pointer);
  14901. };
  14902. /**
  14903. * handle the release of the screen
  14904. *
  14905. * @private
  14906. */
  14907. Graph.prototype._onRelease = function (event) {
  14908. var pointer = this._getPointer(event.gesture.center);
  14909. this._handleOnRelease(pointer);
  14910. };
  14911. /**
  14912. * Handle pinch event
  14913. * @param event
  14914. * @private
  14915. */
  14916. Graph.prototype._onPinch = function (event) {
  14917. var pointer = this._getPointer(event.gesture.center);
  14918. this.drag.pinched = true;
  14919. if (!('scale' in this.pinch)) {
  14920. this.pinch.scale = 1;
  14921. }
  14922. // TODO: enabled moving while pinching?
  14923. var scale = this.pinch.scale * event.gesture.scale;
  14924. this._zoom(scale, pointer)
  14925. };
  14926. /**
  14927. * Zoom the graph in or out
  14928. * @param {Number} scale a number around 1, and between 0.01 and 10
  14929. * @param {{x: Number, y: Number}} pointer Position on screen
  14930. * @return {Number} appliedScale scale is limited within the boundaries
  14931. * @private
  14932. */
  14933. Graph.prototype._zoom = function(scale, pointer) {
  14934. var scaleOld = this._getScale();
  14935. if (scale < 0.00001) {
  14936. scale = 0.00001;
  14937. }
  14938. if (scale > 10) {
  14939. scale = 10;
  14940. }
  14941. // + this.frame.canvas.clientHeight / 2
  14942. var translation = this._getTranslation();
  14943. var scaleFrac = scale / scaleOld;
  14944. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14945. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14946. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  14947. "y" : this._canvasToY(pointer.y)};
  14948. this.pinch.mousewheelScale = scale;
  14949. this._setScale(scale);
  14950. this._setTranslation(tx, ty);
  14951. this.updateClustersDefault();
  14952. this._redraw();
  14953. return scale;
  14954. };
  14955. /**
  14956. * Event handler for mouse wheel event, used to zoom the timeline
  14957. * See http://adomas.org/javascript-mouse-wheel/
  14958. * https://github.com/EightMedia/hammer.js/issues/256
  14959. * @param {MouseEvent} event
  14960. * @private
  14961. */
  14962. Graph.prototype._onMouseWheel = function(event) {
  14963. // retrieve delta
  14964. var delta = 0;
  14965. if (event.wheelDelta) { /* IE/Opera. */
  14966. delta = event.wheelDelta/120;
  14967. } else if (event.detail) { /* Mozilla case. */
  14968. // In Mozilla, sign of delta is different than in IE.
  14969. // Also, delta is multiple of 3.
  14970. delta = -event.detail/3;
  14971. }
  14972. // If delta is nonzero, handle it.
  14973. // Basically, delta is now positive if wheel was scrolled up,
  14974. // and negative, if wheel was scrolled down.
  14975. if (delta) {
  14976. if (!('mousewheelScale' in this.pinch)) {
  14977. this.pinch.mousewheelScale = 1;
  14978. }
  14979. // calculate the new scale
  14980. var scale = this.pinch.mousewheelScale;
  14981. var zoom = delta / 10;
  14982. if (delta < 0) {
  14983. zoom = zoom / (1 - zoom);
  14984. }
  14985. scale *= (1 + zoom);
  14986. // calculate the pointer location
  14987. var gesture = util.fakeGesture(this, event);
  14988. var pointer = this._getPointer(gesture.center);
  14989. // apply the new scale
  14990. this._zoom(scale, pointer);
  14991. // store the new, applied scale -- this is now done in _zoom
  14992. // this.pinch.mousewheelScale = scale;
  14993. }
  14994. // Prevent default actions caused by mouse wheel.
  14995. event.preventDefault();
  14996. };
  14997. /**
  14998. * Mouse move handler for checking whether the title moves over a node with a title.
  14999. * @param {Event} event
  15000. * @private
  15001. */
  15002. Graph.prototype._onMouseMoveTitle = function (event) {
  15003. var gesture = util.fakeGesture(this, event);
  15004. var pointer = this._getPointer(gesture.center);
  15005. // check if the previously selected node is still selected
  15006. if (this.popupNode) {
  15007. this._checkHidePopup(pointer);
  15008. }
  15009. // start a timeout that will check if the mouse is positioned above
  15010. // an element
  15011. var me = this;
  15012. var checkShow = function() {
  15013. me._checkShowPopup(pointer);
  15014. };
  15015. if (this.popupTimer) {
  15016. clearInterval(this.popupTimer); // stop any running calculationTimer
  15017. }
  15018. if (!this.drag.dragging) {
  15019. this.popupTimer = setTimeout(checkShow, 300);
  15020. }
  15021. };
  15022. /**
  15023. * Check if there is an element on the given position in the graph
  15024. * (a node or edge). If so, and if this element has a title,
  15025. * show a popup window with its title.
  15026. *
  15027. * @param {{x:Number, y:Number}} pointer
  15028. * @private
  15029. */
  15030. Graph.prototype._checkShowPopup = function (pointer) {
  15031. var obj = {
  15032. left: this._canvasToX(pointer.x),
  15033. top: this._canvasToY(pointer.y),
  15034. right: this._canvasToX(pointer.x),
  15035. bottom: this._canvasToY(pointer.y)
  15036. };
  15037. var id;
  15038. var lastPopupNode = this.popupNode;
  15039. if (this.popupNode == undefined) {
  15040. // search the nodes for overlap, select the top one in case of multiple nodes
  15041. var nodes = this.nodes;
  15042. for (id in nodes) {
  15043. if (nodes.hasOwnProperty(id)) {
  15044. var node = nodes[id];
  15045. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  15046. this.popupNode = node;
  15047. break;
  15048. }
  15049. }
  15050. }
  15051. }
  15052. if (this.popupNode === undefined) {
  15053. // search the edges for overlap
  15054. var edges = this.edges;
  15055. for (id in edges) {
  15056. if (edges.hasOwnProperty(id)) {
  15057. var edge = edges[id];
  15058. if (edge.connected && (edge.getTitle() !== undefined) &&
  15059. edge.isOverlappingWith(obj)) {
  15060. this.popupNode = edge;
  15061. break;
  15062. }
  15063. }
  15064. }
  15065. }
  15066. if (this.popupNode) {
  15067. // show popup message window
  15068. if (this.popupNode != lastPopupNode) {
  15069. var me = this;
  15070. if (!me.popup) {
  15071. me.popup = new Popup(me.frame);
  15072. }
  15073. // adjust a small offset such that the mouse cursor is located in the
  15074. // bottom left location of the popup, and you can easily move over the
  15075. // popup area
  15076. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  15077. me.popup.setText(me.popupNode.getTitle());
  15078. me.popup.show();
  15079. }
  15080. }
  15081. else {
  15082. if (this.popup) {
  15083. this.popup.hide();
  15084. }
  15085. }
  15086. };
  15087. /**
  15088. * Check if the popup must be hided, which is the case when the mouse is no
  15089. * longer hovering on the object
  15090. * @param {{x:Number, y:Number}} pointer
  15091. * @private
  15092. */
  15093. Graph.prototype._checkHidePopup = function (pointer) {
  15094. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  15095. this.popupNode = undefined;
  15096. if (this.popup) {
  15097. this.popup.hide();
  15098. }
  15099. }
  15100. };
  15101. /**
  15102. * Set a new size for the graph
  15103. * @param {string} width Width in pixels or percentage (for example '800px'
  15104. * or '50%')
  15105. * @param {string} height Height in pixels or percentage (for example '400px'
  15106. * or '30%')
  15107. */
  15108. Graph.prototype.setSize = function(width, height) {
  15109. this.frame.style.width = width;
  15110. this.frame.style.height = height;
  15111. this.frame.canvas.style.width = '100%';
  15112. this.frame.canvas.style.height = '100%';
  15113. this.frame.canvas.width = this.frame.canvas.clientWidth;
  15114. this.frame.canvas.height = this.frame.canvas.clientHeight;
  15115. if (this.manipulationDiv !== undefined) {
  15116. this.manipulationDiv.style.width = this.frame.canvas.clientWidth;
  15117. }
  15118. this.emit('frameResize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  15119. };
  15120. /**
  15121. * Set a data set with nodes for the graph
  15122. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  15123. * @private
  15124. */
  15125. Graph.prototype._setNodes = function(nodes) {
  15126. var oldNodesData = this.nodesData;
  15127. if (nodes instanceof DataSet || nodes instanceof DataView) {
  15128. this.nodesData = nodes;
  15129. }
  15130. else if (nodes instanceof Array) {
  15131. this.nodesData = new DataSet();
  15132. this.nodesData.add(nodes);
  15133. }
  15134. else if (!nodes) {
  15135. this.nodesData = new DataSet();
  15136. }
  15137. else {
  15138. throw new TypeError('Array or DataSet expected');
  15139. }
  15140. if (oldNodesData) {
  15141. // unsubscribe from old dataset
  15142. util.forEach(this.nodesListeners, function (callback, event) {
  15143. oldNodesData.off(event, callback);
  15144. });
  15145. }
  15146. // remove drawn nodes
  15147. this.nodes = {};
  15148. if (this.nodesData) {
  15149. // subscribe to new dataset
  15150. var me = this;
  15151. util.forEach(this.nodesListeners, function (callback, event) {
  15152. me.nodesData.on(event, callback);
  15153. });
  15154. // draw all new nodes
  15155. var ids = this.nodesData.getIds();
  15156. this._addNodes(ids);
  15157. }
  15158. this._updateSelection();
  15159. };
  15160. /**
  15161. * Add nodes
  15162. * @param {Number[] | String[]} ids
  15163. * @private
  15164. */
  15165. Graph.prototype._addNodes = function(ids) {
  15166. var id;
  15167. for (var i = 0, len = ids.length; i < len; i++) {
  15168. id = ids[i];
  15169. var data = this.nodesData.get(id);
  15170. var node = new Node(data, this.images, this.groups, this.constants);
  15171. this.nodes[id] = node; // note: this may replace an existing node
  15172. if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
  15173. var radius = 10 * 0.1*ids.length;
  15174. var angle = 2 * Math.PI * Math.random();
  15175. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15176. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15177. // note: no not use node.isMoving() here, as that gives the current
  15178. // velocity of the node, which is zero after creation of the node.
  15179. this.moving = true;
  15180. }
  15181. }
  15182. this._updateNodeIndexList();
  15183. this._updateCalculationNodes();
  15184. this._reconnectEdges();
  15185. this._updateValueRange(this.nodes);
  15186. this.updateLabels();
  15187. };
  15188. /**
  15189. * Update existing nodes, or create them when not yet existing
  15190. * @param {Number[] | String[]} ids
  15191. * @private
  15192. */
  15193. Graph.prototype._updateNodes = function(ids) {
  15194. var nodes = this.nodes,
  15195. nodesData = this.nodesData;
  15196. for (var i = 0, len = ids.length; i < len; i++) {
  15197. var id = ids[i];
  15198. var node = nodes[id];
  15199. var data = nodesData.get(id);
  15200. if (node) {
  15201. // update node
  15202. node.setProperties(data, this.constants);
  15203. }
  15204. else {
  15205. // create node
  15206. node = new Node(properties, this.images, this.groups, this.constants);
  15207. nodes[id] = node;
  15208. if (!node.isFixed()) {
  15209. this.moving = true;
  15210. }
  15211. }
  15212. }
  15213. this._updateNodeIndexList();
  15214. this._reconnectEdges();
  15215. this._updateValueRange(nodes);
  15216. };
  15217. /**
  15218. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  15219. * @param {Number[] | String[]} ids
  15220. * @private
  15221. */
  15222. Graph.prototype._removeNodes = function(ids) {
  15223. var nodes = this.nodes;
  15224. for (var i = 0, len = ids.length; i < len; i++) {
  15225. var id = ids[i];
  15226. delete nodes[id];
  15227. }
  15228. this._updateNodeIndexList();
  15229. this._reconnectEdges();
  15230. this._updateSelection();
  15231. this._updateValueRange(nodes);
  15232. };
  15233. /**
  15234. * Load edges by reading the data table
  15235. * @param {Array | DataSet | DataView} edges The data containing the edges.
  15236. * @private
  15237. * @private
  15238. */
  15239. Graph.prototype._setEdges = function(edges) {
  15240. var oldEdgesData = this.edgesData;
  15241. if (edges instanceof DataSet || edges instanceof DataView) {
  15242. this.edgesData = edges;
  15243. }
  15244. else if (edges instanceof Array) {
  15245. this.edgesData = new DataSet();
  15246. this.edgesData.add(edges);
  15247. }
  15248. else if (!edges) {
  15249. this.edgesData = new DataSet();
  15250. }
  15251. else {
  15252. throw new TypeError('Array or DataSet expected');
  15253. }
  15254. if (oldEdgesData) {
  15255. // unsubscribe from old dataset
  15256. util.forEach(this.edgesListeners, function (callback, event) {
  15257. oldEdgesData.off(event, callback);
  15258. });
  15259. }
  15260. // remove drawn edges
  15261. this.edges = {};
  15262. if (this.edgesData) {
  15263. // subscribe to new dataset
  15264. var me = this;
  15265. util.forEach(this.edgesListeners, function (callback, event) {
  15266. me.edgesData.on(event, callback);
  15267. });
  15268. // draw all new nodes
  15269. var ids = this.edgesData.getIds();
  15270. this._addEdges(ids);
  15271. }
  15272. this._reconnectEdges();
  15273. };
  15274. /**
  15275. * Add edges
  15276. * @param {Number[] | String[]} ids
  15277. * @private
  15278. */
  15279. Graph.prototype._addEdges = function (ids) {
  15280. var edges = this.edges,
  15281. edgesData = this.edgesData;
  15282. for (var i = 0, len = ids.length; i < len; i++) {
  15283. var id = ids[i];
  15284. var oldEdge = edges[id];
  15285. if (oldEdge) {
  15286. oldEdge.disconnect();
  15287. }
  15288. var data = edgesData.get(id, {"showInternalIds" : true});
  15289. edges[id] = new Edge(data, this, this.constants);
  15290. }
  15291. this.moving = true;
  15292. this._updateValueRange(edges);
  15293. this._createBezierNodes();
  15294. this._updateCalculationNodes();
  15295. };
  15296. /**
  15297. * Update existing edges, or create them when not yet existing
  15298. * @param {Number[] | String[]} ids
  15299. * @private
  15300. */
  15301. Graph.prototype._updateEdges = function (ids) {
  15302. var edges = this.edges,
  15303. edgesData = this.edgesData;
  15304. for (var i = 0, len = ids.length; i < len; i++) {
  15305. var id = ids[i];
  15306. var data = edgesData.get(id);
  15307. var edge = edges[id];
  15308. if (edge) {
  15309. // update edge
  15310. edge.disconnect();
  15311. edge.setProperties(data, this.constants);
  15312. edge.connect();
  15313. }
  15314. else {
  15315. // create edge
  15316. edge = new Edge(data, this, this.constants);
  15317. this.edges[id] = edge;
  15318. }
  15319. }
  15320. this._createBezierNodes();
  15321. this.moving = true;
  15322. this._updateValueRange(edges);
  15323. };
  15324. /**
  15325. * Remove existing edges. Non existing ids will be ignored
  15326. * @param {Number[] | String[]} ids
  15327. * @private
  15328. */
  15329. Graph.prototype._removeEdges = function (ids) {
  15330. var edges = this.edges;
  15331. for (var i = 0, len = ids.length; i < len; i++) {
  15332. var id = ids[i];
  15333. var edge = edges[id];
  15334. if (edge) {
  15335. if (edge.via != null) {
  15336. delete this.sectors['support']['nodes'][edge.via.id];
  15337. }
  15338. edge.disconnect();
  15339. delete edges[id];
  15340. }
  15341. }
  15342. this.moving = true;
  15343. this._updateValueRange(edges);
  15344. this._updateCalculationNodes();
  15345. };
  15346. /**
  15347. * Reconnect all edges
  15348. * @private
  15349. */
  15350. Graph.prototype._reconnectEdges = function() {
  15351. var id,
  15352. nodes = this.nodes,
  15353. edges = this.edges;
  15354. for (id in nodes) {
  15355. if (nodes.hasOwnProperty(id)) {
  15356. nodes[id].edges = [];
  15357. }
  15358. }
  15359. for (id in edges) {
  15360. if (edges.hasOwnProperty(id)) {
  15361. var edge = edges[id];
  15362. edge.from = null;
  15363. edge.to = null;
  15364. edge.connect();
  15365. }
  15366. }
  15367. };
  15368. /**
  15369. * Update the values of all object in the given array according to the current
  15370. * value range of the objects in the array.
  15371. * @param {Object} obj An object containing a set of Edges or Nodes
  15372. * The objects must have a method getValue() and
  15373. * setValueRange(min, max).
  15374. * @private
  15375. */
  15376. Graph.prototype._updateValueRange = function(obj) {
  15377. var id;
  15378. // determine the range of the objects
  15379. var valueMin = undefined;
  15380. var valueMax = undefined;
  15381. for (id in obj) {
  15382. if (obj.hasOwnProperty(id)) {
  15383. var value = obj[id].getValue();
  15384. if (value !== undefined) {
  15385. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  15386. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  15387. }
  15388. }
  15389. }
  15390. // adjust the range of all objects
  15391. if (valueMin !== undefined && valueMax !== undefined) {
  15392. for (id in obj) {
  15393. if (obj.hasOwnProperty(id)) {
  15394. obj[id].setValueRange(valueMin, valueMax);
  15395. }
  15396. }
  15397. }
  15398. };
  15399. /**
  15400. * Redraw the graph with the current data
  15401. * chart will be resized too.
  15402. */
  15403. Graph.prototype.redraw = function() {
  15404. this.setSize(this.width, this.height);
  15405. this._redraw();
  15406. };
  15407. /**
  15408. * Redraw the graph with the current data
  15409. * @private
  15410. */
  15411. Graph.prototype._redraw = function() {
  15412. var ctx = this.frame.canvas.getContext('2d');
  15413. // clear the canvas
  15414. var w = this.frame.canvas.width;
  15415. var h = this.frame.canvas.height;
  15416. ctx.clearRect(0, 0, w, h);
  15417. // set scaling and translation
  15418. ctx.save();
  15419. ctx.translate(this.translation.x, this.translation.y);
  15420. ctx.scale(this.scale, this.scale);
  15421. this.canvasTopLeft = {
  15422. "x": this._canvasToX(0),
  15423. "y": this._canvasToY(0)
  15424. };
  15425. this.canvasBottomRight = {
  15426. "x": this._canvasToX(this.frame.canvas.clientWidth),
  15427. "y": this._canvasToY(this.frame.canvas.clientHeight)
  15428. };
  15429. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15430. this._doInAllSectors("_drawEdges",ctx);
  15431. this._doInAllSectors("_drawNodes",ctx,false);
  15432. // this._doInSupportSector("_drawNodes",ctx,true);
  15433. // this._drawTree(ctx,"#F00F0F");
  15434. // restore original scaling and translation
  15435. ctx.restore();
  15436. };
  15437. /**
  15438. * Set the translation of the graph
  15439. * @param {Number} offsetX Horizontal offset
  15440. * @param {Number} offsetY Vertical offset
  15441. * @private
  15442. */
  15443. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15444. if (this.translation === undefined) {
  15445. this.translation = {
  15446. x: 0,
  15447. y: 0
  15448. };
  15449. }
  15450. if (offsetX !== undefined) {
  15451. this.translation.x = offsetX;
  15452. }
  15453. if (offsetY !== undefined) {
  15454. this.translation.y = offsetY;
  15455. }
  15456. };
  15457. /**
  15458. * Get the translation of the graph
  15459. * @return {Object} translation An object with parameters x and y, both a number
  15460. * @private
  15461. */
  15462. Graph.prototype._getTranslation = function() {
  15463. return {
  15464. x: this.translation.x,
  15465. y: this.translation.y
  15466. };
  15467. };
  15468. /**
  15469. * Scale the graph
  15470. * @param {Number} scale Scaling factor 1.0 is unscaled
  15471. * @private
  15472. */
  15473. Graph.prototype._setScale = function(scale) {
  15474. this.scale = scale;
  15475. };
  15476. /**
  15477. * Get the current scale of the graph
  15478. * @return {Number} scale Scaling factor 1.0 is unscaled
  15479. * @private
  15480. */
  15481. Graph.prototype._getScale = function() {
  15482. return this.scale;
  15483. };
  15484. /**
  15485. * Convert a horizontal point on the HTML canvas to the x-value of the model
  15486. * @param {number} x
  15487. * @returns {number}
  15488. * @private
  15489. */
  15490. Graph.prototype._canvasToX = function(x) {
  15491. return (x - this.translation.x) / this.scale;
  15492. };
  15493. /**
  15494. * Convert an x-value in the model to a horizontal point on the HTML canvas
  15495. * @param {number} x
  15496. * @returns {number}
  15497. * @private
  15498. */
  15499. Graph.prototype._xToCanvas = function(x) {
  15500. return x * this.scale + this.translation.x;
  15501. };
  15502. /**
  15503. * Convert a vertical point on the HTML canvas to the y-value of the model
  15504. * @param {number} y
  15505. * @returns {number}
  15506. * @private
  15507. */
  15508. Graph.prototype._canvasToY = function(y) {
  15509. return (y - this.translation.y) / this.scale;
  15510. };
  15511. /**
  15512. * Convert an y-value in the model to a vertical point on the HTML canvas
  15513. * @param {number} y
  15514. * @returns {number}
  15515. * @private
  15516. */
  15517. Graph.prototype._yToCanvas = function(y) {
  15518. return y * this.scale + this.translation.y ;
  15519. };
  15520. /**
  15521. * Redraw all nodes
  15522. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15523. * @param {CanvasRenderingContext2D} ctx
  15524. * @param {Boolean} [alwaysShow]
  15525. * @private
  15526. */
  15527. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15528. if (alwaysShow === undefined) {
  15529. alwaysShow = false;
  15530. }
  15531. // first draw the unselected nodes
  15532. var nodes = this.nodes;
  15533. var selected = [];
  15534. for (var id in nodes) {
  15535. if (nodes.hasOwnProperty(id)) {
  15536. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15537. if (nodes[id].isSelected()) {
  15538. selected.push(id);
  15539. }
  15540. else {
  15541. if (nodes[id].inArea() || alwaysShow) {
  15542. nodes[id].draw(ctx);
  15543. }
  15544. }
  15545. }
  15546. }
  15547. // draw the selected nodes on top
  15548. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15549. if (nodes[selected[s]].inArea() || alwaysShow) {
  15550. nodes[selected[s]].draw(ctx);
  15551. }
  15552. }
  15553. };
  15554. /**
  15555. * Redraw all edges
  15556. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15557. * @param {CanvasRenderingContext2D} ctx
  15558. * @private
  15559. */
  15560. Graph.prototype._drawEdges = function(ctx) {
  15561. var edges = this.edges;
  15562. for (var id in edges) {
  15563. if (edges.hasOwnProperty(id)) {
  15564. var edge = edges[id];
  15565. edge.setScale(this.scale);
  15566. if (edge.connected) {
  15567. edges[id].draw(ctx);
  15568. }
  15569. }
  15570. }
  15571. };
  15572. /**
  15573. * Find a stable position for all nodes
  15574. * @private
  15575. */
  15576. Graph.prototype._doStabilize = function() {
  15577. // find stable position
  15578. var count = 0;
  15579. while (this.moving && count < this.constants.maxIterations) {
  15580. this._physicsTick();
  15581. count++;
  15582. }
  15583. this.zoomExtent(false,true);
  15584. };
  15585. /**
  15586. * Check if any of the nodes is still moving
  15587. * @param {number} vmin the minimum velocity considered as 'moving'
  15588. * @return {boolean} true if moving, false if non of the nodes is moving
  15589. * @private
  15590. */
  15591. Graph.prototype._isMoving = function(vmin) {
  15592. var nodes = this.nodes;
  15593. for (var id in nodes) {
  15594. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15595. return true;
  15596. }
  15597. }
  15598. return false;
  15599. };
  15600. /**
  15601. * /**
  15602. * Perform one discrete step for all nodes
  15603. *
  15604. * @private
  15605. */
  15606. Graph.prototype._discreteStepNodes = function() {
  15607. var interval = 0.65;
  15608. var nodes = this.nodes;
  15609. var nodeId;
  15610. if (this.constants.maxVelocity > 0) {
  15611. for (nodeId in nodes) {
  15612. if (nodes.hasOwnProperty(nodeId)) {
  15613. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15614. }
  15615. }
  15616. }
  15617. else {
  15618. for (nodeId in nodes) {
  15619. if (nodes.hasOwnProperty(nodeId)) {
  15620. nodes[nodeId].discreteStep(interval);
  15621. }
  15622. }
  15623. }
  15624. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15625. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15626. this.moving = true;
  15627. }
  15628. else {
  15629. this.moving = this._isMoving(vminCorrected);
  15630. }
  15631. };
  15632. Graph.prototype._physicsTick = function() {
  15633. if (!this.freezeSimulation) {
  15634. if (this.moving) {
  15635. this._doInAllActiveSectors("_initializeForceCalculation");
  15636. if (this.constants.smoothCurves) {
  15637. this._doInSupportSector("_discreteStepNodes");
  15638. }
  15639. this._doInAllActiveSectors("_discreteStepNodes");
  15640. this._findCenter(this._getRange())
  15641. }
  15642. }
  15643. };
  15644. /**
  15645. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15646. * It reschedules itself at the beginning of the function
  15647. *
  15648. * @private
  15649. */
  15650. Graph.prototype._animationStep = function() {
  15651. // reset the timer so a new scheduled animation step can be set
  15652. this.timer = undefined;
  15653. // handle the keyboad movement
  15654. this._handleNavigation();
  15655. // this schedules a new animation step
  15656. this.start();
  15657. // start the physics simulation
  15658. var calculationTime = Date.now();
  15659. var maxSteps = 1;
  15660. this._physicsTick();
  15661. var timeRequired = Date.now() - calculationTime;
  15662. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxRenderSteps) {
  15663. this._physicsTick();
  15664. timeRequired = Date.now() - calculationTime;
  15665. maxSteps++;
  15666. }
  15667. // start the rendering process
  15668. var renderTime = Date.now();
  15669. this._redraw();
  15670. this.renderTime = Date.now() - renderTime;
  15671. };
  15672. /**
  15673. * Schedule a animation step with the refreshrate interval.
  15674. *
  15675. * @poram {Boolean} runCalculationStep
  15676. */
  15677. Graph.prototype.start = function() {
  15678. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15679. if (!this.timer) {
  15680. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15681. }
  15682. }
  15683. else {
  15684. this._redraw();
  15685. }
  15686. };
  15687. /**
  15688. * Move the graph according to the keyboard presses.
  15689. *
  15690. * @private
  15691. */
  15692. Graph.prototype._handleNavigation = function() {
  15693. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15694. var translation = this._getTranslation();
  15695. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15696. }
  15697. if (this.zoomIncrement != 0) {
  15698. var center = {
  15699. x: this.frame.canvas.clientWidth / 2,
  15700. y: this.frame.canvas.clientHeight / 2
  15701. };
  15702. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15703. }
  15704. };
  15705. /**
  15706. * Freeze the _animationStep
  15707. */
  15708. Graph.prototype.toggleFreeze = function() {
  15709. if (this.freezeSimulation == false) {
  15710. this.freezeSimulation = true;
  15711. }
  15712. else {
  15713. this.freezeSimulation = false;
  15714. this.start();
  15715. }
  15716. };
  15717. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15718. if (disableStart === undefined) {
  15719. disableStart = true;
  15720. }
  15721. if (this.constants.smoothCurves == true) {
  15722. this._createBezierNodes();
  15723. }
  15724. else {
  15725. // delete the support nodes
  15726. this.sectors['support']['nodes'] = {};
  15727. for (var edgeId in this.edges) {
  15728. if (this.edges.hasOwnProperty(edgeId)) {
  15729. this.edges[edgeId].smooth = false;
  15730. this.edges[edgeId].via = null;
  15731. }
  15732. }
  15733. }
  15734. this._updateCalculationNodes();
  15735. if (!disableStart) {
  15736. this.moving = true;
  15737. this.start();
  15738. }
  15739. };
  15740. Graph.prototype._createBezierNodes = function() {
  15741. if (this.constants.smoothCurves == true) {
  15742. for (var edgeId in this.edges) {
  15743. if (this.edges.hasOwnProperty(edgeId)) {
  15744. var edge = this.edges[edgeId];
  15745. if (edge.via == null) {
  15746. edge.smooth = true;
  15747. var nodeId = "edgeId:".concat(edge.id);
  15748. this.sectors['support']['nodes'][nodeId] = new Node(
  15749. {id:nodeId,
  15750. mass:1,
  15751. shape:'circle',
  15752. internalMultiplier:1
  15753. },{},{},this.constants);
  15754. edge.via = this.sectors['support']['nodes'][nodeId];
  15755. edge.via.parentEdgeId = edge.id;
  15756. edge.positionBezierNode();
  15757. }
  15758. }
  15759. }
  15760. }
  15761. };
  15762. Graph.prototype._initializeMixinLoaders = function () {
  15763. for (var mixinFunction in graphMixinLoaders) {
  15764. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15765. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15766. }
  15767. }
  15768. };
  15769. /**
  15770. * vis.js module exports
  15771. */
  15772. var vis = {
  15773. util: util,
  15774. Controller: Controller,
  15775. DataSet: DataSet,
  15776. DataView: DataView,
  15777. Range: Range,
  15778. Stack: Stack,
  15779. TimeStep: TimeStep,
  15780. components: {
  15781. items: {
  15782. Item: Item,
  15783. ItemBox: ItemBox,
  15784. ItemPoint: ItemPoint,
  15785. ItemRange: ItemRange
  15786. },
  15787. Component: Component,
  15788. Panel: Panel,
  15789. RootPanel: RootPanel,
  15790. ItemSet: ItemSet,
  15791. TimeAxis: TimeAxis
  15792. },
  15793. graph: {
  15794. Node: Node,
  15795. Edge: Edge,
  15796. Popup: Popup,
  15797. Groups: Groups,
  15798. Images: Images
  15799. },
  15800. Timeline: Timeline,
  15801. Graph: Graph
  15802. };
  15803. /**
  15804. * CommonJS module exports
  15805. */
  15806. if (typeof exports !== 'undefined') {
  15807. exports = vis;
  15808. }
  15809. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15810. module.exports = vis;
  15811. }
  15812. /**
  15813. * AMD module exports
  15814. */
  15815. if (typeof(define) === 'function') {
  15816. define(function () {
  15817. return vis;
  15818. });
  15819. }
  15820. /**
  15821. * Window exports
  15822. */
  15823. if (typeof window !== 'undefined') {
  15824. // attach the module to the window, load as a regular javascript file
  15825. window['vis'] = vis;
  15826. }
  15827. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  15828. /**
  15829. * Expose `Emitter`.
  15830. */
  15831. module.exports = Emitter;
  15832. /**
  15833. * Initialize a new `Emitter`.
  15834. *
  15835. * @api public
  15836. */
  15837. function Emitter(obj) {
  15838. if (obj) return mixin(obj);
  15839. };
  15840. /**
  15841. * Mixin the emitter properties.
  15842. *
  15843. * @param {Object} obj
  15844. * @return {Object}
  15845. * @api private
  15846. */
  15847. function mixin(obj) {
  15848. for (var key in Emitter.prototype) {
  15849. obj[key] = Emitter.prototype[key];
  15850. }
  15851. return obj;
  15852. }
  15853. /**
  15854. * Listen on the given `event` with `fn`.
  15855. *
  15856. * @param {String} event
  15857. * @param {Function} fn
  15858. * @return {Emitter}
  15859. * @api public
  15860. */
  15861. Emitter.prototype.on =
  15862. Emitter.prototype.addEventListener = function(event, fn){
  15863. this._callbacks = this._callbacks || {};
  15864. (this._callbacks[event] = this._callbacks[event] || [])
  15865. .push(fn);
  15866. return this;
  15867. };
  15868. /**
  15869. * Adds an `event` listener that will be invoked a single
  15870. * time then automatically removed.
  15871. *
  15872. * @param {String} event
  15873. * @param {Function} fn
  15874. * @return {Emitter}
  15875. * @api public
  15876. */
  15877. Emitter.prototype.once = function(event, fn){
  15878. var self = this;
  15879. this._callbacks = this._callbacks || {};
  15880. function on() {
  15881. self.off(event, on);
  15882. fn.apply(this, arguments);
  15883. }
  15884. on.fn = fn;
  15885. this.on(event, on);
  15886. return this;
  15887. };
  15888. /**
  15889. * Remove the given callback for `event` or all
  15890. * registered callbacks.
  15891. *
  15892. * @param {String} event
  15893. * @param {Function} fn
  15894. * @return {Emitter}
  15895. * @api public
  15896. */
  15897. Emitter.prototype.off =
  15898. Emitter.prototype.removeListener =
  15899. Emitter.prototype.removeAllListeners =
  15900. Emitter.prototype.removeEventListener = function(event, fn){
  15901. this._callbacks = this._callbacks || {};
  15902. // all
  15903. if (0 == arguments.length) {
  15904. this._callbacks = {};
  15905. return this;
  15906. }
  15907. // specific event
  15908. var callbacks = this._callbacks[event];
  15909. if (!callbacks) return this;
  15910. // remove all handlers
  15911. if (1 == arguments.length) {
  15912. delete this._callbacks[event];
  15913. return this;
  15914. }
  15915. // remove specific handler
  15916. var cb;
  15917. for (var i = 0; i < callbacks.length; i++) {
  15918. cb = callbacks[i];
  15919. if (cb === fn || cb.fn === fn) {
  15920. callbacks.splice(i, 1);
  15921. break;
  15922. }
  15923. }
  15924. return this;
  15925. };
  15926. /**
  15927. * Emit `event` with the given args.
  15928. *
  15929. * @param {String} event
  15930. * @param {Mixed} ...
  15931. * @return {Emitter}
  15932. */
  15933. Emitter.prototype.emit = function(event){
  15934. this._callbacks = this._callbacks || {};
  15935. var args = [].slice.call(arguments, 1)
  15936. , callbacks = this._callbacks[event];
  15937. if (callbacks) {
  15938. callbacks = callbacks.slice(0);
  15939. for (var i = 0, len = callbacks.length; i < len; ++i) {
  15940. callbacks[i].apply(this, args);
  15941. }
  15942. }
  15943. return this;
  15944. };
  15945. /**
  15946. * Return array of callbacks for `event`.
  15947. *
  15948. * @param {String} event
  15949. * @return {Array}
  15950. * @api public
  15951. */
  15952. Emitter.prototype.listeners = function(event){
  15953. this._callbacks = this._callbacks || {};
  15954. return this._callbacks[event] || [];
  15955. };
  15956. /**
  15957. * Check if this emitter has `event` handlers.
  15958. *
  15959. * @param {String} event
  15960. * @return {Boolean}
  15961. * @api public
  15962. */
  15963. Emitter.prototype.hasListeners = function(event){
  15964. return !! this.listeners(event).length;
  15965. };
  15966. },{}],3:[function(require,module,exports){
  15967. /*! Hammer.JS - v1.0.5 - 2013-04-07
  15968. * http://eightmedia.github.com/hammer.js
  15969. *
  15970. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  15971. * Licensed under the MIT license */
  15972. (function(window, undefined) {
  15973. 'use strict';
  15974. /**
  15975. * Hammer
  15976. * use this to create instances
  15977. * @param {HTMLElement} element
  15978. * @param {Object} options
  15979. * @returns {Hammer.Instance}
  15980. * @constructor
  15981. */
  15982. var Hammer = function(element, options) {
  15983. return new Hammer.Instance(element, options || {});
  15984. };
  15985. // default settings
  15986. Hammer.defaults = {
  15987. // add styles and attributes to the element to prevent the browser from doing
  15988. // its native behavior. this doesnt prevent the scrolling, but cancels
  15989. // the contextmenu, tap highlighting etc
  15990. // set to false to disable this
  15991. stop_browser_behavior: {
  15992. // this also triggers onselectstart=false for IE
  15993. userSelect: 'none',
  15994. // this makes the element blocking in IE10 >, you could experiment with the value
  15995. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  15996. touchAction: 'none',
  15997. touchCallout: 'none',
  15998. contentZooming: 'none',
  15999. userDrag: 'none',
  16000. tapHighlightColor: 'rgba(0,0,0,0)'
  16001. }
  16002. // more settings are defined per gesture at gestures.js
  16003. };
  16004. // detect touchevents
  16005. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  16006. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  16007. // dont use mouseevents on mobile devices
  16008. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  16009. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  16010. // eventtypes per touchevent (start, move, end)
  16011. // are filled by Hammer.event.determineEventTypes on setup
  16012. Hammer.EVENT_TYPES = {};
  16013. // direction defines
  16014. Hammer.DIRECTION_DOWN = 'down';
  16015. Hammer.DIRECTION_LEFT = 'left';
  16016. Hammer.DIRECTION_UP = 'up';
  16017. Hammer.DIRECTION_RIGHT = 'right';
  16018. // pointer type
  16019. Hammer.POINTER_MOUSE = 'mouse';
  16020. Hammer.POINTER_TOUCH = 'touch';
  16021. Hammer.POINTER_PEN = 'pen';
  16022. // touch event defines
  16023. Hammer.EVENT_START = 'start';
  16024. Hammer.EVENT_MOVE = 'move';
  16025. Hammer.EVENT_END = 'end';
  16026. // hammer document where the base events are added at
  16027. Hammer.DOCUMENT = document;
  16028. // plugins namespace
  16029. Hammer.plugins = {};
  16030. // if the window events are set...
  16031. Hammer.READY = false;
  16032. /**
  16033. * setup events to detect gestures on the document
  16034. */
  16035. function setup() {
  16036. if(Hammer.READY) {
  16037. return;
  16038. }
  16039. // find what eventtypes we add listeners to
  16040. Hammer.event.determineEventTypes();
  16041. // Register all gestures inside Hammer.gestures
  16042. for(var name in Hammer.gestures) {
  16043. if(Hammer.gestures.hasOwnProperty(name)) {
  16044. Hammer.detection.register(Hammer.gestures[name]);
  16045. }
  16046. }
  16047. // Add touch events on the document
  16048. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  16049. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  16050. // Hammer is ready...!
  16051. Hammer.READY = true;
  16052. }
  16053. /**
  16054. * create new hammer instance
  16055. * all methods should return the instance itself, so it is chainable.
  16056. * @param {HTMLElement} element
  16057. * @param {Object} [options={}]
  16058. * @returns {Hammer.Instance}
  16059. * @constructor
  16060. */
  16061. Hammer.Instance = function(element, options) {
  16062. var self = this;
  16063. // setup HammerJS window events and register all gestures
  16064. // this also sets up the default options
  16065. setup();
  16066. this.element = element;
  16067. // start/stop detection option
  16068. this.enabled = true;
  16069. // merge options
  16070. this.options = Hammer.utils.extend(
  16071. Hammer.utils.extend({}, Hammer.defaults),
  16072. options || {});
  16073. // add some css to the element to prevent the browser from doing its native behavoir
  16074. if(this.options.stop_browser_behavior) {
  16075. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  16076. }
  16077. // start detection on touchstart
  16078. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  16079. if(self.enabled) {
  16080. Hammer.detection.startDetect(self, ev);
  16081. }
  16082. });
  16083. // return instance
  16084. return this;
  16085. };
  16086. Hammer.Instance.prototype = {
  16087. /**
  16088. * bind events to the instance
  16089. * @param {String} gesture
  16090. * @param {Function} handler
  16091. * @returns {Hammer.Instance}
  16092. */
  16093. on: function onEvent(gesture, handler){
  16094. var gestures = gesture.split(' ');
  16095. for(var t=0; t<gestures.length; t++) {
  16096. this.element.addEventListener(gestures[t], handler, false);
  16097. }
  16098. return this;
  16099. },
  16100. /**
  16101. * unbind events to the instance
  16102. * @param {String} gesture
  16103. * @param {Function} handler
  16104. * @returns {Hammer.Instance}
  16105. */
  16106. off: function offEvent(gesture, handler){
  16107. var gestures = gesture.split(' ');
  16108. for(var t=0; t<gestures.length; t++) {
  16109. this.element.removeEventListener(gestures[t], handler, false);
  16110. }
  16111. return this;
  16112. },
  16113. /**
  16114. * trigger gesture event
  16115. * @param {String} gesture
  16116. * @param {Object} eventData
  16117. * @returns {Hammer.Instance}
  16118. */
  16119. trigger: function triggerEvent(gesture, eventData){
  16120. // create DOM event
  16121. var event = Hammer.DOCUMENT.createEvent('Event');
  16122. event.initEvent(gesture, true, true);
  16123. event.gesture = eventData;
  16124. // trigger on the target if it is in the instance element,
  16125. // this is for event delegation tricks
  16126. var element = this.element;
  16127. if(Hammer.utils.hasParent(eventData.target, element)) {
  16128. element = eventData.target;
  16129. }
  16130. element.dispatchEvent(event);
  16131. return this;
  16132. },
  16133. /**
  16134. * enable of disable hammer.js detection
  16135. * @param {Boolean} state
  16136. * @returns {Hammer.Instance}
  16137. */
  16138. enable: function enable(state) {
  16139. this.enabled = state;
  16140. return this;
  16141. }
  16142. };
  16143. /**
  16144. * this holds the last move event,
  16145. * used to fix empty touchend issue
  16146. * see the onTouch event for an explanation
  16147. * @type {Object}
  16148. */
  16149. var last_move_event = null;
  16150. /**
  16151. * when the mouse is hold down, this is true
  16152. * @type {Boolean}
  16153. */
  16154. var enable_detect = false;
  16155. /**
  16156. * when touch events have been fired, this is true
  16157. * @type {Boolean}
  16158. */
  16159. var touch_triggered = false;
  16160. Hammer.event = {
  16161. /**
  16162. * simple addEventListener
  16163. * @param {HTMLElement} element
  16164. * @param {String} type
  16165. * @param {Function} handler
  16166. */
  16167. bindDom: function(element, type, handler) {
  16168. var types = type.split(' ');
  16169. for(var t=0; t<types.length; t++) {
  16170. element.addEventListener(types[t], handler, false);
  16171. }
  16172. },
  16173. /**
  16174. * touch events with mouse fallback
  16175. * @param {HTMLElement} element
  16176. * @param {String} eventType like Hammer.EVENT_MOVE
  16177. * @param {Function} handler
  16178. */
  16179. onTouch: function onTouch(element, eventType, handler) {
  16180. var self = this;
  16181. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  16182. var sourceEventType = ev.type.toLowerCase();
  16183. // onmouseup, but when touchend has been fired we do nothing.
  16184. // this is for touchdevices which also fire a mouseup on touchend
  16185. if(sourceEventType.match(/mouse/) && touch_triggered) {
  16186. return;
  16187. }
  16188. // mousebutton must be down or a touch event
  16189. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  16190. sourceEventType.match(/pointerdown/) || // pointerevents touch
  16191. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  16192. ){
  16193. enable_detect = true;
  16194. }
  16195. // we are in a touch event, set the touch triggered bool to true,
  16196. // this for the conflicts that may occur on ios and android
  16197. if(sourceEventType.match(/touch|pointer/)) {
  16198. touch_triggered = true;
  16199. }
  16200. // count the total touches on the screen
  16201. var count_touches = 0;
  16202. // when touch has been triggered in this detection session
  16203. // and we are now handling a mouse event, we stop that to prevent conflicts
  16204. if(enable_detect) {
  16205. // update pointerevent
  16206. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  16207. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16208. }
  16209. // touch
  16210. else if(sourceEventType.match(/touch/)) {
  16211. count_touches = ev.touches.length;
  16212. }
  16213. // mouse
  16214. else if(!touch_triggered) {
  16215. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  16216. }
  16217. // if we are in a end event, but when we remove one touch and
  16218. // we still have enough, set eventType to move
  16219. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  16220. eventType = Hammer.EVENT_MOVE;
  16221. }
  16222. // no touches, force the end event
  16223. else if(!count_touches) {
  16224. eventType = Hammer.EVENT_END;
  16225. }
  16226. // because touchend has no touches, and we often want to use these in our gestures,
  16227. // we send the last move event as our eventData in touchend
  16228. if(!count_touches && last_move_event !== null) {
  16229. ev = last_move_event;
  16230. }
  16231. // store the last move event
  16232. else {
  16233. last_move_event = ev;
  16234. }
  16235. // trigger the handler
  16236. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  16237. // remove pointerevent from list
  16238. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  16239. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16240. }
  16241. }
  16242. //debug(sourceEventType +" "+ eventType);
  16243. // on the end we reset everything
  16244. if(!count_touches) {
  16245. last_move_event = null;
  16246. enable_detect = false;
  16247. touch_triggered = false;
  16248. Hammer.PointerEvent.reset();
  16249. }
  16250. });
  16251. },
  16252. /**
  16253. * we have different events for each device/browser
  16254. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  16255. */
  16256. determineEventTypes: function determineEventTypes() {
  16257. // determine the eventtype we want to set
  16258. var types;
  16259. // pointerEvents magic
  16260. if(Hammer.HAS_POINTEREVENTS) {
  16261. types = Hammer.PointerEvent.getEvents();
  16262. }
  16263. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  16264. else if(Hammer.NO_MOUSEEVENTS) {
  16265. types = [
  16266. 'touchstart',
  16267. 'touchmove',
  16268. 'touchend touchcancel'];
  16269. }
  16270. // for non pointer events browsers and mixed browsers,
  16271. // like chrome on windows8 touch laptop
  16272. else {
  16273. types = [
  16274. 'touchstart mousedown',
  16275. 'touchmove mousemove',
  16276. 'touchend touchcancel mouseup'];
  16277. }
  16278. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  16279. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  16280. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  16281. },
  16282. /**
  16283. * create touchlist depending on the event
  16284. * @param {Object} ev
  16285. * @param {String} eventType used by the fakemultitouch plugin
  16286. */
  16287. getTouchList: function getTouchList(ev/*, eventType*/) {
  16288. // get the fake pointerEvent touchlist
  16289. if(Hammer.HAS_POINTEREVENTS) {
  16290. return Hammer.PointerEvent.getTouchList();
  16291. }
  16292. // get the touchlist
  16293. else if(ev.touches) {
  16294. return ev.touches;
  16295. }
  16296. // make fake touchlist from mouse position
  16297. else {
  16298. return [{
  16299. identifier: 1,
  16300. pageX: ev.pageX,
  16301. pageY: ev.pageY,
  16302. target: ev.target
  16303. }];
  16304. }
  16305. },
  16306. /**
  16307. * collect event data for Hammer js
  16308. * @param {HTMLElement} element
  16309. * @param {String} eventType like Hammer.EVENT_MOVE
  16310. * @param {Object} eventData
  16311. */
  16312. collectEventData: function collectEventData(element, eventType, ev) {
  16313. var touches = this.getTouchList(ev, eventType);
  16314. // find out pointerType
  16315. var pointerType = Hammer.POINTER_TOUCH;
  16316. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  16317. pointerType = Hammer.POINTER_MOUSE;
  16318. }
  16319. return {
  16320. center : Hammer.utils.getCenter(touches),
  16321. timeStamp : new Date().getTime(),
  16322. target : ev.target,
  16323. touches : touches,
  16324. eventType : eventType,
  16325. pointerType : pointerType,
  16326. srcEvent : ev,
  16327. /**
  16328. * prevent the browser default actions
  16329. * mostly used to disable scrolling of the browser
  16330. */
  16331. preventDefault: function() {
  16332. if(this.srcEvent.preventManipulation) {
  16333. this.srcEvent.preventManipulation();
  16334. }
  16335. if(this.srcEvent.preventDefault) {
  16336. this.srcEvent.preventDefault();
  16337. }
  16338. },
  16339. /**
  16340. * stop bubbling the event up to its parents
  16341. */
  16342. stopPropagation: function() {
  16343. this.srcEvent.stopPropagation();
  16344. },
  16345. /**
  16346. * immediately stop gesture detection
  16347. * might be useful after a swipe was detected
  16348. * @return {*}
  16349. */
  16350. stopDetect: function() {
  16351. return Hammer.detection.stopDetect();
  16352. }
  16353. };
  16354. }
  16355. };
  16356. Hammer.PointerEvent = {
  16357. /**
  16358. * holds all pointers
  16359. * @type {Object}
  16360. */
  16361. pointers: {},
  16362. /**
  16363. * get a list of pointers
  16364. * @returns {Array} touchlist
  16365. */
  16366. getTouchList: function() {
  16367. var self = this;
  16368. var touchlist = [];
  16369. // we can use forEach since pointerEvents only is in IE10
  16370. Object.keys(self.pointers).sort().forEach(function(id) {
  16371. touchlist.push(self.pointers[id]);
  16372. });
  16373. return touchlist;
  16374. },
  16375. /**
  16376. * update the position of a pointer
  16377. * @param {String} type Hammer.EVENT_END
  16378. * @param {Object} pointerEvent
  16379. */
  16380. updatePointer: function(type, pointerEvent) {
  16381. if(type == Hammer.EVENT_END) {
  16382. this.pointers = {};
  16383. }
  16384. else {
  16385. pointerEvent.identifier = pointerEvent.pointerId;
  16386. this.pointers[pointerEvent.pointerId] = pointerEvent;
  16387. }
  16388. return Object.keys(this.pointers).length;
  16389. },
  16390. /**
  16391. * check if ev matches pointertype
  16392. * @param {String} pointerType Hammer.POINTER_MOUSE
  16393. * @param {PointerEvent} ev
  16394. */
  16395. matchType: function(pointerType, ev) {
  16396. if(!ev.pointerType) {
  16397. return false;
  16398. }
  16399. var types = {};
  16400. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  16401. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  16402. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  16403. return types[pointerType];
  16404. },
  16405. /**
  16406. * get events
  16407. */
  16408. getEvents: function() {
  16409. return [
  16410. 'pointerdown MSPointerDown',
  16411. 'pointermove MSPointerMove',
  16412. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  16413. ];
  16414. },
  16415. /**
  16416. * reset the list
  16417. */
  16418. reset: function() {
  16419. this.pointers = {};
  16420. }
  16421. };
  16422. Hammer.utils = {
  16423. /**
  16424. * extend method,
  16425. * also used for cloning when dest is an empty object
  16426. * @param {Object} dest
  16427. * @param {Object} src
  16428. * @parm {Boolean} merge do a merge
  16429. * @returns {Object} dest
  16430. */
  16431. extend: function extend(dest, src, merge) {
  16432. for (var key in src) {
  16433. if(dest[key] !== undefined && merge) {
  16434. continue;
  16435. }
  16436. dest[key] = src[key];
  16437. }
  16438. return dest;
  16439. },
  16440. /**
  16441. * find if a node is in the given parent
  16442. * used for event delegation tricks
  16443. * @param {HTMLElement} node
  16444. * @param {HTMLElement} parent
  16445. * @returns {boolean} has_parent
  16446. */
  16447. hasParent: function(node, parent) {
  16448. while(node){
  16449. if(node == parent) {
  16450. return true;
  16451. }
  16452. node = node.parentNode;
  16453. }
  16454. return false;
  16455. },
  16456. /**
  16457. * get the center of all the touches
  16458. * @param {Array} touches
  16459. * @returns {Object} center
  16460. */
  16461. getCenter: function getCenter(touches) {
  16462. var valuesX = [], valuesY = [];
  16463. for(var t= 0,len=touches.length; t<len; t++) {
  16464. valuesX.push(touches[t].pageX);
  16465. valuesY.push(touches[t].pageY);
  16466. }
  16467. return {
  16468. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  16469. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  16470. };
  16471. },
  16472. /**
  16473. * calculate the velocity between two points
  16474. * @param {Number} delta_time
  16475. * @param {Number} delta_x
  16476. * @param {Number} delta_y
  16477. * @returns {Object} velocity
  16478. */
  16479. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  16480. return {
  16481. x: Math.abs(delta_x / delta_time) || 0,
  16482. y: Math.abs(delta_y / delta_time) || 0
  16483. };
  16484. },
  16485. /**
  16486. * calculate the angle between two coordinates
  16487. * @param {Touch} touch1
  16488. * @param {Touch} touch2
  16489. * @returns {Number} angle
  16490. */
  16491. getAngle: function getAngle(touch1, touch2) {
  16492. var y = touch2.pageY - touch1.pageY,
  16493. x = touch2.pageX - touch1.pageX;
  16494. return Math.atan2(y, x) * 180 / Math.PI;
  16495. },
  16496. /**
  16497. * angle to direction define
  16498. * @param {Touch} touch1
  16499. * @param {Touch} touch2
  16500. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  16501. */
  16502. getDirection: function getDirection(touch1, touch2) {
  16503. var x = Math.abs(touch1.pageX - touch2.pageX),
  16504. y = Math.abs(touch1.pageY - touch2.pageY);
  16505. if(x >= y) {
  16506. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16507. }
  16508. else {
  16509. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16510. }
  16511. },
  16512. /**
  16513. * calculate the distance between two touches
  16514. * @param {Touch} touch1
  16515. * @param {Touch} touch2
  16516. * @returns {Number} distance
  16517. */
  16518. getDistance: function getDistance(touch1, touch2) {
  16519. var x = touch2.pageX - touch1.pageX,
  16520. y = touch2.pageY - touch1.pageY;
  16521. return Math.sqrt((x*x) + (y*y));
  16522. },
  16523. /**
  16524. * calculate the scale factor between two touchLists (fingers)
  16525. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  16526. * @param {Array} start
  16527. * @param {Array} end
  16528. * @returns {Number} scale
  16529. */
  16530. getScale: function getScale(start, end) {
  16531. // need two fingers...
  16532. if(start.length >= 2 && end.length >= 2) {
  16533. return this.getDistance(end[0], end[1]) /
  16534. this.getDistance(start[0], start[1]);
  16535. }
  16536. return 1;
  16537. },
  16538. /**
  16539. * calculate the rotation degrees between two touchLists (fingers)
  16540. * @param {Array} start
  16541. * @param {Array} end
  16542. * @returns {Number} rotation
  16543. */
  16544. getRotation: function getRotation(start, end) {
  16545. // need two fingers
  16546. if(start.length >= 2 && end.length >= 2) {
  16547. return this.getAngle(end[1], end[0]) -
  16548. this.getAngle(start[1], start[0]);
  16549. }
  16550. return 0;
  16551. },
  16552. /**
  16553. * boolean if the direction is vertical
  16554. * @param {String} direction
  16555. * @returns {Boolean} is_vertical
  16556. */
  16557. isVertical: function isVertical(direction) {
  16558. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  16559. },
  16560. /**
  16561. * stop browser default behavior with css props
  16562. * @param {HtmlElement} element
  16563. * @param {Object} css_props
  16564. */
  16565. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  16566. var prop,
  16567. vendors = ['webkit','khtml','moz','ms','o',''];
  16568. if(!css_props || !element.style) {
  16569. return;
  16570. }
  16571. // with css properties for modern browsers
  16572. for(var i = 0; i < vendors.length; i++) {
  16573. for(var p in css_props) {
  16574. if(css_props.hasOwnProperty(p)) {
  16575. prop = p;
  16576. // vender prefix at the property
  16577. if(vendors[i]) {
  16578. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  16579. }
  16580. // set the style
  16581. element.style[prop] = css_props[p];
  16582. }
  16583. }
  16584. }
  16585. // also the disable onselectstart
  16586. if(css_props.userSelect == 'none') {
  16587. element.onselectstart = function() {
  16588. return false;
  16589. };
  16590. }
  16591. }
  16592. };
  16593. Hammer.detection = {
  16594. // contains all registred Hammer.gestures in the correct order
  16595. gestures: [],
  16596. // data of the current Hammer.gesture detection session
  16597. current: null,
  16598. // the previous Hammer.gesture session data
  16599. // is a full clone of the previous gesture.current object
  16600. previous: null,
  16601. // when this becomes true, no gestures are fired
  16602. stopped: false,
  16603. /**
  16604. * start Hammer.gesture detection
  16605. * @param {Hammer.Instance} inst
  16606. * @param {Object} eventData
  16607. */
  16608. startDetect: function startDetect(inst, eventData) {
  16609. // already busy with a Hammer.gesture detection on an element
  16610. if(this.current) {
  16611. return;
  16612. }
  16613. this.stopped = false;
  16614. this.current = {
  16615. inst : inst, // reference to HammerInstance we're working for
  16616. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  16617. lastEvent : false, // last eventData
  16618. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  16619. };
  16620. this.detect(eventData);
  16621. },
  16622. /**
  16623. * Hammer.gesture detection
  16624. * @param {Object} eventData
  16625. * @param {Object} eventData
  16626. */
  16627. detect: function detect(eventData) {
  16628. if(!this.current || this.stopped) {
  16629. return;
  16630. }
  16631. // extend event data with calculations about scale, distance etc
  16632. eventData = this.extendEventData(eventData);
  16633. // instance options
  16634. var inst_options = this.current.inst.options;
  16635. // call Hammer.gesture handlers
  16636. for(var g=0,len=this.gestures.length; g<len; g++) {
  16637. var gesture = this.gestures[g];
  16638. // only when the instance options have enabled this gesture
  16639. if(!this.stopped && inst_options[gesture.name] !== false) {
  16640. // if a handler returns false, we stop with the detection
  16641. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  16642. this.stopDetect();
  16643. break;
  16644. }
  16645. }
  16646. }
  16647. // store as previous event event
  16648. if(this.current) {
  16649. this.current.lastEvent = eventData;
  16650. }
  16651. // endevent, but not the last touch, so dont stop
  16652. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  16653. this.stopDetect();
  16654. }
  16655. return eventData;
  16656. },
  16657. /**
  16658. * clear the Hammer.gesture vars
  16659. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  16660. * to stop other Hammer.gestures from being fired
  16661. */
  16662. stopDetect: function stopDetect() {
  16663. // clone current data to the store as the previous gesture
  16664. // used for the double tap gesture, since this is an other gesture detect session
  16665. this.previous = Hammer.utils.extend({}, this.current);
  16666. // reset the current
  16667. this.current = null;
  16668. // stopped!
  16669. this.stopped = true;
  16670. },
  16671. /**
  16672. * extend eventData for Hammer.gestures
  16673. * @param {Object} ev
  16674. * @returns {Object} ev
  16675. */
  16676. extendEventData: function extendEventData(ev) {
  16677. var startEv = this.current.startEvent;
  16678. // if the touches change, set the new touches over the startEvent touches
  16679. // this because touchevents don't have all the touches on touchstart, or the
  16680. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  16681. // but, sometimes it happens that both fingers are touching at the EXACT same time
  16682. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  16683. // extend 1 level deep to get the touchlist with the touch objects
  16684. startEv.touches = [];
  16685. for(var i=0,len=ev.touches.length; i<len; i++) {
  16686. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  16687. }
  16688. }
  16689. var delta_time = ev.timeStamp - startEv.timeStamp,
  16690. delta_x = ev.center.pageX - startEv.center.pageX,
  16691. delta_y = ev.center.pageY - startEv.center.pageY,
  16692. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  16693. Hammer.utils.extend(ev, {
  16694. deltaTime : delta_time,
  16695. deltaX : delta_x,
  16696. deltaY : delta_y,
  16697. velocityX : velocity.x,
  16698. velocityY : velocity.y,
  16699. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  16700. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  16701. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  16702. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  16703. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  16704. startEvent : startEv
  16705. });
  16706. return ev;
  16707. },
  16708. /**
  16709. * register new gesture
  16710. * @param {Object} gesture object, see gestures.js for documentation
  16711. * @returns {Array} gestures
  16712. */
  16713. register: function register(gesture) {
  16714. // add an enable gesture options if there is no given
  16715. var options = gesture.defaults || {};
  16716. if(options[gesture.name] === undefined) {
  16717. options[gesture.name] = true;
  16718. }
  16719. // extend Hammer default options with the Hammer.gesture options
  16720. Hammer.utils.extend(Hammer.defaults, options, true);
  16721. // set its index
  16722. gesture.index = gesture.index || 1000;
  16723. // add Hammer.gesture to the list
  16724. this.gestures.push(gesture);
  16725. // sort the list by index
  16726. this.gestures.sort(function(a, b) {
  16727. if (a.index < b.index) {
  16728. return -1;
  16729. }
  16730. if (a.index > b.index) {
  16731. return 1;
  16732. }
  16733. return 0;
  16734. });
  16735. return this.gestures;
  16736. }
  16737. };
  16738. Hammer.gestures = Hammer.gestures || {};
  16739. /**
  16740. * Custom gestures
  16741. * ==============================
  16742. *
  16743. * Gesture object
  16744. * --------------------
  16745. * The object structure of a gesture:
  16746. *
  16747. * { name: 'mygesture',
  16748. * index: 1337,
  16749. * defaults: {
  16750. * mygesture_option: true
  16751. * }
  16752. * handler: function(type, ev, inst) {
  16753. * // trigger gesture event
  16754. * inst.trigger(this.name, ev);
  16755. * }
  16756. * }
  16757. * @param {String} name
  16758. * this should be the name of the gesture, lowercase
  16759. * it is also being used to disable/enable the gesture per instance config.
  16760. *
  16761. * @param {Number} [index=1000]
  16762. * the index of the gesture, where it is going to be in the stack of gestures detection
  16763. * like when you build an gesture that depends on the drag gesture, it is a good
  16764. * idea to place it after the index of the drag gesture.
  16765. *
  16766. * @param {Object} [defaults={}]
  16767. * the default settings of the gesture. these are added to the instance settings,
  16768. * and can be overruled per instance. you can also add the name of the gesture,
  16769. * but this is also added by default (and set to true).
  16770. *
  16771. * @param {Function} handler
  16772. * this handles the gesture detection of your custom gesture and receives the
  16773. * following arguments:
  16774. *
  16775. * @param {Object} eventData
  16776. * event data containing the following properties:
  16777. * timeStamp {Number} time the event occurred
  16778. * target {HTMLElement} target element
  16779. * touches {Array} touches (fingers, pointers, mouse) on the screen
  16780. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  16781. * center {Object} center position of the touches. contains pageX and pageY
  16782. * deltaTime {Number} the total time of the touches in the screen
  16783. * deltaX {Number} the delta on x axis we haved moved
  16784. * deltaY {Number} the delta on y axis we haved moved
  16785. * velocityX {Number} the velocity on the x
  16786. * velocityY {Number} the velocity on y
  16787. * angle {Number} the angle we are moving
  16788. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  16789. * distance {Number} the distance we haved moved
  16790. * scale {Number} scaling of the touches, needs 2 touches
  16791. * rotation {Number} rotation of the touches, needs 2 touches *
  16792. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  16793. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  16794. * startEvent {Object} contains the same properties as above,
  16795. * but from the first touch. this is used to calculate
  16796. * distances, deltaTime, scaling etc
  16797. *
  16798. * @param {Hammer.Instance} inst
  16799. * the instance we are doing the detection for. you can get the options from
  16800. * the inst.options object and trigger the gesture event by calling inst.trigger
  16801. *
  16802. *
  16803. * Handle gestures
  16804. * --------------------
  16805. * inside the handler you can get/set Hammer.detection.current. This is the current
  16806. * detection session. It has the following properties
  16807. * @param {String} name
  16808. * contains the name of the gesture we have detected. it has not a real function,
  16809. * only to check in other gestures if something is detected.
  16810. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  16811. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  16812. *
  16813. * @readonly
  16814. * @param {Hammer.Instance} inst
  16815. * the instance we do the detection for
  16816. *
  16817. * @readonly
  16818. * @param {Object} startEvent
  16819. * contains the properties of the first gesture detection in this session.
  16820. * Used for calculations about timing, distance, etc.
  16821. *
  16822. * @readonly
  16823. * @param {Object} lastEvent
  16824. * contains all the properties of the last gesture detect in this session.
  16825. *
  16826. * after the gesture detection session has been completed (user has released the screen)
  16827. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  16828. * this is usefull for gestures like doubletap, where you need to know if the
  16829. * previous gesture was a tap
  16830. *
  16831. * options that have been set by the instance can be received by calling inst.options
  16832. *
  16833. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  16834. * The first param is the name of your gesture, the second the event argument
  16835. *
  16836. *
  16837. * Register gestures
  16838. * --------------------
  16839. * When an gesture is added to the Hammer.gestures object, it is auto registered
  16840. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  16841. * manually and pass your gesture object as a param
  16842. *
  16843. */
  16844. /**
  16845. * Hold
  16846. * Touch stays at the same place for x time
  16847. * @events hold
  16848. */
  16849. Hammer.gestures.Hold = {
  16850. name: 'hold',
  16851. index: 10,
  16852. defaults: {
  16853. hold_timeout : 500,
  16854. hold_threshold : 1
  16855. },
  16856. timer: null,
  16857. handler: function holdGesture(ev, inst) {
  16858. switch(ev.eventType) {
  16859. case Hammer.EVENT_START:
  16860. // clear any running timers
  16861. clearTimeout(this.timer);
  16862. // set the gesture so we can check in the timeout if it still is
  16863. Hammer.detection.current.name = this.name;
  16864. // set timer and if after the timeout it still is hold,
  16865. // we trigger the hold event
  16866. this.timer = setTimeout(function() {
  16867. if(Hammer.detection.current.name == 'hold') {
  16868. inst.trigger('hold', ev);
  16869. }
  16870. }, inst.options.hold_timeout);
  16871. break;
  16872. // when you move or end we clear the timer
  16873. case Hammer.EVENT_MOVE:
  16874. if(ev.distance > inst.options.hold_threshold) {
  16875. clearTimeout(this.timer);
  16876. }
  16877. break;
  16878. case Hammer.EVENT_END:
  16879. clearTimeout(this.timer);
  16880. break;
  16881. }
  16882. }
  16883. };
  16884. /**
  16885. * Tap/DoubleTap
  16886. * Quick touch at a place or double at the same place
  16887. * @events tap, doubletap
  16888. */
  16889. Hammer.gestures.Tap = {
  16890. name: 'tap',
  16891. index: 100,
  16892. defaults: {
  16893. tap_max_touchtime : 250,
  16894. tap_max_distance : 10,
  16895. tap_always : true,
  16896. doubletap_distance : 20,
  16897. doubletap_interval : 300
  16898. },
  16899. handler: function tapGesture(ev, inst) {
  16900. if(ev.eventType == Hammer.EVENT_END) {
  16901. // previous gesture, for the double tap since these are two different gesture detections
  16902. var prev = Hammer.detection.previous,
  16903. did_doubletap = false;
  16904. // when the touchtime is higher then the max touch time
  16905. // or when the moving distance is too much
  16906. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  16907. ev.distance > inst.options.tap_max_distance) {
  16908. return;
  16909. }
  16910. // check if double tap
  16911. if(prev && prev.name == 'tap' &&
  16912. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  16913. ev.distance < inst.options.doubletap_distance) {
  16914. inst.trigger('doubletap', ev);
  16915. did_doubletap = true;
  16916. }
  16917. // do a single tap
  16918. if(!did_doubletap || inst.options.tap_always) {
  16919. Hammer.detection.current.name = 'tap';
  16920. inst.trigger(Hammer.detection.current.name, ev);
  16921. }
  16922. }
  16923. }
  16924. };
  16925. /**
  16926. * Swipe
  16927. * triggers swipe events when the end velocity is above the threshold
  16928. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  16929. */
  16930. Hammer.gestures.Swipe = {
  16931. name: 'swipe',
  16932. index: 40,
  16933. defaults: {
  16934. // set 0 for unlimited, but this can conflict with transform
  16935. swipe_max_touches : 1,
  16936. swipe_velocity : 0.7
  16937. },
  16938. handler: function swipeGesture(ev, inst) {
  16939. if(ev.eventType == Hammer.EVENT_END) {
  16940. // max touches
  16941. if(inst.options.swipe_max_touches > 0 &&
  16942. ev.touches.length > inst.options.swipe_max_touches) {
  16943. return;
  16944. }
  16945. // when the distance we moved is too small we skip this gesture
  16946. // or we can be already in dragging
  16947. if(ev.velocityX > inst.options.swipe_velocity ||
  16948. ev.velocityY > inst.options.swipe_velocity) {
  16949. // trigger swipe events
  16950. inst.trigger(this.name, ev);
  16951. inst.trigger(this.name + ev.direction, ev);
  16952. }
  16953. }
  16954. }
  16955. };
  16956. /**
  16957. * Drag
  16958. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  16959. * moving left and right is a good practice. When all the drag events are blocking
  16960. * you disable scrolling on that area.
  16961. * @events drag, drapleft, dragright, dragup, dragdown
  16962. */
  16963. Hammer.gestures.Drag = {
  16964. name: 'drag',
  16965. index: 50,
  16966. defaults: {
  16967. drag_min_distance : 10,
  16968. // set 0 for unlimited, but this can conflict with transform
  16969. drag_max_touches : 1,
  16970. // prevent default browser behavior when dragging occurs
  16971. // be careful with it, it makes the element a blocking element
  16972. // when you are using the drag gesture, it is a good practice to set this true
  16973. drag_block_horizontal : false,
  16974. drag_block_vertical : false,
  16975. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  16976. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  16977. drag_lock_to_axis : false,
  16978. // drag lock only kicks in when distance > drag_lock_min_distance
  16979. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  16980. drag_lock_min_distance : 25
  16981. },
  16982. triggered: false,
  16983. handler: function dragGesture(ev, inst) {
  16984. // current gesture isnt drag, but dragged is true
  16985. // this means an other gesture is busy. now call dragend
  16986. if(Hammer.detection.current.name != this.name && this.triggered) {
  16987. inst.trigger(this.name +'end', ev);
  16988. this.triggered = false;
  16989. return;
  16990. }
  16991. // max touches
  16992. if(inst.options.drag_max_touches > 0 &&
  16993. ev.touches.length > inst.options.drag_max_touches) {
  16994. return;
  16995. }
  16996. switch(ev.eventType) {
  16997. case Hammer.EVENT_START:
  16998. this.triggered = false;
  16999. break;
  17000. case Hammer.EVENT_MOVE:
  17001. // when the distance we moved is too small we skip this gesture
  17002. // or we can be already in dragging
  17003. if(ev.distance < inst.options.drag_min_distance &&
  17004. Hammer.detection.current.name != this.name) {
  17005. return;
  17006. }
  17007. // we are dragging!
  17008. Hammer.detection.current.name = this.name;
  17009. // lock drag to axis?
  17010. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  17011. ev.drag_locked_to_axis = true;
  17012. }
  17013. var last_direction = Hammer.detection.current.lastEvent.direction;
  17014. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  17015. // keep direction on the axis that the drag gesture started on
  17016. if(Hammer.utils.isVertical(last_direction)) {
  17017. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  17018. }
  17019. else {
  17020. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  17021. }
  17022. }
  17023. // first time, trigger dragstart event
  17024. if(!this.triggered) {
  17025. inst.trigger(this.name +'start', ev);
  17026. this.triggered = true;
  17027. }
  17028. // trigger normal event
  17029. inst.trigger(this.name, ev);
  17030. // direction event, like dragdown
  17031. inst.trigger(this.name + ev.direction, ev);
  17032. // block the browser events
  17033. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  17034. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  17035. ev.preventDefault();
  17036. }
  17037. break;
  17038. case Hammer.EVENT_END:
  17039. // trigger dragend
  17040. if(this.triggered) {
  17041. inst.trigger(this.name +'end', ev);
  17042. }
  17043. this.triggered = false;
  17044. break;
  17045. }
  17046. }
  17047. };
  17048. /**
  17049. * Transform
  17050. * User want to scale or rotate with 2 fingers
  17051. * @events transform, pinch, pinchin, pinchout, rotate
  17052. */
  17053. Hammer.gestures.Transform = {
  17054. name: 'transform',
  17055. index: 45,
  17056. defaults: {
  17057. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  17058. transform_min_scale : 0.01,
  17059. // rotation in degrees
  17060. transform_min_rotation : 1,
  17061. // prevent default browser behavior when two touches are on the screen
  17062. // but it makes the element a blocking element
  17063. // when you are using the transform gesture, it is a good practice to set this true
  17064. transform_always_block : false
  17065. },
  17066. triggered: false,
  17067. handler: function transformGesture(ev, inst) {
  17068. // current gesture isnt drag, but dragged is true
  17069. // this means an other gesture is busy. now call dragend
  17070. if(Hammer.detection.current.name != this.name && this.triggered) {
  17071. inst.trigger(this.name +'end', ev);
  17072. this.triggered = false;
  17073. return;
  17074. }
  17075. // atleast multitouch
  17076. if(ev.touches.length < 2) {
  17077. return;
  17078. }
  17079. // prevent default when two fingers are on the screen
  17080. if(inst.options.transform_always_block) {
  17081. ev.preventDefault();
  17082. }
  17083. switch(ev.eventType) {
  17084. case Hammer.EVENT_START:
  17085. this.triggered = false;
  17086. break;
  17087. case Hammer.EVENT_MOVE:
  17088. var scale_threshold = Math.abs(1-ev.scale);
  17089. var rotation_threshold = Math.abs(ev.rotation);
  17090. // when the distance we moved is too small we skip this gesture
  17091. // or we can be already in dragging
  17092. if(scale_threshold < inst.options.transform_min_scale &&
  17093. rotation_threshold < inst.options.transform_min_rotation) {
  17094. return;
  17095. }
  17096. // we are transforming!
  17097. Hammer.detection.current.name = this.name;
  17098. // first time, trigger dragstart event
  17099. if(!this.triggered) {
  17100. inst.trigger(this.name +'start', ev);
  17101. this.triggered = true;
  17102. }
  17103. inst.trigger(this.name, ev); // basic transform event
  17104. // trigger rotate event
  17105. if(rotation_threshold > inst.options.transform_min_rotation) {
  17106. inst.trigger('rotate', ev);
  17107. }
  17108. // trigger pinch event
  17109. if(scale_threshold > inst.options.transform_min_scale) {
  17110. inst.trigger('pinch', ev);
  17111. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  17112. }
  17113. break;
  17114. case Hammer.EVENT_END:
  17115. // trigger dragend
  17116. if(this.triggered) {
  17117. inst.trigger(this.name +'end', ev);
  17118. }
  17119. this.triggered = false;
  17120. break;
  17121. }
  17122. }
  17123. };
  17124. /**
  17125. * Touch
  17126. * Called as first, tells the user has touched the screen
  17127. * @events touch
  17128. */
  17129. Hammer.gestures.Touch = {
  17130. name: 'touch',
  17131. index: -Infinity,
  17132. defaults: {
  17133. // call preventDefault at touchstart, and makes the element blocking by
  17134. // disabling the scrolling of the page, but it improves gestures like
  17135. // transforming and dragging.
  17136. // be careful with using this, it can be very annoying for users to be stuck
  17137. // on the page
  17138. prevent_default: false,
  17139. // disable mouse events, so only touch (or pen!) input triggers events
  17140. prevent_mouseevents: false
  17141. },
  17142. handler: function touchGesture(ev, inst) {
  17143. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  17144. ev.stopDetect();
  17145. return;
  17146. }
  17147. if(inst.options.prevent_default) {
  17148. ev.preventDefault();
  17149. }
  17150. if(ev.eventType == Hammer.EVENT_START) {
  17151. inst.trigger(this.name, ev);
  17152. }
  17153. }
  17154. };
  17155. /**
  17156. * Release
  17157. * Called as last, tells the user has released the screen
  17158. * @events release
  17159. */
  17160. Hammer.gestures.Release = {
  17161. name: 'release',
  17162. index: Infinity,
  17163. handler: function releaseGesture(ev, inst) {
  17164. if(ev.eventType == Hammer.EVENT_END) {
  17165. inst.trigger(this.name, ev);
  17166. }
  17167. }
  17168. };
  17169. // node export
  17170. if(typeof module === 'object' && typeof module.exports === 'object'){
  17171. module.exports = Hammer;
  17172. }
  17173. // just window export
  17174. else {
  17175. window.Hammer = Hammer;
  17176. // requireJS module definition
  17177. if(typeof window.define === 'function' && window.define.amd) {
  17178. window.define('hammer', [], function() {
  17179. return Hammer;
  17180. });
  17181. }
  17182. }
  17183. })(this);
  17184. },{}],4:[function(require,module,exports){
  17185. //! moment.js
  17186. //! version : 2.5.1
  17187. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  17188. //! license : MIT
  17189. //! momentjs.com
  17190. (function (undefined) {
  17191. /************************************
  17192. Constants
  17193. ************************************/
  17194. var moment,
  17195. VERSION = "2.5.1",
  17196. global = this,
  17197. round = Math.round,
  17198. i,
  17199. YEAR = 0,
  17200. MONTH = 1,
  17201. DATE = 2,
  17202. HOUR = 3,
  17203. MINUTE = 4,
  17204. SECOND = 5,
  17205. MILLISECOND = 6,
  17206. // internal storage for language config files
  17207. languages = {},
  17208. // moment internal properties
  17209. momentProperties = {
  17210. _isAMomentObject: null,
  17211. _i : null,
  17212. _f : null,
  17213. _l : null,
  17214. _strict : null,
  17215. _isUTC : null,
  17216. _offset : null, // optional. Combine with _isUTC
  17217. _pf : null,
  17218. _lang : null // optional
  17219. },
  17220. // check for nodeJS
  17221. hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
  17222. // ASP.NET json date format regex
  17223. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  17224. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  17225. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  17226. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  17227. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  17228. // format tokens
  17229. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  17230. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  17231. // parsing token regexes
  17232. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  17233. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  17234. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  17235. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  17236. parseTokenDigits = /\d+/, // nonzero number of digits
  17237. 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.
  17238. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  17239. parseTokenT = /T/i, // T (ISO separator)
  17240. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  17241. //strict parsing regexes
  17242. parseTokenOneDigit = /\d/, // 0 - 9
  17243. parseTokenTwoDigits = /\d\d/, // 00 - 99
  17244. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  17245. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  17246. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  17247. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  17248. // iso 8601 regex
  17249. // 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)
  17250. 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)?)?$/,
  17251. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  17252. isoDates = [
  17253. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  17254. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  17255. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  17256. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  17257. ['YYYY-DDD', /\d{4}-\d{3}/]
  17258. ],
  17259. // iso time formats and regexes
  17260. isoTimes = [
  17261. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  17262. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  17263. ['HH:mm', /(T| )\d\d:\d\d/],
  17264. ['HH', /(T| )\d\d/]
  17265. ],
  17266. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  17267. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  17268. // getter and setter names
  17269. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  17270. unitMillisecondFactors = {
  17271. 'Milliseconds' : 1,
  17272. 'Seconds' : 1e3,
  17273. 'Minutes' : 6e4,
  17274. 'Hours' : 36e5,
  17275. 'Days' : 864e5,
  17276. 'Months' : 2592e6,
  17277. 'Years' : 31536e6
  17278. },
  17279. unitAliases = {
  17280. ms : 'millisecond',
  17281. s : 'second',
  17282. m : 'minute',
  17283. h : 'hour',
  17284. d : 'day',
  17285. D : 'date',
  17286. w : 'week',
  17287. W : 'isoWeek',
  17288. M : 'month',
  17289. y : 'year',
  17290. DDD : 'dayOfYear',
  17291. e : 'weekday',
  17292. E : 'isoWeekday',
  17293. gg: 'weekYear',
  17294. GG: 'isoWeekYear'
  17295. },
  17296. camelFunctions = {
  17297. dayofyear : 'dayOfYear',
  17298. isoweekday : 'isoWeekday',
  17299. isoweek : 'isoWeek',
  17300. weekyear : 'weekYear',
  17301. isoweekyear : 'isoWeekYear'
  17302. },
  17303. // format function strings
  17304. formatFunctions = {},
  17305. // tokens to ordinalize and pad
  17306. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  17307. paddedTokens = 'M D H h m s w W'.split(' '),
  17308. formatTokenFunctions = {
  17309. M : function () {
  17310. return this.month() + 1;
  17311. },
  17312. MMM : function (format) {
  17313. return this.lang().monthsShort(this, format);
  17314. },
  17315. MMMM : function (format) {
  17316. return this.lang().months(this, format);
  17317. },
  17318. D : function () {
  17319. return this.date();
  17320. },
  17321. DDD : function () {
  17322. return this.dayOfYear();
  17323. },
  17324. d : function () {
  17325. return this.day();
  17326. },
  17327. dd : function (format) {
  17328. return this.lang().weekdaysMin(this, format);
  17329. },
  17330. ddd : function (format) {
  17331. return this.lang().weekdaysShort(this, format);
  17332. },
  17333. dddd : function (format) {
  17334. return this.lang().weekdays(this, format);
  17335. },
  17336. w : function () {
  17337. return this.week();
  17338. },
  17339. W : function () {
  17340. return this.isoWeek();
  17341. },
  17342. YY : function () {
  17343. return leftZeroFill(this.year() % 100, 2);
  17344. },
  17345. YYYY : function () {
  17346. return leftZeroFill(this.year(), 4);
  17347. },
  17348. YYYYY : function () {
  17349. return leftZeroFill(this.year(), 5);
  17350. },
  17351. YYYYYY : function () {
  17352. var y = this.year(), sign = y >= 0 ? '+' : '-';
  17353. return sign + leftZeroFill(Math.abs(y), 6);
  17354. },
  17355. gg : function () {
  17356. return leftZeroFill(this.weekYear() % 100, 2);
  17357. },
  17358. gggg : function () {
  17359. return leftZeroFill(this.weekYear(), 4);
  17360. },
  17361. ggggg : function () {
  17362. return leftZeroFill(this.weekYear(), 5);
  17363. },
  17364. GG : function () {
  17365. return leftZeroFill(this.isoWeekYear() % 100, 2);
  17366. },
  17367. GGGG : function () {
  17368. return leftZeroFill(this.isoWeekYear(), 4);
  17369. },
  17370. GGGGG : function () {
  17371. return leftZeroFill(this.isoWeekYear(), 5);
  17372. },
  17373. e : function () {
  17374. return this.weekday();
  17375. },
  17376. E : function () {
  17377. return this.isoWeekday();
  17378. },
  17379. a : function () {
  17380. return this.lang().meridiem(this.hours(), this.minutes(), true);
  17381. },
  17382. A : function () {
  17383. return this.lang().meridiem(this.hours(), this.minutes(), false);
  17384. },
  17385. H : function () {
  17386. return this.hours();
  17387. },
  17388. h : function () {
  17389. return this.hours() % 12 || 12;
  17390. },
  17391. m : function () {
  17392. return this.minutes();
  17393. },
  17394. s : function () {
  17395. return this.seconds();
  17396. },
  17397. S : function () {
  17398. return toInt(this.milliseconds() / 100);
  17399. },
  17400. SS : function () {
  17401. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  17402. },
  17403. SSS : function () {
  17404. return leftZeroFill(this.milliseconds(), 3);
  17405. },
  17406. SSSS : function () {
  17407. return leftZeroFill(this.milliseconds(), 3);
  17408. },
  17409. Z : function () {
  17410. var a = -this.zone(),
  17411. b = "+";
  17412. if (a < 0) {
  17413. a = -a;
  17414. b = "-";
  17415. }
  17416. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  17417. },
  17418. ZZ : function () {
  17419. var a = -this.zone(),
  17420. b = "+";
  17421. if (a < 0) {
  17422. a = -a;
  17423. b = "-";
  17424. }
  17425. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  17426. },
  17427. z : function () {
  17428. return this.zoneAbbr();
  17429. },
  17430. zz : function () {
  17431. return this.zoneName();
  17432. },
  17433. X : function () {
  17434. return this.unix();
  17435. },
  17436. Q : function () {
  17437. return this.quarter();
  17438. }
  17439. },
  17440. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  17441. function defaultParsingFlags() {
  17442. // We need to deep clone this object, and es5 standard is not very
  17443. // helpful.
  17444. return {
  17445. empty : false,
  17446. unusedTokens : [],
  17447. unusedInput : [],
  17448. overflow : -2,
  17449. charsLeftOver : 0,
  17450. nullInput : false,
  17451. invalidMonth : null,
  17452. invalidFormat : false,
  17453. userInvalidated : false,
  17454. iso: false
  17455. };
  17456. }
  17457. function padToken(func, count) {
  17458. return function (a) {
  17459. return leftZeroFill(func.call(this, a), count);
  17460. };
  17461. }
  17462. function ordinalizeToken(func, period) {
  17463. return function (a) {
  17464. return this.lang().ordinal(func.call(this, a), period);
  17465. };
  17466. }
  17467. while (ordinalizeTokens.length) {
  17468. i = ordinalizeTokens.pop();
  17469. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  17470. }
  17471. while (paddedTokens.length) {
  17472. i = paddedTokens.pop();
  17473. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  17474. }
  17475. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  17476. /************************************
  17477. Constructors
  17478. ************************************/
  17479. function Language() {
  17480. }
  17481. // Moment prototype object
  17482. function Moment(config) {
  17483. checkOverflow(config);
  17484. extend(this, config);
  17485. }
  17486. // Duration Constructor
  17487. function Duration(duration) {
  17488. var normalizedInput = normalizeObjectUnits(duration),
  17489. years = normalizedInput.year || 0,
  17490. months = normalizedInput.month || 0,
  17491. weeks = normalizedInput.week || 0,
  17492. days = normalizedInput.day || 0,
  17493. hours = normalizedInput.hour || 0,
  17494. minutes = normalizedInput.minute || 0,
  17495. seconds = normalizedInput.second || 0,
  17496. milliseconds = normalizedInput.millisecond || 0;
  17497. // representation for dateAddRemove
  17498. this._milliseconds = +milliseconds +
  17499. seconds * 1e3 + // 1000
  17500. minutes * 6e4 + // 1000 * 60
  17501. hours * 36e5; // 1000 * 60 * 60
  17502. // Because of dateAddRemove treats 24 hours as different from a
  17503. // day when working around DST, we need to store them separately
  17504. this._days = +days +
  17505. weeks * 7;
  17506. // It is impossible translate months into days without knowing
  17507. // which months you are are talking about, so we have to store
  17508. // it separately.
  17509. this._months = +months +
  17510. years * 12;
  17511. this._data = {};
  17512. this._bubble();
  17513. }
  17514. /************************************
  17515. Helpers
  17516. ************************************/
  17517. function extend(a, b) {
  17518. for (var i in b) {
  17519. if (b.hasOwnProperty(i)) {
  17520. a[i] = b[i];
  17521. }
  17522. }
  17523. if (b.hasOwnProperty("toString")) {
  17524. a.toString = b.toString;
  17525. }
  17526. if (b.hasOwnProperty("valueOf")) {
  17527. a.valueOf = b.valueOf;
  17528. }
  17529. return a;
  17530. }
  17531. function cloneMoment(m) {
  17532. var result = {}, i;
  17533. for (i in m) {
  17534. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  17535. result[i] = m[i];
  17536. }
  17537. }
  17538. return result;
  17539. }
  17540. function absRound(number) {
  17541. if (number < 0) {
  17542. return Math.ceil(number);
  17543. } else {
  17544. return Math.floor(number);
  17545. }
  17546. }
  17547. // left zero fill a number
  17548. // see http://jsperf.com/left-zero-filling for performance comparison
  17549. function leftZeroFill(number, targetLength, forceSign) {
  17550. var output = '' + Math.abs(number),
  17551. sign = number >= 0;
  17552. while (output.length < targetLength) {
  17553. output = '0' + output;
  17554. }
  17555. return (sign ? (forceSign ? '+' : '') : '-') + output;
  17556. }
  17557. // helper function for _.addTime and _.subtractTime
  17558. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  17559. var milliseconds = duration._milliseconds,
  17560. days = duration._days,
  17561. months = duration._months,
  17562. minutes,
  17563. hours;
  17564. if (milliseconds) {
  17565. mom._d.setTime(+mom._d + milliseconds * isAdding);
  17566. }
  17567. // store the minutes and hours so we can restore them
  17568. if (days || months) {
  17569. minutes = mom.minute();
  17570. hours = mom.hour();
  17571. }
  17572. if (days) {
  17573. mom.date(mom.date() + days * isAdding);
  17574. }
  17575. if (months) {
  17576. mom.month(mom.month() + months * isAdding);
  17577. }
  17578. if (milliseconds && !ignoreUpdateOffset) {
  17579. moment.updateOffset(mom);
  17580. }
  17581. // restore the minutes and hours after possibly changing dst
  17582. if (days || months) {
  17583. mom.minute(minutes);
  17584. mom.hour(hours);
  17585. }
  17586. }
  17587. // check if is an array
  17588. function isArray(input) {
  17589. return Object.prototype.toString.call(input) === '[object Array]';
  17590. }
  17591. function isDate(input) {
  17592. return Object.prototype.toString.call(input) === '[object Date]' ||
  17593. input instanceof Date;
  17594. }
  17595. // compare two arrays, return the number of differences
  17596. function compareArrays(array1, array2, dontConvert) {
  17597. var len = Math.min(array1.length, array2.length),
  17598. lengthDiff = Math.abs(array1.length - array2.length),
  17599. diffs = 0,
  17600. i;
  17601. for (i = 0; i < len; i++) {
  17602. if ((dontConvert && array1[i] !== array2[i]) ||
  17603. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  17604. diffs++;
  17605. }
  17606. }
  17607. return diffs + lengthDiff;
  17608. }
  17609. function normalizeUnits(units) {
  17610. if (units) {
  17611. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  17612. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  17613. }
  17614. return units;
  17615. }
  17616. function normalizeObjectUnits(inputObject) {
  17617. var normalizedInput = {},
  17618. normalizedProp,
  17619. prop;
  17620. for (prop in inputObject) {
  17621. if (inputObject.hasOwnProperty(prop)) {
  17622. normalizedProp = normalizeUnits(prop);
  17623. if (normalizedProp) {
  17624. normalizedInput[normalizedProp] = inputObject[prop];
  17625. }
  17626. }
  17627. }
  17628. return normalizedInput;
  17629. }
  17630. function makeList(field) {
  17631. var count, setter;
  17632. if (field.indexOf('week') === 0) {
  17633. count = 7;
  17634. setter = 'day';
  17635. }
  17636. else if (field.indexOf('month') === 0) {
  17637. count = 12;
  17638. setter = 'month';
  17639. }
  17640. else {
  17641. return;
  17642. }
  17643. moment[field] = function (format, index) {
  17644. var i, getter,
  17645. method = moment.fn._lang[field],
  17646. results = [];
  17647. if (typeof format === 'number') {
  17648. index = format;
  17649. format = undefined;
  17650. }
  17651. getter = function (i) {
  17652. var m = moment().utc().set(setter, i);
  17653. return method.call(moment.fn._lang, m, format || '');
  17654. };
  17655. if (index != null) {
  17656. return getter(index);
  17657. }
  17658. else {
  17659. for (i = 0; i < count; i++) {
  17660. results.push(getter(i));
  17661. }
  17662. return results;
  17663. }
  17664. };
  17665. }
  17666. function toInt(argumentForCoercion) {
  17667. var coercedNumber = +argumentForCoercion,
  17668. value = 0;
  17669. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  17670. if (coercedNumber >= 0) {
  17671. value = Math.floor(coercedNumber);
  17672. } else {
  17673. value = Math.ceil(coercedNumber);
  17674. }
  17675. }
  17676. return value;
  17677. }
  17678. function daysInMonth(year, month) {
  17679. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  17680. }
  17681. function daysInYear(year) {
  17682. return isLeapYear(year) ? 366 : 365;
  17683. }
  17684. function isLeapYear(year) {
  17685. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  17686. }
  17687. function checkOverflow(m) {
  17688. var overflow;
  17689. if (m._a && m._pf.overflow === -2) {
  17690. overflow =
  17691. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  17692. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  17693. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  17694. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  17695. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  17696. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  17697. -1;
  17698. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  17699. overflow = DATE;
  17700. }
  17701. m._pf.overflow = overflow;
  17702. }
  17703. }
  17704. function isValid(m) {
  17705. if (m._isValid == null) {
  17706. m._isValid = !isNaN(m._d.getTime()) &&
  17707. m._pf.overflow < 0 &&
  17708. !m._pf.empty &&
  17709. !m._pf.invalidMonth &&
  17710. !m._pf.nullInput &&
  17711. !m._pf.invalidFormat &&
  17712. !m._pf.userInvalidated;
  17713. if (m._strict) {
  17714. m._isValid = m._isValid &&
  17715. m._pf.charsLeftOver === 0 &&
  17716. m._pf.unusedTokens.length === 0;
  17717. }
  17718. }
  17719. return m._isValid;
  17720. }
  17721. function normalizeLanguage(key) {
  17722. return key ? key.toLowerCase().replace('_', '-') : key;
  17723. }
  17724. // Return a moment from input, that is local/utc/zone equivalent to model.
  17725. function makeAs(input, model) {
  17726. return model._isUTC ? moment(input).zone(model._offset || 0) :
  17727. moment(input).local();
  17728. }
  17729. /************************************
  17730. Languages
  17731. ************************************/
  17732. extend(Language.prototype, {
  17733. set : function (config) {
  17734. var prop, i;
  17735. for (i in config) {
  17736. prop = config[i];
  17737. if (typeof prop === 'function') {
  17738. this[i] = prop;
  17739. } else {
  17740. this['_' + i] = prop;
  17741. }
  17742. }
  17743. },
  17744. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  17745. months : function (m) {
  17746. return this._months[m.month()];
  17747. },
  17748. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  17749. monthsShort : function (m) {
  17750. return this._monthsShort[m.month()];
  17751. },
  17752. monthsParse : function (monthName) {
  17753. var i, mom, regex;
  17754. if (!this._monthsParse) {
  17755. this._monthsParse = [];
  17756. }
  17757. for (i = 0; i < 12; i++) {
  17758. // make the regex if we don't have it already
  17759. if (!this._monthsParse[i]) {
  17760. mom = moment.utc([2000, i]);
  17761. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  17762. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17763. }
  17764. // test the regex
  17765. if (this._monthsParse[i].test(monthName)) {
  17766. return i;
  17767. }
  17768. }
  17769. },
  17770. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  17771. weekdays : function (m) {
  17772. return this._weekdays[m.day()];
  17773. },
  17774. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  17775. weekdaysShort : function (m) {
  17776. return this._weekdaysShort[m.day()];
  17777. },
  17778. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  17779. weekdaysMin : function (m) {
  17780. return this._weekdaysMin[m.day()];
  17781. },
  17782. weekdaysParse : function (weekdayName) {
  17783. var i, mom, regex;
  17784. if (!this._weekdaysParse) {
  17785. this._weekdaysParse = [];
  17786. }
  17787. for (i = 0; i < 7; i++) {
  17788. // make the regex if we don't have it already
  17789. if (!this._weekdaysParse[i]) {
  17790. mom = moment([2000, 1]).day(i);
  17791. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  17792. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17793. }
  17794. // test the regex
  17795. if (this._weekdaysParse[i].test(weekdayName)) {
  17796. return i;
  17797. }
  17798. }
  17799. },
  17800. _longDateFormat : {
  17801. LT : "h:mm A",
  17802. L : "MM/DD/YYYY",
  17803. LL : "MMMM D YYYY",
  17804. LLL : "MMMM D YYYY LT",
  17805. LLLL : "dddd, MMMM D YYYY LT"
  17806. },
  17807. longDateFormat : function (key) {
  17808. var output = this._longDateFormat[key];
  17809. if (!output && this._longDateFormat[key.toUpperCase()]) {
  17810. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  17811. return val.slice(1);
  17812. });
  17813. this._longDateFormat[key] = output;
  17814. }
  17815. return output;
  17816. },
  17817. isPM : function (input) {
  17818. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  17819. // Using charAt should be more compatible.
  17820. return ((input + '').toLowerCase().charAt(0) === 'p');
  17821. },
  17822. _meridiemParse : /[ap]\.?m?\.?/i,
  17823. meridiem : function (hours, minutes, isLower) {
  17824. if (hours > 11) {
  17825. return isLower ? 'pm' : 'PM';
  17826. } else {
  17827. return isLower ? 'am' : 'AM';
  17828. }
  17829. },
  17830. _calendar : {
  17831. sameDay : '[Today at] LT',
  17832. nextDay : '[Tomorrow at] LT',
  17833. nextWeek : 'dddd [at] LT',
  17834. lastDay : '[Yesterday at] LT',
  17835. lastWeek : '[Last] dddd [at] LT',
  17836. sameElse : 'L'
  17837. },
  17838. calendar : function (key, mom) {
  17839. var output = this._calendar[key];
  17840. return typeof output === 'function' ? output.apply(mom) : output;
  17841. },
  17842. _relativeTime : {
  17843. future : "in %s",
  17844. past : "%s ago",
  17845. s : "a few seconds",
  17846. m : "a minute",
  17847. mm : "%d minutes",
  17848. h : "an hour",
  17849. hh : "%d hours",
  17850. d : "a day",
  17851. dd : "%d days",
  17852. M : "a month",
  17853. MM : "%d months",
  17854. y : "a year",
  17855. yy : "%d years"
  17856. },
  17857. relativeTime : function (number, withoutSuffix, string, isFuture) {
  17858. var output = this._relativeTime[string];
  17859. return (typeof output === 'function') ?
  17860. output(number, withoutSuffix, string, isFuture) :
  17861. output.replace(/%d/i, number);
  17862. },
  17863. pastFuture : function (diff, output) {
  17864. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  17865. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  17866. },
  17867. ordinal : function (number) {
  17868. return this._ordinal.replace("%d", number);
  17869. },
  17870. _ordinal : "%d",
  17871. preparse : function (string) {
  17872. return string;
  17873. },
  17874. postformat : function (string) {
  17875. return string;
  17876. },
  17877. week : function (mom) {
  17878. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  17879. },
  17880. _week : {
  17881. dow : 0, // Sunday is the first day of the week.
  17882. doy : 6 // The week that contains Jan 1st is the first week of the year.
  17883. },
  17884. _invalidDate: 'Invalid date',
  17885. invalidDate: function () {
  17886. return this._invalidDate;
  17887. }
  17888. });
  17889. // Loads a language definition into the `languages` cache. The function
  17890. // takes a key and optionally values. If not in the browser and no values
  17891. // are provided, it will load the language file module. As a convenience,
  17892. // this function also returns the language values.
  17893. function loadLang(key, values) {
  17894. values.abbr = key;
  17895. if (!languages[key]) {
  17896. languages[key] = new Language();
  17897. }
  17898. languages[key].set(values);
  17899. return languages[key];
  17900. }
  17901. // Remove a language from the `languages` cache. Mostly useful in tests.
  17902. function unloadLang(key) {
  17903. delete languages[key];
  17904. }
  17905. // Determines which language definition to use and returns it.
  17906. //
  17907. // With no parameters, it will return the global language. If you
  17908. // pass in a language key, such as 'en', it will return the
  17909. // definition for 'en', so long as 'en' has already been loaded using
  17910. // moment.lang.
  17911. function getLangDefinition(key) {
  17912. var i = 0, j, lang, next, split,
  17913. get = function (k) {
  17914. if (!languages[k] && hasModule) {
  17915. try {
  17916. require('./lang/' + k);
  17917. } catch (e) { }
  17918. }
  17919. return languages[k];
  17920. };
  17921. if (!key) {
  17922. return moment.fn._lang;
  17923. }
  17924. if (!isArray(key)) {
  17925. //short-circuit everything else
  17926. lang = get(key);
  17927. if (lang) {
  17928. return lang;
  17929. }
  17930. key = [key];
  17931. }
  17932. //pick the language from the array
  17933. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  17934. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  17935. while (i < key.length) {
  17936. split = normalizeLanguage(key[i]).split('-');
  17937. j = split.length;
  17938. next = normalizeLanguage(key[i + 1]);
  17939. next = next ? next.split('-') : null;
  17940. while (j > 0) {
  17941. lang = get(split.slice(0, j).join('-'));
  17942. if (lang) {
  17943. return lang;
  17944. }
  17945. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  17946. //the next array item is better than a shallower substring of this one
  17947. break;
  17948. }
  17949. j--;
  17950. }
  17951. i++;
  17952. }
  17953. return moment.fn._lang;
  17954. }
  17955. /************************************
  17956. Formatting
  17957. ************************************/
  17958. function removeFormattingTokens(input) {
  17959. if (input.match(/\[[\s\S]/)) {
  17960. return input.replace(/^\[|\]$/g, "");
  17961. }
  17962. return input.replace(/\\/g, "");
  17963. }
  17964. function makeFormatFunction(format) {
  17965. var array = format.match(formattingTokens), i, length;
  17966. for (i = 0, length = array.length; i < length; i++) {
  17967. if (formatTokenFunctions[array[i]]) {
  17968. array[i] = formatTokenFunctions[array[i]];
  17969. } else {
  17970. array[i] = removeFormattingTokens(array[i]);
  17971. }
  17972. }
  17973. return function (mom) {
  17974. var output = "";
  17975. for (i = 0; i < length; i++) {
  17976. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  17977. }
  17978. return output;
  17979. };
  17980. }
  17981. // format date using native date object
  17982. function formatMoment(m, format) {
  17983. if (!m.isValid()) {
  17984. return m.lang().invalidDate();
  17985. }
  17986. format = expandFormat(format, m.lang());
  17987. if (!formatFunctions[format]) {
  17988. formatFunctions[format] = makeFormatFunction(format);
  17989. }
  17990. return formatFunctions[format](m);
  17991. }
  17992. function expandFormat(format, lang) {
  17993. var i = 5;
  17994. function replaceLongDateFormatTokens(input) {
  17995. return lang.longDateFormat(input) || input;
  17996. }
  17997. localFormattingTokens.lastIndex = 0;
  17998. while (i >= 0 && localFormattingTokens.test(format)) {
  17999. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  18000. localFormattingTokens.lastIndex = 0;
  18001. i -= 1;
  18002. }
  18003. return format;
  18004. }
  18005. /************************************
  18006. Parsing
  18007. ************************************/
  18008. // get the regex to find the next token
  18009. function getParseRegexForToken(token, config) {
  18010. var a, strict = config._strict;
  18011. switch (token) {
  18012. case 'DDDD':
  18013. return parseTokenThreeDigits;
  18014. case 'YYYY':
  18015. case 'GGGG':
  18016. case 'gggg':
  18017. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  18018. case 'Y':
  18019. case 'G':
  18020. case 'g':
  18021. return parseTokenSignedNumber;
  18022. case 'YYYYYY':
  18023. case 'YYYYY':
  18024. case 'GGGGG':
  18025. case 'ggggg':
  18026. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  18027. case 'S':
  18028. if (strict) { return parseTokenOneDigit; }
  18029. /* falls through */
  18030. case 'SS':
  18031. if (strict) { return parseTokenTwoDigits; }
  18032. /* falls through */
  18033. case 'SSS':
  18034. if (strict) { return parseTokenThreeDigits; }
  18035. /* falls through */
  18036. case 'DDD':
  18037. return parseTokenOneToThreeDigits;
  18038. case 'MMM':
  18039. case 'MMMM':
  18040. case 'dd':
  18041. case 'ddd':
  18042. case 'dddd':
  18043. return parseTokenWord;
  18044. case 'a':
  18045. case 'A':
  18046. return getLangDefinition(config._l)._meridiemParse;
  18047. case 'X':
  18048. return parseTokenTimestampMs;
  18049. case 'Z':
  18050. case 'ZZ':
  18051. return parseTokenTimezone;
  18052. case 'T':
  18053. return parseTokenT;
  18054. case 'SSSS':
  18055. return parseTokenDigits;
  18056. case 'MM':
  18057. case 'DD':
  18058. case 'YY':
  18059. case 'GG':
  18060. case 'gg':
  18061. case 'HH':
  18062. case 'hh':
  18063. case 'mm':
  18064. case 'ss':
  18065. case 'ww':
  18066. case 'WW':
  18067. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  18068. case 'M':
  18069. case 'D':
  18070. case 'd':
  18071. case 'H':
  18072. case 'h':
  18073. case 'm':
  18074. case 's':
  18075. case 'w':
  18076. case 'W':
  18077. case 'e':
  18078. case 'E':
  18079. return parseTokenOneOrTwoDigits;
  18080. default :
  18081. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  18082. return a;
  18083. }
  18084. }
  18085. function timezoneMinutesFromString(string) {
  18086. string = string || "";
  18087. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  18088. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  18089. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  18090. minutes = +(parts[1] * 60) + toInt(parts[2]);
  18091. return parts[0] === '+' ? -minutes : minutes;
  18092. }
  18093. // function to convert string input to date
  18094. function addTimeToArrayFromToken(token, input, config) {
  18095. var a, datePartArray = config._a;
  18096. switch (token) {
  18097. // MONTH
  18098. case 'M' : // fall through to MM
  18099. case 'MM' :
  18100. if (input != null) {
  18101. datePartArray[MONTH] = toInt(input) - 1;
  18102. }
  18103. break;
  18104. case 'MMM' : // fall through to MMMM
  18105. case 'MMMM' :
  18106. a = getLangDefinition(config._l).monthsParse(input);
  18107. // if we didn't find a month name, mark the date as invalid.
  18108. if (a != null) {
  18109. datePartArray[MONTH] = a;
  18110. } else {
  18111. config._pf.invalidMonth = input;
  18112. }
  18113. break;
  18114. // DAY OF MONTH
  18115. case 'D' : // fall through to DD
  18116. case 'DD' :
  18117. if (input != null) {
  18118. datePartArray[DATE] = toInt(input);
  18119. }
  18120. break;
  18121. // DAY OF YEAR
  18122. case 'DDD' : // fall through to DDDD
  18123. case 'DDDD' :
  18124. if (input != null) {
  18125. config._dayOfYear = toInt(input);
  18126. }
  18127. break;
  18128. // YEAR
  18129. case 'YY' :
  18130. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  18131. break;
  18132. case 'YYYY' :
  18133. case 'YYYYY' :
  18134. case 'YYYYYY' :
  18135. datePartArray[YEAR] = toInt(input);
  18136. break;
  18137. // AM / PM
  18138. case 'a' : // fall through to A
  18139. case 'A' :
  18140. config._isPm = getLangDefinition(config._l).isPM(input);
  18141. break;
  18142. // 24 HOUR
  18143. case 'H' : // fall through to hh
  18144. case 'HH' : // fall through to hh
  18145. case 'h' : // fall through to hh
  18146. case 'hh' :
  18147. datePartArray[HOUR] = toInt(input);
  18148. break;
  18149. // MINUTE
  18150. case 'm' : // fall through to mm
  18151. case 'mm' :
  18152. datePartArray[MINUTE] = toInt(input);
  18153. break;
  18154. // SECOND
  18155. case 's' : // fall through to ss
  18156. case 'ss' :
  18157. datePartArray[SECOND] = toInt(input);
  18158. break;
  18159. // MILLISECOND
  18160. case 'S' :
  18161. case 'SS' :
  18162. case 'SSS' :
  18163. case 'SSSS' :
  18164. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  18165. break;
  18166. // UNIX TIMESTAMP WITH MS
  18167. case 'X':
  18168. config._d = new Date(parseFloat(input) * 1000);
  18169. break;
  18170. // TIMEZONE
  18171. case 'Z' : // fall through to ZZ
  18172. case 'ZZ' :
  18173. config._useUTC = true;
  18174. config._tzm = timezoneMinutesFromString(input);
  18175. break;
  18176. case 'w':
  18177. case 'ww':
  18178. case 'W':
  18179. case 'WW':
  18180. case 'd':
  18181. case 'dd':
  18182. case 'ddd':
  18183. case 'dddd':
  18184. case 'e':
  18185. case 'E':
  18186. token = token.substr(0, 1);
  18187. /* falls through */
  18188. case 'gg':
  18189. case 'gggg':
  18190. case 'GG':
  18191. case 'GGGG':
  18192. case 'GGGGG':
  18193. token = token.substr(0, 2);
  18194. if (input) {
  18195. config._w = config._w || {};
  18196. config._w[token] = input;
  18197. }
  18198. break;
  18199. }
  18200. }
  18201. // convert an array to a date.
  18202. // the array should mirror the parameters below
  18203. // note: all values past the year are optional and will default to the lowest possible value.
  18204. // [year, month, day , hour, minute, second, millisecond]
  18205. function dateFromConfig(config) {
  18206. var i, date, input = [], currentDate,
  18207. yearToUse, fixYear, w, temp, lang, weekday, week;
  18208. if (config._d) {
  18209. return;
  18210. }
  18211. currentDate = currentDateArray(config);
  18212. //compute day of the year from weeks and weekdays
  18213. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  18214. fixYear = function (val) {
  18215. var int_val = parseInt(val, 10);
  18216. return val ?
  18217. (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
  18218. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  18219. };
  18220. w = config._w;
  18221. if (w.GG != null || w.W != null || w.E != null) {
  18222. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  18223. }
  18224. else {
  18225. lang = getLangDefinition(config._l);
  18226. weekday = w.d != null ? parseWeekday(w.d, lang) :
  18227. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  18228. week = parseInt(w.w, 10) || 1;
  18229. //if we're parsing 'd', then the low day numbers may be next week
  18230. if (w.d != null && weekday < lang._week.dow) {
  18231. week++;
  18232. }
  18233. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  18234. }
  18235. config._a[YEAR] = temp.year;
  18236. config._dayOfYear = temp.dayOfYear;
  18237. }
  18238. //if the day of the year is set, figure out what it is
  18239. if (config._dayOfYear) {
  18240. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  18241. if (config._dayOfYear > daysInYear(yearToUse)) {
  18242. config._pf._overflowDayOfYear = true;
  18243. }
  18244. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  18245. config._a[MONTH] = date.getUTCMonth();
  18246. config._a[DATE] = date.getUTCDate();
  18247. }
  18248. // Default to current date.
  18249. // * if no year, month, day of month are given, default to today
  18250. // * if day of month is given, default month and year
  18251. // * if month is given, default only year
  18252. // * if year is given, don't default anything
  18253. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  18254. config._a[i] = input[i] = currentDate[i];
  18255. }
  18256. // Zero out whatever was not defaulted, including time
  18257. for (; i < 7; i++) {
  18258. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  18259. }
  18260. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  18261. input[HOUR] += toInt((config._tzm || 0) / 60);
  18262. input[MINUTE] += toInt((config._tzm || 0) % 60);
  18263. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  18264. }
  18265. function dateFromObject(config) {
  18266. var normalizedInput;
  18267. if (config._d) {
  18268. return;
  18269. }
  18270. normalizedInput = normalizeObjectUnits(config._i);
  18271. config._a = [
  18272. normalizedInput.year,
  18273. normalizedInput.month,
  18274. normalizedInput.day,
  18275. normalizedInput.hour,
  18276. normalizedInput.minute,
  18277. normalizedInput.second,
  18278. normalizedInput.millisecond
  18279. ];
  18280. dateFromConfig(config);
  18281. }
  18282. function currentDateArray(config) {
  18283. var now = new Date();
  18284. if (config._useUTC) {
  18285. return [
  18286. now.getUTCFullYear(),
  18287. now.getUTCMonth(),
  18288. now.getUTCDate()
  18289. ];
  18290. } else {
  18291. return [now.getFullYear(), now.getMonth(), now.getDate()];
  18292. }
  18293. }
  18294. // date from string and format string
  18295. function makeDateFromStringAndFormat(config) {
  18296. config._a = [];
  18297. config._pf.empty = true;
  18298. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  18299. var lang = getLangDefinition(config._l),
  18300. string = '' + config._i,
  18301. i, parsedInput, tokens, token, skipped,
  18302. stringLength = string.length,
  18303. totalParsedInputLength = 0;
  18304. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  18305. for (i = 0; i < tokens.length; i++) {
  18306. token = tokens[i];
  18307. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  18308. if (parsedInput) {
  18309. skipped = string.substr(0, string.indexOf(parsedInput));
  18310. if (skipped.length > 0) {
  18311. config._pf.unusedInput.push(skipped);
  18312. }
  18313. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  18314. totalParsedInputLength += parsedInput.length;
  18315. }
  18316. // don't parse if it's not a known token
  18317. if (formatTokenFunctions[token]) {
  18318. if (parsedInput) {
  18319. config._pf.empty = false;
  18320. }
  18321. else {
  18322. config._pf.unusedTokens.push(token);
  18323. }
  18324. addTimeToArrayFromToken(token, parsedInput, config);
  18325. }
  18326. else if (config._strict && !parsedInput) {
  18327. config._pf.unusedTokens.push(token);
  18328. }
  18329. }
  18330. // add remaining unparsed input length to the string
  18331. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  18332. if (string.length > 0) {
  18333. config._pf.unusedInput.push(string);
  18334. }
  18335. // handle am pm
  18336. if (config._isPm && config._a[HOUR] < 12) {
  18337. config._a[HOUR] += 12;
  18338. }
  18339. // if is 12 am, change hours to 0
  18340. if (config._isPm === false && config._a[HOUR] === 12) {
  18341. config._a[HOUR] = 0;
  18342. }
  18343. dateFromConfig(config);
  18344. checkOverflow(config);
  18345. }
  18346. function unescapeFormat(s) {
  18347. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  18348. return p1 || p2 || p3 || p4;
  18349. });
  18350. }
  18351. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  18352. function regexpEscape(s) {
  18353. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  18354. }
  18355. // date from string and array of format strings
  18356. function makeDateFromStringAndArray(config) {
  18357. var tempConfig,
  18358. bestMoment,
  18359. scoreToBeat,
  18360. i,
  18361. currentScore;
  18362. if (config._f.length === 0) {
  18363. config._pf.invalidFormat = true;
  18364. config._d = new Date(NaN);
  18365. return;
  18366. }
  18367. for (i = 0; i < config._f.length; i++) {
  18368. currentScore = 0;
  18369. tempConfig = extend({}, config);
  18370. tempConfig._pf = defaultParsingFlags();
  18371. tempConfig._f = config._f[i];
  18372. makeDateFromStringAndFormat(tempConfig);
  18373. if (!isValid(tempConfig)) {
  18374. continue;
  18375. }
  18376. // if there is any input that was not parsed add a penalty for that format
  18377. currentScore += tempConfig._pf.charsLeftOver;
  18378. //or tokens
  18379. currentScore += tempConfig._pf.unusedTokens.length * 10;
  18380. tempConfig._pf.score = currentScore;
  18381. if (scoreToBeat == null || currentScore < scoreToBeat) {
  18382. scoreToBeat = currentScore;
  18383. bestMoment = tempConfig;
  18384. }
  18385. }
  18386. extend(config, bestMoment || tempConfig);
  18387. }
  18388. // date from iso format
  18389. function makeDateFromString(config) {
  18390. var i, l,
  18391. string = config._i,
  18392. match = isoRegex.exec(string);
  18393. if (match) {
  18394. config._pf.iso = true;
  18395. for (i = 0, l = isoDates.length; i < l; i++) {
  18396. if (isoDates[i][1].exec(string)) {
  18397. // match[5] should be "T" or undefined
  18398. config._f = isoDates[i][0] + (match[6] || " ");
  18399. break;
  18400. }
  18401. }
  18402. for (i = 0, l = isoTimes.length; i < l; i++) {
  18403. if (isoTimes[i][1].exec(string)) {
  18404. config._f += isoTimes[i][0];
  18405. break;
  18406. }
  18407. }
  18408. if (string.match(parseTokenTimezone)) {
  18409. config._f += "Z";
  18410. }
  18411. makeDateFromStringAndFormat(config);
  18412. }
  18413. else {
  18414. config._d = new Date(string);
  18415. }
  18416. }
  18417. function makeDateFromInput(config) {
  18418. var input = config._i,
  18419. matched = aspNetJsonRegex.exec(input);
  18420. if (input === undefined) {
  18421. config._d = new Date();
  18422. } else if (matched) {
  18423. config._d = new Date(+matched[1]);
  18424. } else if (typeof input === 'string') {
  18425. makeDateFromString(config);
  18426. } else if (isArray(input)) {
  18427. config._a = input.slice(0);
  18428. dateFromConfig(config);
  18429. } else if (isDate(input)) {
  18430. config._d = new Date(+input);
  18431. } else if (typeof(input) === 'object') {
  18432. dateFromObject(config);
  18433. } else {
  18434. config._d = new Date(input);
  18435. }
  18436. }
  18437. function makeDate(y, m, d, h, M, s, ms) {
  18438. //can't just apply() to create a date:
  18439. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  18440. var date = new Date(y, m, d, h, M, s, ms);
  18441. //the date constructor doesn't accept years < 1970
  18442. if (y < 1970) {
  18443. date.setFullYear(y);
  18444. }
  18445. return date;
  18446. }
  18447. function makeUTCDate(y) {
  18448. var date = new Date(Date.UTC.apply(null, arguments));
  18449. if (y < 1970) {
  18450. date.setUTCFullYear(y);
  18451. }
  18452. return date;
  18453. }
  18454. function parseWeekday(input, language) {
  18455. if (typeof input === 'string') {
  18456. if (!isNaN(input)) {
  18457. input = parseInt(input, 10);
  18458. }
  18459. else {
  18460. input = language.weekdaysParse(input);
  18461. if (typeof input !== 'number') {
  18462. return null;
  18463. }
  18464. }
  18465. }
  18466. return input;
  18467. }
  18468. /************************************
  18469. Relative Time
  18470. ************************************/
  18471. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  18472. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  18473. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  18474. }
  18475. function relativeTime(milliseconds, withoutSuffix, lang) {
  18476. var seconds = round(Math.abs(milliseconds) / 1000),
  18477. minutes = round(seconds / 60),
  18478. hours = round(minutes / 60),
  18479. days = round(hours / 24),
  18480. years = round(days / 365),
  18481. args = seconds < 45 && ['s', seconds] ||
  18482. minutes === 1 && ['m'] ||
  18483. minutes < 45 && ['mm', minutes] ||
  18484. hours === 1 && ['h'] ||
  18485. hours < 22 && ['hh', hours] ||
  18486. days === 1 && ['d'] ||
  18487. days <= 25 && ['dd', days] ||
  18488. days <= 45 && ['M'] ||
  18489. days < 345 && ['MM', round(days / 30)] ||
  18490. years === 1 && ['y'] || ['yy', years];
  18491. args[2] = withoutSuffix;
  18492. args[3] = milliseconds > 0;
  18493. args[4] = lang;
  18494. return substituteTimeAgo.apply({}, args);
  18495. }
  18496. /************************************
  18497. Week of Year
  18498. ************************************/
  18499. // firstDayOfWeek 0 = sun, 6 = sat
  18500. // the day of the week that starts the week
  18501. // (usually sunday or monday)
  18502. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  18503. // the first week is the week that contains the first
  18504. // of this day of the week
  18505. // (eg. ISO weeks use thursday (4))
  18506. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  18507. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  18508. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  18509. adjustedMoment;
  18510. if (daysToDayOfWeek > end) {
  18511. daysToDayOfWeek -= 7;
  18512. }
  18513. if (daysToDayOfWeek < end - 7) {
  18514. daysToDayOfWeek += 7;
  18515. }
  18516. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  18517. return {
  18518. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  18519. year: adjustedMoment.year()
  18520. };
  18521. }
  18522. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  18523. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  18524. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  18525. weekday = weekday != null ? weekday : firstDayOfWeek;
  18526. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  18527. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  18528. return {
  18529. year: dayOfYear > 0 ? year : year - 1,
  18530. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  18531. };
  18532. }
  18533. /************************************
  18534. Top Level Functions
  18535. ************************************/
  18536. function makeMoment(config) {
  18537. var input = config._i,
  18538. format = config._f;
  18539. if (input === null) {
  18540. return moment.invalid({nullInput: true});
  18541. }
  18542. if (typeof input === 'string') {
  18543. config._i = input = getLangDefinition().preparse(input);
  18544. }
  18545. if (moment.isMoment(input)) {
  18546. config = cloneMoment(input);
  18547. config._d = new Date(+input._d);
  18548. } else if (format) {
  18549. if (isArray(format)) {
  18550. makeDateFromStringAndArray(config);
  18551. } else {
  18552. makeDateFromStringAndFormat(config);
  18553. }
  18554. } else {
  18555. makeDateFromInput(config);
  18556. }
  18557. return new Moment(config);
  18558. }
  18559. moment = function (input, format, lang, strict) {
  18560. var c;
  18561. if (typeof(lang) === "boolean") {
  18562. strict = lang;
  18563. lang = undefined;
  18564. }
  18565. // object construction must be done this way.
  18566. // https://github.com/moment/moment/issues/1423
  18567. c = {};
  18568. c._isAMomentObject = true;
  18569. c._i = input;
  18570. c._f = format;
  18571. c._l = lang;
  18572. c._strict = strict;
  18573. c._isUTC = false;
  18574. c._pf = defaultParsingFlags();
  18575. return makeMoment(c);
  18576. };
  18577. // creating with utc
  18578. moment.utc = function (input, format, lang, strict) {
  18579. var c;
  18580. if (typeof(lang) === "boolean") {
  18581. strict = lang;
  18582. lang = undefined;
  18583. }
  18584. // object construction must be done this way.
  18585. // https://github.com/moment/moment/issues/1423
  18586. c = {};
  18587. c._isAMomentObject = true;
  18588. c._useUTC = true;
  18589. c._isUTC = true;
  18590. c._l = lang;
  18591. c._i = input;
  18592. c._f = format;
  18593. c._strict = strict;
  18594. c._pf = defaultParsingFlags();
  18595. return makeMoment(c).utc();
  18596. };
  18597. // creating with unix timestamp (in seconds)
  18598. moment.unix = function (input) {
  18599. return moment(input * 1000);
  18600. };
  18601. // duration
  18602. moment.duration = function (input, key) {
  18603. var duration = input,
  18604. // matching against regexp is expensive, do it on demand
  18605. match = null,
  18606. sign,
  18607. ret,
  18608. parseIso;
  18609. if (moment.isDuration(input)) {
  18610. duration = {
  18611. ms: input._milliseconds,
  18612. d: input._days,
  18613. M: input._months
  18614. };
  18615. } else if (typeof input === 'number') {
  18616. duration = {};
  18617. if (key) {
  18618. duration[key] = input;
  18619. } else {
  18620. duration.milliseconds = input;
  18621. }
  18622. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  18623. sign = (match[1] === "-") ? -1 : 1;
  18624. duration = {
  18625. y: 0,
  18626. d: toInt(match[DATE]) * sign,
  18627. h: toInt(match[HOUR]) * sign,
  18628. m: toInt(match[MINUTE]) * sign,
  18629. s: toInt(match[SECOND]) * sign,
  18630. ms: toInt(match[MILLISECOND]) * sign
  18631. };
  18632. } else if (!!(match = isoDurationRegex.exec(input))) {
  18633. sign = (match[1] === "-") ? -1 : 1;
  18634. parseIso = function (inp) {
  18635. // We'd normally use ~~inp for this, but unfortunately it also
  18636. // converts floats to ints.
  18637. // inp may be undefined, so careful calling replace on it.
  18638. var res = inp && parseFloat(inp.replace(',', '.'));
  18639. // apply sign while we're at it
  18640. return (isNaN(res) ? 0 : res) * sign;
  18641. };
  18642. duration = {
  18643. y: parseIso(match[2]),
  18644. M: parseIso(match[3]),
  18645. d: parseIso(match[4]),
  18646. h: parseIso(match[5]),
  18647. m: parseIso(match[6]),
  18648. s: parseIso(match[7]),
  18649. w: parseIso(match[8])
  18650. };
  18651. }
  18652. ret = new Duration(duration);
  18653. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  18654. ret._lang = input._lang;
  18655. }
  18656. return ret;
  18657. };
  18658. // version number
  18659. moment.version = VERSION;
  18660. // default format
  18661. moment.defaultFormat = isoFormat;
  18662. // This function will be called whenever a moment is mutated.
  18663. // It is intended to keep the offset in sync with the timezone.
  18664. moment.updateOffset = function () {};
  18665. // This function will load languages and then set the global language. If
  18666. // no arguments are passed in, it will simply return the current global
  18667. // language key.
  18668. moment.lang = function (key, values) {
  18669. var r;
  18670. if (!key) {
  18671. return moment.fn._lang._abbr;
  18672. }
  18673. if (values) {
  18674. loadLang(normalizeLanguage(key), values);
  18675. } else if (values === null) {
  18676. unloadLang(key);
  18677. key = 'en';
  18678. } else if (!languages[key]) {
  18679. getLangDefinition(key);
  18680. }
  18681. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  18682. return r._abbr;
  18683. };
  18684. // returns language data
  18685. moment.langData = function (key) {
  18686. if (key && key._lang && key._lang._abbr) {
  18687. key = key._lang._abbr;
  18688. }
  18689. return getLangDefinition(key);
  18690. };
  18691. // compare moment object
  18692. moment.isMoment = function (obj) {
  18693. return obj instanceof Moment ||
  18694. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  18695. };
  18696. // for typechecking Duration objects
  18697. moment.isDuration = function (obj) {
  18698. return obj instanceof Duration;
  18699. };
  18700. for (i = lists.length - 1; i >= 0; --i) {
  18701. makeList(lists[i]);
  18702. }
  18703. moment.normalizeUnits = function (units) {
  18704. return normalizeUnits(units);
  18705. };
  18706. moment.invalid = function (flags) {
  18707. var m = moment.utc(NaN);
  18708. if (flags != null) {
  18709. extend(m._pf, flags);
  18710. }
  18711. else {
  18712. m._pf.userInvalidated = true;
  18713. }
  18714. return m;
  18715. };
  18716. moment.parseZone = function (input) {
  18717. return moment(input).parseZone();
  18718. };
  18719. /************************************
  18720. Moment Prototype
  18721. ************************************/
  18722. extend(moment.fn = Moment.prototype, {
  18723. clone : function () {
  18724. return moment(this);
  18725. },
  18726. valueOf : function () {
  18727. return +this._d + ((this._offset || 0) * 60000);
  18728. },
  18729. unix : function () {
  18730. return Math.floor(+this / 1000);
  18731. },
  18732. toString : function () {
  18733. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  18734. },
  18735. toDate : function () {
  18736. return this._offset ? new Date(+this) : this._d;
  18737. },
  18738. toISOString : function () {
  18739. var m = moment(this).utc();
  18740. if (0 < m.year() && m.year() <= 9999) {
  18741. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18742. } else {
  18743. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18744. }
  18745. },
  18746. toArray : function () {
  18747. var m = this;
  18748. return [
  18749. m.year(),
  18750. m.month(),
  18751. m.date(),
  18752. m.hours(),
  18753. m.minutes(),
  18754. m.seconds(),
  18755. m.milliseconds()
  18756. ];
  18757. },
  18758. isValid : function () {
  18759. return isValid(this);
  18760. },
  18761. isDSTShifted : function () {
  18762. if (this._a) {
  18763. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  18764. }
  18765. return false;
  18766. },
  18767. parsingFlags : function () {
  18768. return extend({}, this._pf);
  18769. },
  18770. invalidAt: function () {
  18771. return this._pf.overflow;
  18772. },
  18773. utc : function () {
  18774. return this.zone(0);
  18775. },
  18776. local : function () {
  18777. this.zone(0);
  18778. this._isUTC = false;
  18779. return this;
  18780. },
  18781. format : function (inputString) {
  18782. var output = formatMoment(this, inputString || moment.defaultFormat);
  18783. return this.lang().postformat(output);
  18784. },
  18785. add : function (input, val) {
  18786. var dur;
  18787. // switch args to support add('s', 1) and add(1, 's')
  18788. if (typeof input === 'string') {
  18789. dur = moment.duration(+val, input);
  18790. } else {
  18791. dur = moment.duration(input, val);
  18792. }
  18793. addOrSubtractDurationFromMoment(this, dur, 1);
  18794. return this;
  18795. },
  18796. subtract : function (input, val) {
  18797. var dur;
  18798. // switch args to support subtract('s', 1) and subtract(1, 's')
  18799. if (typeof input === 'string') {
  18800. dur = moment.duration(+val, input);
  18801. } else {
  18802. dur = moment.duration(input, val);
  18803. }
  18804. addOrSubtractDurationFromMoment(this, dur, -1);
  18805. return this;
  18806. },
  18807. diff : function (input, units, asFloat) {
  18808. var that = makeAs(input, this),
  18809. zoneDiff = (this.zone() - that.zone()) * 6e4,
  18810. diff, output;
  18811. units = normalizeUnits(units);
  18812. if (units === 'year' || units === 'month') {
  18813. // average number of days in the months in the given dates
  18814. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  18815. // difference in months
  18816. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  18817. // adjust by taking difference in days, average number of days
  18818. // and dst in the given months.
  18819. output += ((this - moment(this).startOf('month')) -
  18820. (that - moment(that).startOf('month'))) / diff;
  18821. // same as above but with zones, to negate all dst
  18822. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  18823. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  18824. if (units === 'year') {
  18825. output = output / 12;
  18826. }
  18827. } else {
  18828. diff = (this - that);
  18829. output = units === 'second' ? diff / 1e3 : // 1000
  18830. units === 'minute' ? diff / 6e4 : // 1000 * 60
  18831. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  18832. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  18833. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  18834. diff;
  18835. }
  18836. return asFloat ? output : absRound(output);
  18837. },
  18838. from : function (time, withoutSuffix) {
  18839. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  18840. },
  18841. fromNow : function (withoutSuffix) {
  18842. return this.from(moment(), withoutSuffix);
  18843. },
  18844. calendar : function () {
  18845. // We want to compare the start of today, vs this.
  18846. // Getting start-of-today depends on whether we're zone'd or not.
  18847. var sod = makeAs(moment(), this).startOf('day'),
  18848. diff = this.diff(sod, 'days', true),
  18849. format = diff < -6 ? 'sameElse' :
  18850. diff < -1 ? 'lastWeek' :
  18851. diff < 0 ? 'lastDay' :
  18852. diff < 1 ? 'sameDay' :
  18853. diff < 2 ? 'nextDay' :
  18854. diff < 7 ? 'nextWeek' : 'sameElse';
  18855. return this.format(this.lang().calendar(format, this));
  18856. },
  18857. isLeapYear : function () {
  18858. return isLeapYear(this.year());
  18859. },
  18860. isDST : function () {
  18861. return (this.zone() < this.clone().month(0).zone() ||
  18862. this.zone() < this.clone().month(5).zone());
  18863. },
  18864. day : function (input) {
  18865. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  18866. if (input != null) {
  18867. input = parseWeekday(input, this.lang());
  18868. return this.add({ d : input - day });
  18869. } else {
  18870. return day;
  18871. }
  18872. },
  18873. month : function (input) {
  18874. var utc = this._isUTC ? 'UTC' : '',
  18875. dayOfMonth;
  18876. if (input != null) {
  18877. if (typeof input === 'string') {
  18878. input = this.lang().monthsParse(input);
  18879. if (typeof input !== 'number') {
  18880. return this;
  18881. }
  18882. }
  18883. dayOfMonth = this.date();
  18884. this.date(1);
  18885. this._d['set' + utc + 'Month'](input);
  18886. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  18887. moment.updateOffset(this);
  18888. return this;
  18889. } else {
  18890. return this._d['get' + utc + 'Month']();
  18891. }
  18892. },
  18893. startOf: function (units) {
  18894. units = normalizeUnits(units);
  18895. // the following switch intentionally omits break keywords
  18896. // to utilize falling through the cases.
  18897. switch (units) {
  18898. case 'year':
  18899. this.month(0);
  18900. /* falls through */
  18901. case 'month':
  18902. this.date(1);
  18903. /* falls through */
  18904. case 'week':
  18905. case 'isoWeek':
  18906. case 'day':
  18907. this.hours(0);
  18908. /* falls through */
  18909. case 'hour':
  18910. this.minutes(0);
  18911. /* falls through */
  18912. case 'minute':
  18913. this.seconds(0);
  18914. /* falls through */
  18915. case 'second':
  18916. this.milliseconds(0);
  18917. /* falls through */
  18918. }
  18919. // weeks are a special case
  18920. if (units === 'week') {
  18921. this.weekday(0);
  18922. } else if (units === 'isoWeek') {
  18923. this.isoWeekday(1);
  18924. }
  18925. return this;
  18926. },
  18927. endOf: function (units) {
  18928. units = normalizeUnits(units);
  18929. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  18930. },
  18931. isAfter: function (input, units) {
  18932. units = typeof units !== 'undefined' ? units : 'millisecond';
  18933. return +this.clone().startOf(units) > +moment(input).startOf(units);
  18934. },
  18935. isBefore: function (input, units) {
  18936. units = typeof units !== 'undefined' ? units : 'millisecond';
  18937. return +this.clone().startOf(units) < +moment(input).startOf(units);
  18938. },
  18939. isSame: function (input, units) {
  18940. units = units || 'ms';
  18941. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  18942. },
  18943. min: function (other) {
  18944. other = moment.apply(null, arguments);
  18945. return other < this ? this : other;
  18946. },
  18947. max: function (other) {
  18948. other = moment.apply(null, arguments);
  18949. return other > this ? this : other;
  18950. },
  18951. zone : function (input) {
  18952. var offset = this._offset || 0;
  18953. if (input != null) {
  18954. if (typeof input === "string") {
  18955. input = timezoneMinutesFromString(input);
  18956. }
  18957. if (Math.abs(input) < 16) {
  18958. input = input * 60;
  18959. }
  18960. this._offset = input;
  18961. this._isUTC = true;
  18962. if (offset !== input) {
  18963. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  18964. }
  18965. } else {
  18966. return this._isUTC ? offset : this._d.getTimezoneOffset();
  18967. }
  18968. return this;
  18969. },
  18970. zoneAbbr : function () {
  18971. return this._isUTC ? "UTC" : "";
  18972. },
  18973. zoneName : function () {
  18974. return this._isUTC ? "Coordinated Universal Time" : "";
  18975. },
  18976. parseZone : function () {
  18977. if (this._tzm) {
  18978. this.zone(this._tzm);
  18979. } else if (typeof this._i === 'string') {
  18980. this.zone(this._i);
  18981. }
  18982. return this;
  18983. },
  18984. hasAlignedHourOffset : function (input) {
  18985. if (!input) {
  18986. input = 0;
  18987. }
  18988. else {
  18989. input = moment(input).zone();
  18990. }
  18991. return (this.zone() - input) % 60 === 0;
  18992. },
  18993. daysInMonth : function () {
  18994. return daysInMonth(this.year(), this.month());
  18995. },
  18996. dayOfYear : function (input) {
  18997. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  18998. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  18999. },
  19000. quarter : function () {
  19001. return Math.ceil((this.month() + 1.0) / 3.0);
  19002. },
  19003. weekYear : function (input) {
  19004. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  19005. return input == null ? year : this.add("y", (input - year));
  19006. },
  19007. isoWeekYear : function (input) {
  19008. var year = weekOfYear(this, 1, 4).year;
  19009. return input == null ? year : this.add("y", (input - year));
  19010. },
  19011. week : function (input) {
  19012. var week = this.lang().week(this);
  19013. return input == null ? week : this.add("d", (input - week) * 7);
  19014. },
  19015. isoWeek : function (input) {
  19016. var week = weekOfYear(this, 1, 4).week;
  19017. return input == null ? week : this.add("d", (input - week) * 7);
  19018. },
  19019. weekday : function (input) {
  19020. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  19021. return input == null ? weekday : this.add("d", input - weekday);
  19022. },
  19023. isoWeekday : function (input) {
  19024. // behaves the same as moment#day except
  19025. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  19026. // as a setter, sunday should belong to the previous week.
  19027. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  19028. },
  19029. get : function (units) {
  19030. units = normalizeUnits(units);
  19031. return this[units]();
  19032. },
  19033. set : function (units, value) {
  19034. units = normalizeUnits(units);
  19035. if (typeof this[units] === 'function') {
  19036. this[units](value);
  19037. }
  19038. return this;
  19039. },
  19040. // If passed a language key, it will set the language for this
  19041. // instance. Otherwise, it will return the language configuration
  19042. // variables for this instance.
  19043. lang : function (key) {
  19044. if (key === undefined) {
  19045. return this._lang;
  19046. } else {
  19047. this._lang = getLangDefinition(key);
  19048. return this;
  19049. }
  19050. }
  19051. });
  19052. // helper for adding shortcuts
  19053. function makeGetterAndSetter(name, key) {
  19054. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  19055. var utc = this._isUTC ? 'UTC' : '';
  19056. if (input != null) {
  19057. this._d['set' + utc + key](input);
  19058. moment.updateOffset(this);
  19059. return this;
  19060. } else {
  19061. return this._d['get' + utc + key]();
  19062. }
  19063. };
  19064. }
  19065. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  19066. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  19067. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  19068. }
  19069. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  19070. makeGetterAndSetter('year', 'FullYear');
  19071. // add plural methods
  19072. moment.fn.days = moment.fn.day;
  19073. moment.fn.months = moment.fn.month;
  19074. moment.fn.weeks = moment.fn.week;
  19075. moment.fn.isoWeeks = moment.fn.isoWeek;
  19076. // add aliased format methods
  19077. moment.fn.toJSON = moment.fn.toISOString;
  19078. /************************************
  19079. Duration Prototype
  19080. ************************************/
  19081. extend(moment.duration.fn = Duration.prototype, {
  19082. _bubble : function () {
  19083. var milliseconds = this._milliseconds,
  19084. days = this._days,
  19085. months = this._months,
  19086. data = this._data,
  19087. seconds, minutes, hours, years;
  19088. // The following code bubbles up values, see the tests for
  19089. // examples of what that means.
  19090. data.milliseconds = milliseconds % 1000;
  19091. seconds = absRound(milliseconds / 1000);
  19092. data.seconds = seconds % 60;
  19093. minutes = absRound(seconds / 60);
  19094. data.minutes = minutes % 60;
  19095. hours = absRound(minutes / 60);
  19096. data.hours = hours % 24;
  19097. days += absRound(hours / 24);
  19098. data.days = days % 30;
  19099. months += absRound(days / 30);
  19100. data.months = months % 12;
  19101. years = absRound(months / 12);
  19102. data.years = years;
  19103. },
  19104. weeks : function () {
  19105. return absRound(this.days() / 7);
  19106. },
  19107. valueOf : function () {
  19108. return this._milliseconds +
  19109. this._days * 864e5 +
  19110. (this._months % 12) * 2592e6 +
  19111. toInt(this._months / 12) * 31536e6;
  19112. },
  19113. humanize : function (withSuffix) {
  19114. var difference = +this,
  19115. output = relativeTime(difference, !withSuffix, this.lang());
  19116. if (withSuffix) {
  19117. output = this.lang().pastFuture(difference, output);
  19118. }
  19119. return this.lang().postformat(output);
  19120. },
  19121. add : function (input, val) {
  19122. // supports only 2.0-style add(1, 's') or add(moment)
  19123. var dur = moment.duration(input, val);
  19124. this._milliseconds += dur._milliseconds;
  19125. this._days += dur._days;
  19126. this._months += dur._months;
  19127. this._bubble();
  19128. return this;
  19129. },
  19130. subtract : function (input, val) {
  19131. var dur = moment.duration(input, val);
  19132. this._milliseconds -= dur._milliseconds;
  19133. this._days -= dur._days;
  19134. this._months -= dur._months;
  19135. this._bubble();
  19136. return this;
  19137. },
  19138. get : function (units) {
  19139. units = normalizeUnits(units);
  19140. return this[units.toLowerCase() + 's']();
  19141. },
  19142. as : function (units) {
  19143. units = normalizeUnits(units);
  19144. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  19145. },
  19146. lang : moment.fn.lang,
  19147. toIsoString : function () {
  19148. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  19149. var years = Math.abs(this.years()),
  19150. months = Math.abs(this.months()),
  19151. days = Math.abs(this.days()),
  19152. hours = Math.abs(this.hours()),
  19153. minutes = Math.abs(this.minutes()),
  19154. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  19155. if (!this.asSeconds()) {
  19156. // this is the same as C#'s (Noda) and python (isodate)...
  19157. // but not other JS (goog.date)
  19158. return 'P0D';
  19159. }
  19160. return (this.asSeconds() < 0 ? '-' : '') +
  19161. 'P' +
  19162. (years ? years + 'Y' : '') +
  19163. (months ? months + 'M' : '') +
  19164. (days ? days + 'D' : '') +
  19165. ((hours || minutes || seconds) ? 'T' : '') +
  19166. (hours ? hours + 'H' : '') +
  19167. (minutes ? minutes + 'M' : '') +
  19168. (seconds ? seconds + 'S' : '');
  19169. }
  19170. });
  19171. function makeDurationGetter(name) {
  19172. moment.duration.fn[name] = function () {
  19173. return this._data[name];
  19174. };
  19175. }
  19176. function makeDurationAsGetter(name, factor) {
  19177. moment.duration.fn['as' + name] = function () {
  19178. return +this / factor;
  19179. };
  19180. }
  19181. for (i in unitMillisecondFactors) {
  19182. if (unitMillisecondFactors.hasOwnProperty(i)) {
  19183. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  19184. makeDurationGetter(i.toLowerCase());
  19185. }
  19186. }
  19187. makeDurationAsGetter('Weeks', 6048e5);
  19188. moment.duration.fn.asMonths = function () {
  19189. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  19190. };
  19191. /************************************
  19192. Default Lang
  19193. ************************************/
  19194. // Set default language, other languages will inherit from English.
  19195. moment.lang('en', {
  19196. ordinal : function (number) {
  19197. var b = number % 10,
  19198. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  19199. (b === 1) ? 'st' :
  19200. (b === 2) ? 'nd' :
  19201. (b === 3) ? 'rd' : 'th';
  19202. return number + output;
  19203. }
  19204. });
  19205. /* EMBED_LANGUAGES */
  19206. /************************************
  19207. Exposing Moment
  19208. ************************************/
  19209. function makeGlobal(deprecate) {
  19210. var warned = false, local_moment = moment;
  19211. /*global ender:false */
  19212. if (typeof ender !== 'undefined') {
  19213. return;
  19214. }
  19215. // here, `this` means `window` in the browser, or `global` on the server
  19216. // add `moment` as a global object via a string identifier,
  19217. // for Closure Compiler "advanced" mode
  19218. if (deprecate) {
  19219. global.moment = function () {
  19220. if (!warned && console && console.warn) {
  19221. warned = true;
  19222. console.warn(
  19223. "Accessing Moment through the global scope is " +
  19224. "deprecated, and will be removed in an upcoming " +
  19225. "release.");
  19226. }
  19227. return local_moment.apply(null, arguments);
  19228. };
  19229. extend(global.moment, local_moment);
  19230. } else {
  19231. global['moment'] = moment;
  19232. }
  19233. }
  19234. // CommonJS module is defined
  19235. if (hasModule) {
  19236. module.exports = moment;
  19237. makeGlobal(true);
  19238. } else if (typeof define === "function" && define.amd) {
  19239. define("moment", function (require, exports, module) {
  19240. if (module.config && module.config() && module.config().noGlobal !== true) {
  19241. // If user provided noGlobal, he is aware of global
  19242. makeGlobal(module.config().noGlobal === undefined);
  19243. }
  19244. return moment;
  19245. });
  19246. } else {
  19247. makeGlobal();
  19248. }
  19249. }).call(this);
  19250. },{}],5:[function(require,module,exports){
  19251. /**
  19252. * Copyright 2012 Craig Campbell
  19253. *
  19254. * Licensed under the Apache License, Version 2.0 (the "License");
  19255. * you may not use this file except in compliance with the License.
  19256. * You may obtain a copy of the License at
  19257. *
  19258. * http://www.apache.org/licenses/LICENSE-2.0
  19259. *
  19260. * Unless required by applicable law or agreed to in writing, software
  19261. * distributed under the License is distributed on an "AS IS" BASIS,
  19262. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19263. * See the License for the specific language governing permissions and
  19264. * limitations under the License.
  19265. *
  19266. * Mousetrap is a simple keyboard shortcut library for Javascript with
  19267. * no external dependencies
  19268. *
  19269. * @version 1.1.2
  19270. * @url craig.is/killing/mice
  19271. */
  19272. /**
  19273. * mapping of special keycodes to their corresponding keys
  19274. *
  19275. * everything in this dictionary cannot use keypress events
  19276. * so it has to be here to map to the correct keycodes for
  19277. * keyup/keydown events
  19278. *
  19279. * @type {Object}
  19280. */
  19281. var _MAP = {
  19282. 8: 'backspace',
  19283. 9: 'tab',
  19284. 13: 'enter',
  19285. 16: 'shift',
  19286. 17: 'ctrl',
  19287. 18: 'alt',
  19288. 20: 'capslock',
  19289. 27: 'esc',
  19290. 32: 'space',
  19291. 33: 'pageup',
  19292. 34: 'pagedown',
  19293. 35: 'end',
  19294. 36: 'home',
  19295. 37: 'left',
  19296. 38: 'up',
  19297. 39: 'right',
  19298. 40: 'down',
  19299. 45: 'ins',
  19300. 46: 'del',
  19301. 91: 'meta',
  19302. 93: 'meta',
  19303. 224: 'meta'
  19304. },
  19305. /**
  19306. * mapping for special characters so they can support
  19307. *
  19308. * this dictionary is only used incase you want to bind a
  19309. * keyup or keydown event to one of these keys
  19310. *
  19311. * @type {Object}
  19312. */
  19313. _KEYCODE_MAP = {
  19314. 106: '*',
  19315. 107: '+',
  19316. 109: '-',
  19317. 110: '.',
  19318. 111 : '/',
  19319. 186: ';',
  19320. 187: '=',
  19321. 188: ',',
  19322. 189: '-',
  19323. 190: '.',
  19324. 191: '/',
  19325. 192: '`',
  19326. 219: '[',
  19327. 220: '\\',
  19328. 221: ']',
  19329. 222: '\''
  19330. },
  19331. /**
  19332. * this is a mapping of keys that require shift on a US keypad
  19333. * back to the non shift equivelents
  19334. *
  19335. * this is so you can use keyup events with these keys
  19336. *
  19337. * note that this will only work reliably on US keyboards
  19338. *
  19339. * @type {Object}
  19340. */
  19341. _SHIFT_MAP = {
  19342. '~': '`',
  19343. '!': '1',
  19344. '@': '2',
  19345. '#': '3',
  19346. '$': '4',
  19347. '%': '5',
  19348. '^': '6',
  19349. '&': '7',
  19350. '*': '8',
  19351. '(': '9',
  19352. ')': '0',
  19353. '_': '-',
  19354. '+': '=',
  19355. ':': ';',
  19356. '\"': '\'',
  19357. '<': ',',
  19358. '>': '.',
  19359. '?': '/',
  19360. '|': '\\'
  19361. },
  19362. /**
  19363. * this is a list of special strings you can use to map
  19364. * to modifier keys when you specify your keyboard shortcuts
  19365. *
  19366. * @type {Object}
  19367. */
  19368. _SPECIAL_ALIASES = {
  19369. 'option': 'alt',
  19370. 'command': 'meta',
  19371. 'return': 'enter',
  19372. 'escape': 'esc'
  19373. },
  19374. /**
  19375. * variable to store the flipped version of _MAP from above
  19376. * needed to check if we should use keypress or not when no action
  19377. * is specified
  19378. *
  19379. * @type {Object|undefined}
  19380. */
  19381. _REVERSE_MAP,
  19382. /**
  19383. * a list of all the callbacks setup via Mousetrap.bind()
  19384. *
  19385. * @type {Object}
  19386. */
  19387. _callbacks = {},
  19388. /**
  19389. * direct map of string combinations to callbacks used for trigger()
  19390. *
  19391. * @type {Object}
  19392. */
  19393. _direct_map = {},
  19394. /**
  19395. * keeps track of what level each sequence is at since multiple
  19396. * sequences can start out with the same sequence
  19397. *
  19398. * @type {Object}
  19399. */
  19400. _sequence_levels = {},
  19401. /**
  19402. * variable to store the setTimeout call
  19403. *
  19404. * @type {null|number}
  19405. */
  19406. _reset_timer,
  19407. /**
  19408. * temporary state where we will ignore the next keyup
  19409. *
  19410. * @type {boolean|string}
  19411. */
  19412. _ignore_next_keyup = false,
  19413. /**
  19414. * are we currently inside of a sequence?
  19415. * type of action ("keyup" or "keydown" or "keypress") or false
  19416. *
  19417. * @type {boolean|string}
  19418. */
  19419. _inside_sequence = false;
  19420. /**
  19421. * loop through the f keys, f1 to f19 and add them to the map
  19422. * programatically
  19423. */
  19424. for (var i = 1; i < 20; ++i) {
  19425. _MAP[111 + i] = 'f' + i;
  19426. }
  19427. /**
  19428. * loop through to map numbers on the numeric keypad
  19429. */
  19430. for (i = 0; i <= 9; ++i) {
  19431. _MAP[i + 96] = i;
  19432. }
  19433. /**
  19434. * cross browser add event method
  19435. *
  19436. * @param {Element|HTMLDocument} object
  19437. * @param {string} type
  19438. * @param {Function} callback
  19439. * @returns void
  19440. */
  19441. function _addEvent(object, type, callback) {
  19442. if (object.addEventListener) {
  19443. return object.addEventListener(type, callback, false);
  19444. }
  19445. object.attachEvent('on' + type, callback);
  19446. }
  19447. /**
  19448. * takes the event and returns the key character
  19449. *
  19450. * @param {Event} e
  19451. * @return {string}
  19452. */
  19453. function _characterFromEvent(e) {
  19454. // for keypress events we should return the character as is
  19455. if (e.type == 'keypress') {
  19456. return String.fromCharCode(e.which);
  19457. }
  19458. // for non keypress events the special maps are needed
  19459. if (_MAP[e.which]) {
  19460. return _MAP[e.which];
  19461. }
  19462. if (_KEYCODE_MAP[e.which]) {
  19463. return _KEYCODE_MAP[e.which];
  19464. }
  19465. // if it is not in the special map
  19466. return String.fromCharCode(e.which).toLowerCase();
  19467. }
  19468. /**
  19469. * should we stop this event before firing off callbacks
  19470. *
  19471. * @param {Event} e
  19472. * @return {boolean}
  19473. */
  19474. function _stop(e) {
  19475. var element = e.target || e.srcElement,
  19476. tag_name = element.tagName;
  19477. // if the element has the class "mousetrap" then no need to stop
  19478. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  19479. return false;
  19480. }
  19481. // stop for input, select, and textarea
  19482. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  19483. }
  19484. /**
  19485. * checks if two arrays are equal
  19486. *
  19487. * @param {Array} modifiers1
  19488. * @param {Array} modifiers2
  19489. * @returns {boolean}
  19490. */
  19491. function _modifiersMatch(modifiers1, modifiers2) {
  19492. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  19493. }
  19494. /**
  19495. * resets all sequence counters except for the ones passed in
  19496. *
  19497. * @param {Object} do_not_reset
  19498. * @returns void
  19499. */
  19500. function _resetSequences(do_not_reset) {
  19501. do_not_reset = do_not_reset || {};
  19502. var active_sequences = false,
  19503. key;
  19504. for (key in _sequence_levels) {
  19505. if (do_not_reset[key]) {
  19506. active_sequences = true;
  19507. continue;
  19508. }
  19509. _sequence_levels[key] = 0;
  19510. }
  19511. if (!active_sequences) {
  19512. _inside_sequence = false;
  19513. }
  19514. }
  19515. /**
  19516. * finds all callbacks that match based on the keycode, modifiers,
  19517. * and action
  19518. *
  19519. * @param {string} character
  19520. * @param {Array} modifiers
  19521. * @param {string} action
  19522. * @param {boolean=} remove - should we remove any matches
  19523. * @param {string=} combination
  19524. * @returns {Array}
  19525. */
  19526. function _getMatches(character, modifiers, action, remove, combination) {
  19527. var i,
  19528. callback,
  19529. matches = [];
  19530. // if there are no events related to this keycode
  19531. if (!_callbacks[character]) {
  19532. return [];
  19533. }
  19534. // if a modifier key is coming up on its own we should allow it
  19535. if (action == 'keyup' && _isModifier(character)) {
  19536. modifiers = [character];
  19537. }
  19538. // loop through all callbacks for the key that was pressed
  19539. // and see if any of them match
  19540. for (i = 0; i < _callbacks[character].length; ++i) {
  19541. callback = _callbacks[character][i];
  19542. // if this is a sequence but it is not at the right level
  19543. // then move onto the next match
  19544. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  19545. continue;
  19546. }
  19547. // if the action we are looking for doesn't match the action we got
  19548. // then we should keep going
  19549. if (action != callback.action) {
  19550. continue;
  19551. }
  19552. // if this is a keypress event that means that we need to only
  19553. // look at the character, otherwise check the modifiers as
  19554. // well
  19555. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  19556. // remove is used so if you change your mind and call bind a
  19557. // second time with a new function the first one is overwritten
  19558. if (remove && callback.combo == combination) {
  19559. _callbacks[character].splice(i, 1);
  19560. }
  19561. matches.push(callback);
  19562. }
  19563. }
  19564. return matches;
  19565. }
  19566. /**
  19567. * takes a key event and figures out what the modifiers are
  19568. *
  19569. * @param {Event} e
  19570. * @returns {Array}
  19571. */
  19572. function _eventModifiers(e) {
  19573. var modifiers = [];
  19574. if (e.shiftKey) {
  19575. modifiers.push('shift');
  19576. }
  19577. if (e.altKey) {
  19578. modifiers.push('alt');
  19579. }
  19580. if (e.ctrlKey) {
  19581. modifiers.push('ctrl');
  19582. }
  19583. if (e.metaKey) {
  19584. modifiers.push('meta');
  19585. }
  19586. return modifiers;
  19587. }
  19588. /**
  19589. * actually calls the callback function
  19590. *
  19591. * if your callback function returns false this will use the jquery
  19592. * convention - prevent default and stop propogation on the event
  19593. *
  19594. * @param {Function} callback
  19595. * @param {Event} e
  19596. * @returns void
  19597. */
  19598. function _fireCallback(callback, e) {
  19599. if (callback(e) === false) {
  19600. if (e.preventDefault) {
  19601. e.preventDefault();
  19602. }
  19603. if (e.stopPropagation) {
  19604. e.stopPropagation();
  19605. }
  19606. e.returnValue = false;
  19607. e.cancelBubble = true;
  19608. }
  19609. }
  19610. /**
  19611. * handles a character key event
  19612. *
  19613. * @param {string} character
  19614. * @param {Event} e
  19615. * @returns void
  19616. */
  19617. function _handleCharacter(character, e) {
  19618. // if this event should not happen stop here
  19619. if (_stop(e)) {
  19620. return;
  19621. }
  19622. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  19623. i,
  19624. do_not_reset = {},
  19625. processed_sequence_callback = false;
  19626. // loop through matching callbacks for this key event
  19627. for (i = 0; i < callbacks.length; ++i) {
  19628. // fire for all sequence callbacks
  19629. // this is because if for example you have multiple sequences
  19630. // bound such as "g i" and "g t" they both need to fire the
  19631. // callback for matching g cause otherwise you can only ever
  19632. // match the first one
  19633. if (callbacks[i].seq) {
  19634. processed_sequence_callback = true;
  19635. // keep a list of which sequences were matches for later
  19636. do_not_reset[callbacks[i].seq] = 1;
  19637. _fireCallback(callbacks[i].callback, e);
  19638. continue;
  19639. }
  19640. // if there were no sequence matches but we are still here
  19641. // that means this is a regular match so we should fire that
  19642. if (!processed_sequence_callback && !_inside_sequence) {
  19643. _fireCallback(callbacks[i].callback, e);
  19644. }
  19645. }
  19646. // if you are inside of a sequence and the key you are pressing
  19647. // is not a modifier key then we should reset all sequences
  19648. // that were not matched by this key event
  19649. if (e.type == _inside_sequence && !_isModifier(character)) {
  19650. _resetSequences(do_not_reset);
  19651. }
  19652. }
  19653. /**
  19654. * handles a keydown event
  19655. *
  19656. * @param {Event} e
  19657. * @returns void
  19658. */
  19659. function _handleKey(e) {
  19660. // normalize e.which for key events
  19661. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  19662. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  19663. var character = _characterFromEvent(e);
  19664. // no character found then stop
  19665. if (!character) {
  19666. return;
  19667. }
  19668. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  19669. _ignore_next_keyup = false;
  19670. return;
  19671. }
  19672. _handleCharacter(character, e);
  19673. }
  19674. /**
  19675. * determines if the keycode specified is a modifier key or not
  19676. *
  19677. * @param {string} key
  19678. * @returns {boolean}
  19679. */
  19680. function _isModifier(key) {
  19681. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  19682. }
  19683. /**
  19684. * called to set a 1 second timeout on the specified sequence
  19685. *
  19686. * this is so after each key press in the sequence you have 1 second
  19687. * to press the next key before you have to start over
  19688. *
  19689. * @returns void
  19690. */
  19691. function _resetSequenceTimer() {
  19692. clearTimeout(_reset_timer);
  19693. _reset_timer = setTimeout(_resetSequences, 1000);
  19694. }
  19695. /**
  19696. * reverses the map lookup so that we can look for specific keys
  19697. * to see what can and can't use keypress
  19698. *
  19699. * @return {Object}
  19700. */
  19701. function _getReverseMap() {
  19702. if (!_REVERSE_MAP) {
  19703. _REVERSE_MAP = {};
  19704. for (var key in _MAP) {
  19705. // pull out the numeric keypad from here cause keypress should
  19706. // be able to detect the keys from the character
  19707. if (key > 95 && key < 112) {
  19708. continue;
  19709. }
  19710. if (_MAP.hasOwnProperty(key)) {
  19711. _REVERSE_MAP[_MAP[key]] = key;
  19712. }
  19713. }
  19714. }
  19715. return _REVERSE_MAP;
  19716. }
  19717. /**
  19718. * picks the best action based on the key combination
  19719. *
  19720. * @param {string} key - character for key
  19721. * @param {Array} modifiers
  19722. * @param {string=} action passed in
  19723. */
  19724. function _pickBestAction(key, modifiers, action) {
  19725. // if no action was picked in we should try to pick the one
  19726. // that we think would work best for this key
  19727. if (!action) {
  19728. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  19729. }
  19730. // modifier keys don't work as expected with keypress,
  19731. // switch to keydown
  19732. if (action == 'keypress' && modifiers.length) {
  19733. action = 'keydown';
  19734. }
  19735. return action;
  19736. }
  19737. /**
  19738. * binds a key sequence to an event
  19739. *
  19740. * @param {string} combo - combo specified in bind call
  19741. * @param {Array} keys
  19742. * @param {Function} callback
  19743. * @param {string=} action
  19744. * @returns void
  19745. */
  19746. function _bindSequence(combo, keys, callback, action) {
  19747. // start off by adding a sequence level record for this combination
  19748. // and setting the level to 0
  19749. _sequence_levels[combo] = 0;
  19750. // if there is no action pick the best one for the first key
  19751. // in the sequence
  19752. if (!action) {
  19753. action = _pickBestAction(keys[0], []);
  19754. }
  19755. /**
  19756. * callback to increase the sequence level for this sequence and reset
  19757. * all other sequences that were active
  19758. *
  19759. * @param {Event} e
  19760. * @returns void
  19761. */
  19762. var _increaseSequence = function(e) {
  19763. _inside_sequence = action;
  19764. ++_sequence_levels[combo];
  19765. _resetSequenceTimer();
  19766. },
  19767. /**
  19768. * wraps the specified callback inside of another function in order
  19769. * to reset all sequence counters as soon as this sequence is done
  19770. *
  19771. * @param {Event} e
  19772. * @returns void
  19773. */
  19774. _callbackAndReset = function(e) {
  19775. _fireCallback(callback, e);
  19776. // we should ignore the next key up if the action is key down
  19777. // or keypress. this is so if you finish a sequence and
  19778. // release the key the final key will not trigger a keyup
  19779. if (action !== 'keyup') {
  19780. _ignore_next_keyup = _characterFromEvent(e);
  19781. }
  19782. // weird race condition if a sequence ends with the key
  19783. // another sequence begins with
  19784. setTimeout(_resetSequences, 10);
  19785. },
  19786. i;
  19787. // loop through keys one at a time and bind the appropriate callback
  19788. // function. for any key leading up to the final one it should
  19789. // increase the sequence. after the final, it should reset all sequences
  19790. for (i = 0; i < keys.length; ++i) {
  19791. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  19792. }
  19793. }
  19794. /**
  19795. * binds a single keyboard combination
  19796. *
  19797. * @param {string} combination
  19798. * @param {Function} callback
  19799. * @param {string=} action
  19800. * @param {string=} sequence_name - name of sequence if part of sequence
  19801. * @param {number=} level - what part of the sequence the command is
  19802. * @returns void
  19803. */
  19804. function _bindSingle(combination, callback, action, sequence_name, level) {
  19805. // make sure multiple spaces in a row become a single space
  19806. combination = combination.replace(/\s+/g, ' ');
  19807. var sequence = combination.split(' '),
  19808. i,
  19809. key,
  19810. keys,
  19811. modifiers = [];
  19812. // if this pattern is a sequence of keys then run through this method
  19813. // to reprocess each pattern one key at a time
  19814. if (sequence.length > 1) {
  19815. return _bindSequence(combination, sequence, callback, action);
  19816. }
  19817. // take the keys from this pattern and figure out what the actual
  19818. // pattern is all about
  19819. keys = combination === '+' ? ['+'] : combination.split('+');
  19820. for (i = 0; i < keys.length; ++i) {
  19821. key = keys[i];
  19822. // normalize key names
  19823. if (_SPECIAL_ALIASES[key]) {
  19824. key = _SPECIAL_ALIASES[key];
  19825. }
  19826. // if this is not a keypress event then we should
  19827. // be smart about using shift keys
  19828. // this will only work for US keyboards however
  19829. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  19830. key = _SHIFT_MAP[key];
  19831. modifiers.push('shift');
  19832. }
  19833. // if this key is a modifier then add it to the list of modifiers
  19834. if (_isModifier(key)) {
  19835. modifiers.push(key);
  19836. }
  19837. }
  19838. // depending on what the key combination is
  19839. // we will try to pick the best event for it
  19840. action = _pickBestAction(key, modifiers, action);
  19841. // make sure to initialize array if this is the first time
  19842. // a callback is added for this key
  19843. if (!_callbacks[key]) {
  19844. _callbacks[key] = [];
  19845. }
  19846. // remove an existing match if there is one
  19847. _getMatches(key, modifiers, action, !sequence_name, combination);
  19848. // add this call back to the array
  19849. // if it is a sequence put it at the beginning
  19850. // if not put it at the end
  19851. //
  19852. // this is important because the way these are processed expects
  19853. // the sequence ones to come first
  19854. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  19855. callback: callback,
  19856. modifiers: modifiers,
  19857. action: action,
  19858. seq: sequence_name,
  19859. level: level,
  19860. combo: combination
  19861. });
  19862. }
  19863. /**
  19864. * binds multiple combinations to the same callback
  19865. *
  19866. * @param {Array} combinations
  19867. * @param {Function} callback
  19868. * @param {string|undefined} action
  19869. * @returns void
  19870. */
  19871. function _bindMultiple(combinations, callback, action) {
  19872. for (var i = 0; i < combinations.length; ++i) {
  19873. _bindSingle(combinations[i], callback, action);
  19874. }
  19875. }
  19876. // start!
  19877. _addEvent(document, 'keypress', _handleKey);
  19878. _addEvent(document, 'keydown', _handleKey);
  19879. _addEvent(document, 'keyup', _handleKey);
  19880. var mousetrap = {
  19881. /**
  19882. * binds an event to mousetrap
  19883. *
  19884. * can be a single key, a combination of keys separated with +,
  19885. * a comma separated list of keys, an array of keys, or
  19886. * a sequence of keys separated by spaces
  19887. *
  19888. * be sure to list the modifier keys first to make sure that the
  19889. * correct key ends up getting bound (the last key in the pattern)
  19890. *
  19891. * @param {string|Array} keys
  19892. * @param {Function} callback
  19893. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  19894. * @returns void
  19895. */
  19896. bind: function(keys, callback, action) {
  19897. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  19898. _direct_map[keys + ':' + action] = callback;
  19899. return this;
  19900. },
  19901. /**
  19902. * unbinds an event to mousetrap
  19903. *
  19904. * the unbinding sets the callback function of the specified key combo
  19905. * to an empty function and deletes the corresponding key in the
  19906. * _direct_map dict.
  19907. *
  19908. * the keycombo+action has to be exactly the same as
  19909. * it was defined in the bind method
  19910. *
  19911. * TODO: actually remove this from the _callbacks dictionary instead
  19912. * of binding an empty function
  19913. *
  19914. * @param {string|Array} keys
  19915. * @param {string} action
  19916. * @returns void
  19917. */
  19918. unbind: function(keys, action) {
  19919. if (_direct_map[keys + ':' + action]) {
  19920. delete _direct_map[keys + ':' + action];
  19921. this.bind(keys, function() {}, action);
  19922. }
  19923. return this;
  19924. },
  19925. /**
  19926. * triggers an event that has already been bound
  19927. *
  19928. * @param {string} keys
  19929. * @param {string=} action
  19930. * @returns void
  19931. */
  19932. trigger: function(keys, action) {
  19933. _direct_map[keys + ':' + action]();
  19934. return this;
  19935. },
  19936. /**
  19937. * resets the library back to its initial state. this is useful
  19938. * if you want to clear out the current keyboard shortcuts and bind
  19939. * new ones - for example if you switch to another page
  19940. *
  19941. * @returns void
  19942. */
  19943. reset: function() {
  19944. _callbacks = {};
  19945. _direct_map = {};
  19946. return this;
  19947. }
  19948. };
  19949. module.exports = mousetrap;
  19950. },{}]},{},[1])
  19951. (1)
  19952. });